Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook) and split all 44 files exceeding 500 LOC into domain-focused modules: - consent-service (Go): models, handlers, services, database splits - backend-core (Python): security_api, rbac_api, pdf_service, auth splits - admin-core (TypeScript): 5 page.tsx + sidebar extractions - pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits - voice-service (Python): enhanced_task_orchestrator split Result: 0 violations, 36 exempted (pipeline, tests, pure-data files). Go build verified clean. No behavior changes — pure structural splits. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
409 lines
12 KiB
Go
409 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/consent-service/internal/models"
|
|
"github.com/breakpilot/consent-service/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// SchoolHandlers contains all school-related HTTP handlers
|
|
type SchoolHandlers struct {
|
|
schoolService *services.SchoolService
|
|
attendanceService *services.AttendanceService
|
|
gradeService *services.GradeService
|
|
}
|
|
|
|
// NewSchoolHandlers creates new school handlers
|
|
func NewSchoolHandlers(schoolService *services.SchoolService, attendanceService *services.AttendanceService, gradeService *services.GradeService) *SchoolHandlers {
|
|
return &SchoolHandlers{
|
|
schoolService: schoolService,
|
|
attendanceService: attendanceService,
|
|
gradeService: gradeService,
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// School Handlers
|
|
// ========================================
|
|
|
|
// CreateSchool creates a new school
|
|
// POST /api/v1/schools
|
|
func (h *SchoolHandlers) CreateSchool(c *gin.Context) {
|
|
var req models.CreateSchoolRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
school, err := h.schoolService.CreateSchool(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, school)
|
|
}
|
|
|
|
// GetSchool retrieves a school by ID
|
|
// GET /api/v1/schools/:id
|
|
func (h *SchoolHandlers) GetSchool(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
school, err := h.schoolService.GetSchool(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, school)
|
|
}
|
|
|
|
// ListSchools lists all schools
|
|
// GET /api/v1/schools
|
|
func (h *SchoolHandlers) ListSchools(c *gin.Context) {
|
|
schools, err := h.schoolService.ListSchools(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, schools)
|
|
}
|
|
|
|
// ========================================
|
|
// School Year Handlers
|
|
// ========================================
|
|
|
|
// CreateSchoolYear creates a new school year
|
|
// POST /api/v1/schools/:id/years
|
|
func (h *SchoolHandlers) CreateSchoolYear(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name" binding:"required"`
|
|
StartDate string `json:"start_date" binding:"required"`
|
|
EndDate string `json:"end_date" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
startDate, err := time.Parse("2006-01-02", req.StartDate)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start date format"})
|
|
return
|
|
}
|
|
|
|
endDate, err := time.Parse("2006-01-02", req.EndDate)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end date format"})
|
|
return
|
|
}
|
|
|
|
schoolYear, err := h.schoolService.CreateSchoolYear(c.Request.Context(), schoolID, req.Name, startDate, endDate)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, schoolYear)
|
|
}
|
|
|
|
// SetCurrentSchoolYear sets a school year as current
|
|
// PUT /api/v1/schools/:id/years/:yearId/current
|
|
func (h *SchoolHandlers) SetCurrentSchoolYear(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
yearIDStr := c.Param("yearId")
|
|
yearID, err := uuid.Parse(yearIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
|
return
|
|
}
|
|
|
|
if err := h.schoolService.SetCurrentSchoolYear(c.Request.Context(), schoolID, yearID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "school year set as current"})
|
|
}
|
|
|
|
// ========================================
|
|
// Class Handlers
|
|
// ========================================
|
|
|
|
// CreateClass creates a new class
|
|
// POST /api/v1/schools/:id/classes
|
|
func (h *SchoolHandlers) CreateClass(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
var req models.CreateClassRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
class, err := h.schoolService.CreateClass(c.Request.Context(), schoolID, req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, class)
|
|
}
|
|
|
|
// GetClass retrieves a class by ID
|
|
// GET /api/v1/classes/:id
|
|
func (h *SchoolHandlers) GetClass(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
|
return
|
|
}
|
|
|
|
class, err := h.schoolService.GetClass(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, class)
|
|
}
|
|
|
|
// ListClasses lists all classes for a school in a school year
|
|
// GET /api/v1/schools/:id/classes?school_year_id=...
|
|
func (h *SchoolHandlers) ListClasses(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
schoolYearIDStr := c.Query("school_year_id")
|
|
if schoolYearIDStr == "" {
|
|
// Get current school year
|
|
schoolYear, err := h.schoolService.GetCurrentSchoolYear(c.Request.Context(), schoolID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no current school year set"})
|
|
return
|
|
}
|
|
schoolYearIDStr = schoolYear.ID.String()
|
|
}
|
|
|
|
schoolYearID, err := uuid.Parse(schoolYearIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
|
return
|
|
}
|
|
|
|
classes, err := h.schoolService.ListClasses(c.Request.Context(), schoolID, schoolYearID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, classes)
|
|
}
|
|
|
|
// ========================================
|
|
// Student Handlers
|
|
// ========================================
|
|
|
|
// CreateStudent creates a new student
|
|
// POST /api/v1/schools/:id/students
|
|
func (h *SchoolHandlers) CreateStudent(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
var req models.CreateStudentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
student, err := h.schoolService.CreateStudent(c.Request.Context(), schoolID, req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, student)
|
|
}
|
|
|
|
// GetStudent retrieves a student by ID
|
|
// GET /api/v1/students/:id
|
|
func (h *SchoolHandlers) GetStudent(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := uuid.Parse(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
|
|
return
|
|
}
|
|
|
|
student, err := h.schoolService.GetStudent(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, student)
|
|
}
|
|
|
|
// ListStudentsByClass lists all students in a class
|
|
// GET /api/v1/classes/:id/students
|
|
func (h *SchoolHandlers) ListStudentsByClass(c *gin.Context) {
|
|
classIDStr := c.Param("id")
|
|
classID, err := uuid.Parse(classIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
|
return
|
|
}
|
|
|
|
students, err := h.schoolService.ListStudentsByClass(c.Request.Context(), classID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, students)
|
|
}
|
|
|
|
// ========================================
|
|
// Subject Handlers
|
|
// ========================================
|
|
|
|
// CreateSubject creates a new subject
|
|
// POST /api/v1/schools/:id/subjects
|
|
func (h *SchoolHandlers) CreateSubject(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name" binding:"required"`
|
|
ShortName string `json:"short_name" binding:"required"`
|
|
Color *string `json:"color"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
subject, err := h.schoolService.CreateSubject(c.Request.Context(), schoolID, req.Name, req.ShortName, req.Color)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, subject)
|
|
}
|
|
|
|
// ListSubjects lists all subjects for a school
|
|
// GET /api/v1/schools/:id/subjects
|
|
func (h *SchoolHandlers) ListSubjects(c *gin.Context) {
|
|
schoolIDStr := c.Param("id")
|
|
schoolID, err := uuid.Parse(schoolIDStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
|
return
|
|
}
|
|
|
|
subjects, err := h.schoolService.ListSubjects(c.Request.Context(), schoolID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subjects)
|
|
}
|
|
|
|
// ========================================
|
|
// Register Routes
|
|
// ========================================
|
|
|
|
// RegisterRoutes registers all school-related routes
|
|
func (h *SchoolHandlers) RegisterRoutes(r *gin.RouterGroup, authMiddleware gin.HandlerFunc) {
|
|
// Public routes (for onboarding)
|
|
r.GET("/onboarding/validate", h.ValidateOnboardingToken)
|
|
|
|
// Protected routes
|
|
protected := r.Group("")
|
|
protected.Use(authMiddleware)
|
|
|
|
// Schools
|
|
protected.POST("/schools", h.CreateSchool)
|
|
protected.GET("/schools", h.ListSchools)
|
|
protected.GET("/schools/:id", h.GetSchool)
|
|
protected.POST("/schools/:id/years", h.CreateSchoolYear)
|
|
protected.PUT("/schools/:id/years/:yearId/current", h.SetCurrentSchoolYear)
|
|
protected.POST("/schools/:id/classes", h.CreateClass)
|
|
protected.GET("/schools/:id/classes", h.ListClasses)
|
|
protected.POST("/schools/:id/students", h.CreateStudent)
|
|
protected.POST("/schools/:id/subjects", h.CreateSubject)
|
|
protected.GET("/schools/:id/subjects", h.ListSubjects)
|
|
|
|
// Classes
|
|
protected.GET("/classes/:id", h.GetClass)
|
|
protected.GET("/classes/:id/students", h.ListStudentsByClass)
|
|
protected.GET("/classes/:id/attendance", h.GetClassAttendance)
|
|
protected.POST("/classes/:id/attendance", h.RecordBulkAttendance)
|
|
protected.GET("/classes/:id/absence/pending", h.GetPendingAbsenceReports)
|
|
protected.GET("/classes/:id/grades/:subjectId", h.GetClassGrades)
|
|
protected.GET("/classes/:id/grades/:subjectId/stats", h.GetGradeStatistics)
|
|
|
|
// Students
|
|
protected.GET("/students/:id", h.GetStudent)
|
|
protected.GET("/students/:id/attendance", h.GetStudentAttendance)
|
|
protected.GET("/students/:id/grades", h.GetStudentGrades)
|
|
|
|
// Attendance & Absence
|
|
protected.POST("/attendance", h.RecordAttendance)
|
|
protected.POST("/absence/report", h.ReportAbsence)
|
|
protected.PUT("/absence/:id/confirm", h.ConfirmAbsence)
|
|
|
|
// Grades
|
|
protected.POST("/grades", h.CreateGrade)
|
|
|
|
// Onboarding
|
|
protected.POST("/onboarding/tokens", h.GenerateOnboardingToken)
|
|
protected.POST("/onboarding/redeem", h.RedeemOnboardingToken)
|
|
}
|