Files
breakpilot-core/consent-service/internal/services/school_service.go
Benjamin Boenisch ad111d5e69 Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:13 +01:00

699 lines
22 KiB
Go

package services
import (
"context"
"crypto/rand"
"encoding/hex"
"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
}
// ========================================
// Student Management
// ========================================
// CreateStudent creates a new student
func (s *SchoolService) CreateStudent(ctx context.Context, schoolID uuid.UUID, req models.CreateStudentRequest) (*models.Student, error) {
classID, err := uuid.Parse(req.ClassID)
if err != nil {
return nil, fmt.Errorf("invalid class ID: %w", err)
}
student := &models.Student{
ID: uuid.New(),
SchoolID: schoolID,
ClassID: classID,
StudentNumber: req.StudentNumber,
FirstName: req.FirstName,
LastName: req.LastName,
Gender: req.Gender,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if req.DateOfBirth != nil {
dob, err := time.Parse("2006-01-02", *req.DateOfBirth)
if err == nil {
student.DateOfBirth = &dob
}
}
query := `
INSERT INTO students (id, school_id, class_id, student_number, first_name, last_name, date_of_birth, gender, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id`
err = s.db.Pool.QueryRow(ctx, query,
student.ID, student.SchoolID, student.ClassID, student.StudentNumber,
student.FirstName, student.LastName, student.DateOfBirth, student.Gender,
student.IsActive, student.CreatedAt, student.UpdatedAt,
).Scan(&student.ID)
if err != nil {
return nil, fmt.Errorf("failed to create student: %w", err)
}
return student, nil
}
// GetStudent retrieves a student by ID
func (s *SchoolService) GetStudent(ctx context.Context, studentID uuid.UUID) (*models.Student, error) {
query := `
SELECT id, school_id, class_id, user_id, student_number, first_name, last_name, date_of_birth, gender, matrix_user_id, matrix_dm_room, is_active, created_at, updated_at
FROM students
WHERE id = $1`
student := &models.Student{}
err := s.db.Pool.QueryRow(ctx, query, studentID).Scan(
&student.ID, &student.SchoolID, &student.ClassID, &student.UserID,
&student.StudentNumber, &student.FirstName, &student.LastName,
&student.DateOfBirth, &student.Gender, &student.MatrixUserID,
&student.MatrixDMRoom, &student.IsActive, &student.CreatedAt, &student.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get student: %w", err)
}
return student, nil
}
// ListStudentsByClass lists all students in a class
func (s *SchoolService) ListStudentsByClass(ctx context.Context, classID uuid.UUID) ([]models.Student, error) {
query := `
SELECT id, school_id, class_id, user_id, student_number, first_name, last_name, date_of_birth, gender, matrix_user_id, matrix_dm_room, is_active, created_at, updated_at
FROM students
WHERE class_id = $1 AND is_active = true
ORDER BY last_name, first_name`
rows, err := s.db.Pool.Query(ctx, query, classID)
if err != nil {
return nil, fmt.Errorf("failed to list students: %w", err)
}
defer rows.Close()
var students []models.Student
for rows.Next() {
var student models.Student
err := rows.Scan(
&student.ID, &student.SchoolID, &student.ClassID, &student.UserID,
&student.StudentNumber, &student.FirstName, &student.LastName,
&student.DateOfBirth, &student.Gender, &student.MatrixUserID,
&student.MatrixDMRoom, &student.IsActive, &student.CreatedAt, &student.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan student: %w", err)
}
students = append(students, student)
}
return students, nil
}
// ========================================
// Teacher Management
// ========================================
// CreateTeacher creates a new teacher linked to a user account
func (s *SchoolService) CreateTeacher(ctx context.Context, schoolID, userID uuid.UUID, firstName, lastName string, teacherCode, title *string) (*models.Teacher, error) {
teacher := &models.Teacher{
ID: uuid.New(),
SchoolID: schoolID,
UserID: userID,
TeacherCode: teacherCode,
Title: title,
FirstName: firstName,
LastName: lastName,
IsActive: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `
INSERT INTO teachers (id, school_id, user_id, teacher_code, title, first_name, last_name, 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,
teacher.ID, teacher.SchoolID, teacher.UserID, teacher.TeacherCode,
teacher.Title, teacher.FirstName, teacher.LastName,
teacher.IsActive, teacher.CreatedAt, teacher.UpdatedAt,
).Scan(&teacher.ID)
if err != nil {
return nil, fmt.Errorf("failed to create teacher: %w", err)
}
return teacher, nil
}
// GetTeacher retrieves a teacher by ID
func (s *SchoolService) GetTeacher(ctx context.Context, teacherID uuid.UUID) (*models.Teacher, error) {
query := `
SELECT id, school_id, user_id, teacher_code, title, first_name, last_name, matrix_user_id, is_active, created_at, updated_at
FROM teachers
WHERE id = $1`
teacher := &models.Teacher{}
err := s.db.Pool.QueryRow(ctx, query, teacherID).Scan(
&teacher.ID, &teacher.SchoolID, &teacher.UserID, &teacher.TeacherCode,
&teacher.Title, &teacher.FirstName, &teacher.LastName, &teacher.MatrixUserID,
&teacher.IsActive, &teacher.CreatedAt, &teacher.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get teacher: %w", err)
}
return teacher, nil
}
// GetTeacherByUserID retrieves a teacher by their user ID
func (s *SchoolService) GetTeacherByUserID(ctx context.Context, userID uuid.UUID) (*models.Teacher, error) {
query := `
SELECT id, school_id, user_id, teacher_code, title, first_name, last_name, matrix_user_id, is_active, created_at, updated_at
FROM teachers
WHERE user_id = $1 AND is_active = true`
teacher := &models.Teacher{}
err := s.db.Pool.QueryRow(ctx, query, userID).Scan(
&teacher.ID, &teacher.SchoolID, &teacher.UserID, &teacher.TeacherCode,
&teacher.Title, &teacher.FirstName, &teacher.LastName, &teacher.MatrixUserID,
&teacher.IsActive, &teacher.CreatedAt, &teacher.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to get teacher by user ID: %w", err)
}
return teacher, nil
}
// AssignClassTeacher assigns a teacher to a class
func (s *SchoolService) AssignClassTeacher(ctx context.Context, classID, teacherID uuid.UUID, isPrimary bool) error {
query := `
INSERT INTO class_teachers (id, class_id, teacher_id, is_primary, created_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (class_id, teacher_id) DO UPDATE SET is_primary = EXCLUDED.is_primary`
_, err := s.db.Pool.Exec(ctx, query, uuid.New(), classID, teacherID, isPrimary, time.Now())
if err != nil {
return fmt.Errorf("failed to assign class teacher: %w", err)
}
return nil
}
// ========================================
// Subject Management
// ========================================
// CreateSubject creates a new subject
func (s *SchoolService) CreateSubject(ctx context.Context, schoolID uuid.UUID, name, shortName string, color *string) (*models.Subject, error) {
subject := &models.Subject{
ID: uuid.New(),
SchoolID: schoolID,
Name: name,
ShortName: shortName,
Color: color,
IsActive: true,
CreatedAt: time.Now(),
}
query := `
INSERT INTO subjects (id, school_id, name, short_name, color, is_active, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id`
err := s.db.Pool.QueryRow(ctx, query,
subject.ID, subject.SchoolID, subject.Name, subject.ShortName,
subject.Color, subject.IsActive, subject.CreatedAt,
).Scan(&subject.ID)
if err != nil {
return nil, fmt.Errorf("failed to create subject: %w", err)
}
return subject, nil
}
// ListSubjects lists all subjects for a school
func (s *SchoolService) ListSubjects(ctx context.Context, schoolID uuid.UUID) ([]models.Subject, error) {
query := `
SELECT id, school_id, name, short_name, color, is_active, created_at
FROM subjects
WHERE school_id = $1 AND is_active = true
ORDER BY name`
rows, err := s.db.Pool.Query(ctx, query, schoolID)
if err != nil {
return nil, fmt.Errorf("failed to list subjects: %w", err)
}
defer rows.Close()
var subjects []models.Subject
for rows.Next() {
var subject models.Subject
err := rows.Scan(
&subject.ID, &subject.SchoolID, &subject.Name, &subject.ShortName,
&subject.Color, &subject.IsActive, &subject.CreatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan subject: %w", err)
}
subjects = append(subjects, subject)
}
return subjects, nil
}
// ========================================
// Parent Onboarding
// ========================================
// GenerateParentOnboardingToken generates a QR code token for parent onboarding
func (s *SchoolService) GenerateParentOnboardingToken(ctx context.Context, schoolID, classID, studentID, createdByUserID uuid.UUID, role string) (*models.ParentOnboardingToken, error) {
// Generate secure random token
tokenBytes := make([]byte, 32)
if _, err := rand.Read(tokenBytes); err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
token := hex.EncodeToString(tokenBytes)
onboardingToken := &models.ParentOnboardingToken{
ID: uuid.New(),
SchoolID: schoolID,
ClassID: classID,
StudentID: studentID,
Token: token,
Role: role,
ExpiresAt: time.Now().Add(72 * time.Hour), // Valid for 72 hours
CreatedAt: time.Now(),
CreatedBy: createdByUserID,
}
query := `
INSERT INTO parent_onboarding_tokens (id, school_id, class_id, student_id, token, role, expires_at, created_at, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id`
err := s.db.Pool.QueryRow(ctx, query,
onboardingToken.ID, onboardingToken.SchoolID, onboardingToken.ClassID,
onboardingToken.StudentID, onboardingToken.Token, onboardingToken.Role,
onboardingToken.ExpiresAt, onboardingToken.CreatedAt, onboardingToken.CreatedBy,
).Scan(&onboardingToken.ID)
if err != nil {
return nil, fmt.Errorf("failed to create onboarding token: %w", err)
}
return onboardingToken, nil
}
// ValidateOnboardingToken validates and retrieves info for an onboarding token
func (s *SchoolService) ValidateOnboardingToken(ctx context.Context, token string) (*models.ParentOnboardingToken, error) {
query := `
SELECT id, school_id, class_id, student_id, token, role, expires_at, used_at, used_by_user_id, created_at, created_by
FROM parent_onboarding_tokens
WHERE token = $1 AND used_at IS NULL AND expires_at > NOW()`
onboardingToken := &models.ParentOnboardingToken{}
err := s.db.Pool.QueryRow(ctx, query, token).Scan(
&onboardingToken.ID, &onboardingToken.SchoolID, &onboardingToken.ClassID,
&onboardingToken.StudentID, &onboardingToken.Token, &onboardingToken.Role,
&onboardingToken.ExpiresAt, &onboardingToken.UsedAt, &onboardingToken.UsedByUserID,
&onboardingToken.CreatedAt, &onboardingToken.CreatedBy,
)
if err != nil {
return nil, fmt.Errorf("invalid or expired token: %w", err)
}
return onboardingToken, nil
}
// RedeemOnboardingToken marks a token as used and creates the parent account
func (s *SchoolService) RedeemOnboardingToken(ctx context.Context, token string, userID uuid.UUID) error {
query := `
UPDATE parent_onboarding_tokens
SET used_at = NOW(), used_by_user_id = $1
WHERE token = $2 AND used_at IS NULL AND expires_at > NOW()`
result, err := s.db.Pool.Exec(ctx, query, userID, token)
if err != nil {
return fmt.Errorf("failed to redeem token: %w", err)
}
if result.RowsAffected() == 0 {
return fmt.Errorf("token not found or already used")
}
return 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
}