[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>
This commit is contained in:
@@ -2,8 +2,6 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -35,21 +33,21 @@ func NewSchoolService(db *database.DB, matrixService *matrix.MatrixService) *Sch
|
||||
// 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,
|
||||
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(),
|
||||
State: req.State,
|
||||
Country: "DE",
|
||||
Phone: req.Phone,
|
||||
Email: req.Email,
|
||||
Website: req.Website,
|
||||
IsActive: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
query := `
|
||||
@@ -298,350 +296,6 @@ func (s *SchoolService) ListClasses(ctx context.Context, schoolID, schoolYearID
|
||||
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
|
||||
// ========================================
|
||||
|
||||
Reference in New Issue
Block a user