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>
699 lines
22 KiB
Go
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
|
|
}
|