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>
353 lines
11 KiB
Go
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
|
|
}
|