Files
breakpilot-core/consent-service/internal/services/school_service.go
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
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>
2026-04-27 00:09:30 +02:00

353 lines
11 KiB
Go

package services
import (
"context"
"fmt"
"time"
"github.com/breakpilot/consent-service/internal/database"
"github.com/breakpilot/consent-service/internal/models"
"github.com/breakpilot/consent-service/internal/services/matrix"
"github.com/google/uuid"
)
// SchoolService handles school management operations
type SchoolService struct {
db *database.DB
matrix *matrix.MatrixService
}
// NewSchoolService creates a new school service
func NewSchoolService(db *database.DB, matrixService *matrix.MatrixService) *SchoolService {
return &SchoolService{
db: db,
matrix: matrixService,
}
}
// ========================================
// School CRUD
// ========================================
// CreateSchool creates a new school
func (s *SchoolService) CreateSchool(ctx context.Context, req models.CreateSchoolRequest) (*models.School, error) {
school := &models.School{
ID: uuid.New(),
Name: req.Name,
ShortName: req.ShortName,
Type: req.Type,
Address: req.Address,
City: req.City,
PostalCode: req.PostalCode,
State: req.State,
Country: "DE",
Phone: req.Phone,
Email: req.Email,
Website: req.Website,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `
INSERT INTO schools (id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
RETURNING id`
err := s.db.Pool.QueryRow(ctx, query,
school.ID, school.Name, school.ShortName, school.Type, school.Address,
school.City, school.PostalCode, school.State, school.Country, school.Phone,
school.Email, school.Website, school.IsActive, school.CreatedAt, school.UpdatedAt,
).Scan(&school.ID)
if err != nil {
return nil, fmt.Errorf("failed to create school: %w", err)
}
// Create default timetable slots for the school
if err := s.createDefaultTimetableSlots(ctx, school.ID); err != nil {
// Log but don't fail
fmt.Printf("Warning: failed to create default timetable slots: %v\n", err)
}
// Create default grade scale
if err := s.createDefaultGradeScale(ctx, school.ID); err != nil {
fmt.Printf("Warning: failed to create default grade scale: %v\n", err)
}
return school, nil
}
// GetSchool retrieves a school by ID
func (s *SchoolService) GetSchool(ctx context.Context, schoolID uuid.UUID) (*models.School, error) {
query := `
SELECT id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, matrix_server_name, logo_url, is_active, created_at, updated_at
FROM schools
WHERE id = $1`
school := &models.School{}
err := s.db.Pool.QueryRow(ctx, query, schoolID).Scan(
&school.ID, &school.Name, &school.ShortName, &school.Type, &school.Address,
&school.City, &school.PostalCode, &school.State, &school.Country, &school.Phone,
&school.Email, &school.Website, &school.MatrixServerName, &school.LogoURL,
&school.IsActive, &school.CreatedAt, &school.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get school: %w", err)
}
return school, nil
}
// ListSchools lists all active schools
func (s *SchoolService) ListSchools(ctx context.Context) ([]models.School, error) {
query := `
SELECT id, name, short_name, type, address, city, postal_code, state, country, phone, email, website, matrix_server_name, logo_url, is_active, created_at, updated_at
FROM schools
WHERE is_active = true
ORDER BY name`
rows, err := s.db.Pool.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list schools: %w", err)
}
defer rows.Close()
var schools []models.School
for rows.Next() {
var school models.School
err := rows.Scan(
&school.ID, &school.Name, &school.ShortName, &school.Type, &school.Address,
&school.City, &school.PostalCode, &school.State, &school.Country, &school.Phone,
&school.Email, &school.Website, &school.MatrixServerName, &school.LogoURL,
&school.IsActive, &school.CreatedAt, &school.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan school: %w", err)
}
schools = append(schools, school)
}
return schools, nil
}
// ========================================
// School Year Management
// ========================================
// CreateSchoolYear creates a new school year
func (s *SchoolService) CreateSchoolYear(ctx context.Context, schoolID uuid.UUID, name string, startDate, endDate time.Time) (*models.SchoolYear, error) {
schoolYear := &models.SchoolYear{
ID: uuid.New(),
SchoolID: schoolID,
Name: name,
StartDate: startDate,
EndDate: endDate,
IsCurrent: false,
CreatedAt: time.Now(),
}
query := `
INSERT INTO school_years (id, school_id, name, start_date, end_date, is_current, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id`
err := s.db.Pool.QueryRow(ctx, query,
schoolYear.ID, schoolYear.SchoolID, schoolYear.Name,
schoolYear.StartDate, schoolYear.EndDate, schoolYear.IsCurrent, schoolYear.CreatedAt,
).Scan(&schoolYear.ID)
if err != nil {
return nil, fmt.Errorf("failed to create school year: %w", err)
}
return schoolYear, nil
}
// SetCurrentSchoolYear sets a school year as the current one
func (s *SchoolService) SetCurrentSchoolYear(ctx context.Context, schoolID, schoolYearID uuid.UUID) error {
// First, unset all current school years for this school
_, err := s.db.Pool.Exec(ctx, `UPDATE school_years SET is_current = false WHERE school_id = $1`, schoolID)
if err != nil {
return fmt.Errorf("failed to unset current school years: %w", err)
}
// Then set the specified school year as current
_, err = s.db.Pool.Exec(ctx, `UPDATE school_years SET is_current = true WHERE id = $1 AND school_id = $2`, schoolYearID, schoolID)
if err != nil {
return fmt.Errorf("failed to set current school year: %w", err)
}
return nil
}
// GetCurrentSchoolYear gets the current school year for a school
func (s *SchoolService) GetCurrentSchoolYear(ctx context.Context, schoolID uuid.UUID) (*models.SchoolYear, error) {
query := `
SELECT id, school_id, name, start_date, end_date, is_current, created_at
FROM school_years
WHERE school_id = $1 AND is_current = true`
schoolYear := &models.SchoolYear{}
err := s.db.Pool.QueryRow(ctx, query, schoolID).Scan(
&schoolYear.ID, &schoolYear.SchoolID, &schoolYear.Name,
&schoolYear.StartDate, &schoolYear.EndDate, &schoolYear.IsCurrent, &schoolYear.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get current school year: %w", err)
}
return schoolYear, nil
}
// ========================================
// Class Management
// ========================================
// CreateClass creates a new class
func (s *SchoolService) CreateClass(ctx context.Context, schoolID uuid.UUID, req models.CreateClassRequest) (*models.Class, error) {
schoolYearID, err := uuid.Parse(req.SchoolYearID)
if err != nil {
return nil, fmt.Errorf("invalid school year ID: %w", err)
}
class := &models.Class{
ID: uuid.New(),
SchoolID: schoolID,
SchoolYearID: schoolYearID,
Name: req.Name,
Grade: req.Grade,
Section: req.Section,
Room: req.Room,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `
INSERT INTO classes (id, school_id, school_year_id, name, grade, section, room, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id`
err = s.db.Pool.QueryRow(ctx, query,
class.ID, class.SchoolID, class.SchoolYearID, class.Name,
class.Grade, class.Section, class.Room, class.IsActive, class.CreatedAt, class.UpdatedAt,
).Scan(&class.ID)
if err != nil {
return nil, fmt.Errorf("failed to create class: %w", err)
}
return class, nil
}
// GetClass retrieves a class by ID
func (s *SchoolService) GetClass(ctx context.Context, classID uuid.UUID) (*models.Class, error) {
query := `
SELECT id, school_id, school_year_id, name, grade, section, room, matrix_info_room, matrix_rep_room, is_active, created_at, updated_at
FROM classes
WHERE id = $1`
class := &models.Class{}
err := s.db.Pool.QueryRow(ctx, query, classID).Scan(
&class.ID, &class.SchoolID, &class.SchoolYearID, &class.Name,
&class.Grade, &class.Section, &class.Room, &class.MatrixInfoRoom,
&class.MatrixRepRoom, &class.IsActive, &class.CreatedAt, &class.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get class: %w", err)
}
return class, nil
}
// ListClasses lists all classes for a school in a school year
func (s *SchoolService) ListClasses(ctx context.Context, schoolID, schoolYearID uuid.UUID) ([]models.Class, error) {
query := `
SELECT id, school_id, school_year_id, name, grade, section, room, matrix_info_room, matrix_rep_room, is_active, created_at, updated_at
FROM classes
WHERE school_id = $1 AND school_year_id = $2 AND is_active = true
ORDER BY grade, name`
rows, err := s.db.Pool.Query(ctx, query, schoolID, schoolYearID)
if err != nil {
return nil, fmt.Errorf("failed to list classes: %w", err)
}
defer rows.Close()
var classes []models.Class
for rows.Next() {
var class models.Class
err := rows.Scan(
&class.ID, &class.SchoolID, &class.SchoolYearID, &class.Name,
&class.Grade, &class.Section, &class.Room, &class.MatrixInfoRoom,
&class.MatrixRepRoom, &class.IsActive, &class.CreatedAt, &class.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan class: %w", err)
}
classes = append(classes, class)
}
return classes, nil
}
// ========================================
// Helper Functions
// ========================================
func (s *SchoolService) createDefaultTimetableSlots(ctx context.Context, schoolID uuid.UUID) error {
slots := []struct {
Number int
StartTime string
EndTime string
IsBreak bool
Name string
}{
{1, "08:00", "08:45", false, "1. Stunde"},
{2, "08:45", "09:30", false, "2. Stunde"},
{3, "09:30", "09:50", true, "Erste Pause"},
{4, "09:50", "10:35", false, "3. Stunde"},
{5, "10:35", "11:20", false, "4. Stunde"},
{6, "11:20", "11:40", true, "Zweite Pause"},
{7, "11:40", "12:25", false, "5. Stunde"},
{8, "12:25", "13:10", false, "6. Stunde"},
{9, "13:10", "14:00", true, "Mittagspause"},
{10, "14:00", "14:45", false, "7. Stunde"},
{11, "14:45", "15:30", false, "8. Stunde"},
}
query := `
INSERT INTO timetable_slots (id, school_id, slot_number, start_time, end_time, is_break, name)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (school_id, slot_number) DO NOTHING`
for _, slot := range slots {
_, err := s.db.Pool.Exec(ctx, query,
uuid.New(), schoolID, slot.Number, slot.StartTime, slot.EndTime, slot.IsBreak, slot.Name,
)
if err != nil {
return err
}
}
return nil
}
func (s *SchoolService) createDefaultGradeScale(ctx context.Context, schoolID uuid.UUID) error {
query := `
INSERT INTO grade_scales (id, school_id, name, min_value, max_value, passing_value, is_ascending, is_default, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT DO NOTHING`
_, err := s.db.Pool.Exec(ctx, query,
uuid.New(), schoolID, "1-6 (Noten)", 1.0, 6.0, 4.0, false, true, time.Now(),
)
return err
}