Add timetable scheduler Phases 1 + 2 to school-service
Phase 1 — Stammdaten (7 tables):
tt_class, tt_period, tt_room, tt_subject, tt_teacher,
tt_curriculum, tt_assignment with CRUD endpoints.
Phase 2 — Constraints (15 typed tables):
Teacher (6): unavailable_day, unavailable_window, max_hours_day,
max_hours_week, excluded_subject, excluded_room
Subject (5): min_day_gap, max_consecutive, contiguous_when_repeated,
preferred_period, double_lesson
Class (2): max_hours_day, no_gaps
Room (2): requires_type, unavailable
Each constraint row carries is_hard / weight / active / note /
created_by_user_id; ownership enforced via WHERE EXISTS against the
parent tt_teacher/tt_class/tt_subject/tt_room row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// Class- and room-scoped constraint CRUD. Ownership is via tt_class /
|
||||
// tt_subject / tt_room.created_by_user_id.
|
||||
|
||||
// ---------- Class Max Hours / Day ----------
|
||||
|
||||
func (s *TimetableService) CreateClassMaxHoursDay(ctx context.Context, userID string, req *models.CreateClassMaxHoursDayRequest) (*models.ClassMaxHoursDay, error) {
|
||||
var c models.ClassMaxHoursDay
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_class_max_hours_day
|
||||
(created_by_user_id, class_id, max_hours, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, class_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.ClassID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.ClassID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListClassMaxHoursDay(ctx context.Context, userID string) ([]models.ClassMaxHoursDay, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, class_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_class_max_hours_day WHERE created_by_user_id = $1 ORDER BY class_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.ClassMaxHoursDay
|
||||
for rows.Next() {
|
||||
var c models.ClassMaxHoursDay
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.ClassID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteClassMaxHoursDay(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_class_max_hours_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Class No Gaps ----------
|
||||
|
||||
func (s *TimetableService) CreateClassNoGaps(ctx context.Context, userID string, req *models.CreateClassNoGapsRequest) (*models.ClassNoGaps, error) {
|
||||
var c models.ClassNoGaps
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_class_no_gaps
|
||||
(created_by_user_id, class_id, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6
|
||||
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, class_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.ClassID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.ClassID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListClassNoGaps(ctx context.Context, userID string) ([]models.ClassNoGaps, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, class_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_class_no_gaps WHERE created_by_user_id = $1 ORDER BY class_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.ClassNoGaps
|
||||
for rows.Next() {
|
||||
var c models.ClassNoGaps
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.ClassID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteClassNoGaps(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_class_no_gaps WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Room Requires Type ----------
|
||||
|
||||
func (s *TimetableService) CreateRoomRequiresType(ctx context.Context, userID string, req *models.CreateRoomRequiresTypeRequest) (*models.RoomRequiresType, error) {
|
||||
var c models.RoomRequiresType
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_room_requires_type
|
||||
(created_by_user_id, subject_id, room_type, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, room_type, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.RoomType, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.RoomType, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListRoomRequiresTypes(ctx context.Context, userID string) ([]models.RoomRequiresType, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, room_type, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_room_requires_type WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.RoomRequiresType
|
||||
for rows.Next() {
|
||||
var c models.RoomRequiresType
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.RoomType, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteRoomRequiresType(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_room_requires_type WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Room Unavailable ----------
|
||||
|
||||
func (s *TimetableService) CreateRoomUnavailable(ctx context.Context, userID string, req *models.CreateRoomUnavailableRequest) (*models.RoomUnavailable, error) {
|
||||
var c models.RoomUnavailable
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_room_unavailable
|
||||
(created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8
|
||||
WHERE EXISTS (SELECT 1 FROM tt_room WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.RoomID, req.DayOfWeek, req.PeriodIndex, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.RoomID, &c.DayOfWeek, &c.PeriodIndex, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListRoomUnavailable(ctx context.Context, userID string) ([]models.RoomUnavailable, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, room_id, day_of_week, period_index, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_room_unavailable WHERE created_by_user_id = $1 ORDER BY room_id, day_of_week, period_index
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.RoomUnavailable
|
||||
for rows.Next() {
|
||||
var c models.RoomUnavailable
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.RoomID, &c.DayOfWeek, &c.PeriodIndex, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteRoomUnavailable(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_room_unavailable WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// Subject-scoped constraint CRUD. Ownership via tt_subject.created_by_user_id.
|
||||
|
||||
// ---------- Subject Min Day Gap ----------
|
||||
|
||||
func (s *TimetableService) CreateSubjectMinDayGap(ctx context.Context, userID string, req *models.CreateSubjectMinDayGapRequest) (*models.SubjectMinDayGap, error) {
|
||||
var c models.SubjectMinDayGap
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_subject_min_day_gap
|
||||
(created_by_user_id, subject_id, min_gap_days, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, min_gap_days, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.MinGapDays, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.MinGapDays, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjectMinDayGaps(ctx context.Context, userID string) ([]models.SubjectMinDayGap, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, min_gap_days, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_subject_min_day_gap WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.SubjectMinDayGap
|
||||
for rows.Next() {
|
||||
var c models.SubjectMinDayGap
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.MinGapDays, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubjectMinDayGap(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_subject_min_day_gap WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Subject Max Consecutive ----------
|
||||
|
||||
func (s *TimetableService) CreateSubjectMaxConsecutive(ctx context.Context, userID string, req *models.CreateSubjectMaxConsecutiveRequest) (*models.SubjectMaxConsecutive, error) {
|
||||
var c models.SubjectMaxConsecutive
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_subject_max_consecutive
|
||||
(created_by_user_id, subject_id, max_consecutive, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, max_consecutive, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.MaxConsecutive, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.MaxConsecutive, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjectMaxConsecutives(ctx context.Context, userID string) ([]models.SubjectMaxConsecutive, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, max_consecutive, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_subject_max_consecutive WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.SubjectMaxConsecutive
|
||||
for rows.Next() {
|
||||
var c models.SubjectMaxConsecutive
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.MaxConsecutive, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubjectMaxConsecutive(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_subject_max_consecutive WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Subject Contiguous When Repeated ----------
|
||||
|
||||
func (s *TimetableService) CreateSubjectContiguousWhenRepeated(ctx context.Context, userID string, req *models.CreateSubjectContiguousWhenRepeatedRequest) (*models.SubjectContiguousWhenRepeated, error) {
|
||||
var c models.SubjectContiguousWhenRepeated
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_subject_contiguous_when_repeated
|
||||
(created_by_user_id, subject_id, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjectContiguousWhenRepeated(ctx context.Context, userID string) ([]models.SubjectContiguousWhenRepeated, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_subject_contiguous_when_repeated WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.SubjectContiguousWhenRepeated
|
||||
for rows.Next() {
|
||||
var c models.SubjectContiguousWhenRepeated
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubjectContiguousWhenRepeated(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_subject_contiguous_when_repeated WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Subject Preferred Period ----------
|
||||
|
||||
func (s *TimetableService) CreateSubjectPreferredPeriod(ctx context.Context, userID string, req *models.CreateSubjectPreferredPeriodRequest) (*models.SubjectPreferredPeriod, error) {
|
||||
var c models.SubjectPreferredPeriod
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_subject_preferred_period
|
||||
(created_by_user_id, subject_id, period_from, period_to, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, period_from, period_to, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.PeriodFrom, req.PeriodTo, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.PeriodFrom, &c.PeriodTo, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjectPreferredPeriods(ctx context.Context, userID string) ([]models.SubjectPreferredPeriod, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, period_from, period_to, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_subject_preferred_period WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.SubjectPreferredPeriod
|
||||
for rows.Next() {
|
||||
var c models.SubjectPreferredPeriod
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.PeriodFrom, &c.PeriodTo, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubjectPreferredPeriod(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_subject_preferred_period WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Subject Double Lesson ----------
|
||||
|
||||
func (s *TimetableService) CreateSubjectDoubleLesson(ctx context.Context, userID string, req *models.CreateSubjectDoubleLessonRequest) (*models.SubjectDoubleLesson, error) {
|
||||
var c models.SubjectDoubleLesson
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_subject_double_lesson
|
||||
(created_by_user_id, subject_id, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6
|
||||
WHERE EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.SubjectID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjectDoubleLessons(ctx context.Context, userID string) ([]models.SubjectDoubleLesson, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_subject_double_lesson WHERE created_by_user_id = $1 ORDER BY subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.SubjectDoubleLesson
|
||||
for rows.Next() {
|
||||
var c models.SubjectDoubleLesson
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubjectDoubleLesson(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_subject_double_lesson WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// Teacher-scoped constraint CRUD. Ownership is enforced via the parent
|
||||
// tt_teacher row's created_by_user_id (and tt_subject / tt_room for the
|
||||
// composite excluded-* constraints).
|
||||
|
||||
// ---------- Teacher Unavailable Day ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherUnavailableDay(ctx context.Context, userID string, req *models.CreateTeacherUnavailableDayRequest) (*models.TeacherUnavailableDay, error) {
|
||||
var c models.TeacherUnavailableDay
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_unavailable_day
|
||||
(created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.DayOfWeek, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherUnavailableDays(ctx context.Context, userID string) ([]models.TeacherUnavailableDay, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, day_of_week, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_unavailable_day
|
||||
WHERE created_by_user_id = $1
|
||||
ORDER BY teacher_id, day_of_week
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherUnavailableDay
|
||||
for rows.Next() {
|
||||
var c models.TeacherUnavailableDay
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherUnavailableDay(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_unavailable_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teacher Unavailable Window ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherUnavailableWindow(ctx context.Context, userID string, req *models.CreateTeacherUnavailableWindowRequest) (*models.TeacherUnavailableWindow, error) {
|
||||
var c models.TeacherUnavailableWindow
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_unavailable_window
|
||||
(created_by_user_id, teacher_id, day_of_week, start_time, end_time, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, day_of_week, start_time::text, end_time::text, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.DayOfWeek, req.StartTime, req.EndTime, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.StartTime, &c.EndTime, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherUnavailableWindows(ctx context.Context, userID string) ([]models.TeacherUnavailableWindow, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, day_of_week, start_time::text, end_time::text, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_unavailable_window
|
||||
WHERE created_by_user_id = $1
|
||||
ORDER BY teacher_id, day_of_week, start_time
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherUnavailableWindow
|
||||
for rows.Next() {
|
||||
var c models.TeacherUnavailableWindow
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.DayOfWeek, &c.StartTime, &c.EndTime, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherUnavailableWindow(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_unavailable_window WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teacher Max Hours / Day ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherMaxHoursDay(ctx context.Context, userID string, req *models.CreateTeacherMaxHoursDayRequest) (*models.TeacherMaxHoursDay, error) {
|
||||
var c models.TeacherMaxHoursDay
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_max_hours_day
|
||||
(created_by_user_id, teacher_id, max_hours, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherMaxHoursDay(ctx context.Context, userID string) ([]models.TeacherMaxHoursDay, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_max_hours_day WHERE created_by_user_id = $1 ORDER BY teacher_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherMaxHoursDay
|
||||
for rows.Next() {
|
||||
var c models.TeacherMaxHoursDay
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherMaxHoursDay(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_max_hours_day WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teacher Max Hours / Week ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherMaxHoursWeek(ctx context.Context, userID string, req *models.CreateTeacherMaxHoursWeekRequest) (*models.TeacherMaxHoursWeek, error) {
|
||||
var c models.TeacherMaxHoursWeek
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_max_hours_week
|
||||
(created_by_user_id, teacher_id, max_hours, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.MaxHours, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherMaxHoursWeek(ctx context.Context, userID string) ([]models.TeacherMaxHoursWeek, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, max_hours, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_max_hours_week WHERE created_by_user_id = $1 ORDER BY teacher_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherMaxHoursWeek
|
||||
for rows.Next() {
|
||||
var c models.TeacherMaxHoursWeek
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.MaxHours, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherMaxHoursWeek(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_max_hours_week WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teacher Excluded Subject ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherExcludedSubject(ctx context.Context, userID string, req *models.CreateTeacherExcludedSubjectRequest) (*models.TeacherExcludedSubject, error) {
|
||||
var c models.TeacherExcludedSubject
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_excluded_subject
|
||||
(created_by_user_id, teacher_id, subject_id, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $3 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.SubjectID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherExcludedSubjects(ctx context.Context, userID string) ([]models.TeacherExcludedSubject, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, subject_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_excluded_subject WHERE created_by_user_id = $1 ORDER BY teacher_id, subject_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherExcludedSubject
|
||||
for rows.Next() {
|
||||
var c models.TeacherExcludedSubject
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.SubjectID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherExcludedSubject(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_excluded_subject WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teacher Excluded Room ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacherExcludedRoom(ctx context.Context, userID string, req *models.CreateTeacherExcludedRoomRequest) (*models.TeacherExcludedRoom, error) {
|
||||
var c models.TeacherExcludedRoom
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_constraint_teacher_excluded_room
|
||||
(created_by_user_id, teacher_id, room_id, is_hard, weight, active, note)
|
||||
SELECT $1, $2, $3, $4, $5, $6, $7
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $2 AND created_by_user_id = $1)
|
||||
AND EXISTS (SELECT 1 FROM tt_room WHERE id = $3 AND created_by_user_id = $1)
|
||||
RETURNING id, created_by_user_id, teacher_id, room_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
`, userID, req.TeacherID, req.RoomID, req.IsHard, req.Weight, req.Active, req.Note).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.RoomID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeacherExcludedRooms(ctx context.Context, userID string) ([]models.TeacherExcludedRoom, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, teacher_id, room_id, is_hard, weight, active, COALESCE(note,''), created_at
|
||||
FROM tt_constraint_teacher_excluded_room WHERE created_by_user_id = $1 ORDER BY teacher_id, room_id
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TeacherExcludedRoom
|
||||
for rows.Next() {
|
||||
var c models.TeacherExcludedRoom
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.TeacherID, &c.RoomID, &c.IsHard, &c.Weight, &c.Active, &c.Note, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacherExcludedRoom(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_constraint_teacher_excluded_room WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// These tests exercise the request DTO binding tags (the same the Gin layer
|
||||
// uses). They don't hit the database — DB-level checks live in integration
|
||||
// tests against a real Postgres.
|
||||
|
||||
func TestCreateTeacherUnavailableDayRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000001"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTeacherUnavailableDayRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid monday", models.CreateTeacherUnavailableDayRequest{TeacherID: uid, DayOfWeek: 1, IsHard: true, Weight: 100, Active: true}, false},
|
||||
{"day too low", models.CreateTeacherUnavailableDayRequest{TeacherID: uid, DayOfWeek: 0}, true},
|
||||
{"day too high", models.CreateTeacherUnavailableDayRequest{TeacherID: uid, DayOfWeek: 8}, true},
|
||||
{"non-uuid teacher", models.CreateTeacherUnavailableDayRequest{TeacherID: "not-a-uuid", DayOfWeek: 1}, true},
|
||||
{"weight above 100", models.CreateTeacherUnavailableDayRequest{TeacherID: uid, DayOfWeek: 1, Weight: 150}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTeacherUnavailableWindowRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000001"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTeacherUnavailableWindowRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", models.CreateTeacherUnavailableWindowRequest{TeacherID: uid, DayOfWeek: 2, StartTime: "13:00", EndTime: "17:00"}, false},
|
||||
{"missing times", models.CreateTeacherUnavailableWindowRequest{TeacherID: uid, DayOfWeek: 2}, true},
|
||||
{"day too high", models.CreateTeacherUnavailableWindowRequest{TeacherID: uid, DayOfWeek: 8, StartTime: "13:00", EndTime: "17:00"}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubjectMaxConsecutiveRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000002"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateSubjectMaxConsecutiveRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid 2 in a row", models.CreateSubjectMaxConsecutiveRequest{SubjectID: uid, MaxConsecutive: 2, IsHard: true, Weight: 100}, false},
|
||||
{"below 1", models.CreateSubjectMaxConsecutiveRequest{SubjectID: uid, MaxConsecutive: 0}, true},
|
||||
{"above 5", models.CreateSubjectMaxConsecutiveRequest{SubjectID: uid, MaxConsecutive: 6}, true},
|
||||
{"non-uuid subject", models.CreateSubjectMaxConsecutiveRequest{SubjectID: "x", MaxConsecutive: 2}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubjectPreferredPeriodRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000002"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateSubjectPreferredPeriodRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid morning", models.CreateSubjectPreferredPeriodRequest{SubjectID: uid, PeriodFrom: 1, PeriodTo: 4, IsHard: false, Weight: 40}, false},
|
||||
{"from missing", models.CreateSubjectPreferredPeriodRequest{SubjectID: uid, PeriodTo: 4}, true},
|
||||
{"to too high", models.CreateSubjectPreferredPeriodRequest{SubjectID: uid, PeriodFrom: 1, PeriodTo: 13}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateClassMaxHoursDayRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000003"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateClassMaxHoursDayRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", models.CreateClassMaxHoursDayRequest{ClassID: uid, MaxHours: 6, IsHard: true, Weight: 100}, false},
|
||||
{"hours below 1", models.CreateClassMaxHoursDayRequest{ClassID: uid, MaxHours: 0}, true},
|
||||
{"hours above 12", models.CreateClassMaxHoursDayRequest{ClassID: uid, MaxHours: 13}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRoomUnavailableRequest_Validation(t *testing.T) {
|
||||
uid := "00000000-0000-0000-0000-000000000004"
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateRoomUnavailableRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", models.CreateRoomUnavailableRequest{RoomID: uid, DayOfWeek: 3, PeriodIndex: 4, IsHard: true, Weight: 100}, false},
|
||||
{"missing day", models.CreateRoomUnavailableRequest{RoomID: uid, PeriodIndex: 4}, true},
|
||||
{"period too high", models.CreateRoomUnavailableRequest{RoomID: uid, DayOfWeek: 3, PeriodIndex: 13}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// Curriculum and Assignment operations.
|
||||
// Ownership is enforced by joining against tt_class.created_by_user_id.
|
||||
|
||||
// ---------- Curriculum (class × subject → weekly hours) ----------
|
||||
|
||||
func (s *TimetableService) CreateCurriculum(ctx context.Context, userID string, req *models.CreateTimetableCurriculumRequest) (*models.TimetableCurriculum, error) {
|
||||
var c models.TimetableCurriculum
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_curriculum (class_id, subject_id, weekly_hours)
|
||||
SELECT $1, $2, $3
|
||||
WHERE EXISTS (SELECT 1 FROM tt_class WHERE id = $1 AND created_by_user_id = $4)
|
||||
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $2 AND created_by_user_id = $4)
|
||||
RETURNING id, class_id, subject_id, weekly_hours, created_at
|
||||
`, req.ClassID, req.SubjectID, req.WeeklyHours, userID).Scan(
|
||||
&c.ID, &c.ClassID, &c.SubjectID, &c.WeeklyHours, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListCurriculum(ctx context.Context, userID string) ([]models.TimetableCurriculum, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT cu.id, cu.class_id, cu.subject_id, cu.weekly_hours, cu.created_at,
|
||||
sub.name, cl.name
|
||||
FROM tt_curriculum cu
|
||||
JOIN tt_class cl ON cu.class_id = cl.id
|
||||
JOIN tt_subject sub ON cu.subject_id = sub.id
|
||||
WHERE cl.created_by_user_id = $1
|
||||
ORDER BY cl.grade_level, cl.name, sub.name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableCurriculum
|
||||
for rows.Next() {
|
||||
var c models.TimetableCurriculum
|
||||
if err := rows.Scan(&c.ID, &c.ClassID, &c.SubjectID, &c.WeeklyHours, &c.CreatedAt, &c.SubjectName, &c.ClassName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteCurriculum(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `
|
||||
DELETE FROM tt_curriculum cu
|
||||
USING tt_class cl
|
||||
WHERE cu.id = $1 AND cu.class_id = cl.id AND cl.created_by_user_id = $2
|
||||
`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Assignment (teacher × class × subject) ----------
|
||||
|
||||
func (s *TimetableService) CreateAssignment(ctx context.Context, userID string, req *models.CreateTimetableAssignmentRequest) (*models.TimetableAssignment, error) {
|
||||
var a models.TimetableAssignment
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_assignment (teacher_id, class_id, subject_id)
|
||||
SELECT $1, $2, $3
|
||||
WHERE EXISTS (SELECT 1 FROM tt_teacher WHERE id = $1 AND created_by_user_id = $4)
|
||||
AND EXISTS (SELECT 1 FROM tt_class WHERE id = $2 AND created_by_user_id = $4)
|
||||
AND EXISTS (SELECT 1 FROM tt_subject WHERE id = $3 AND created_by_user_id = $4)
|
||||
RETURNING id, teacher_id, class_id, subject_id, created_at
|
||||
`, req.TeacherID, req.ClassID, req.SubjectID, userID).Scan(
|
||||
&a.ID, &a.TeacherID, &a.ClassID, &a.SubjectID, &a.CreatedAt,
|
||||
)
|
||||
return &a, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListAssignments(ctx context.Context, userID string) ([]models.TimetableAssignment, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT a.id, a.teacher_id, a.class_id, a.subject_id, a.created_at,
|
||||
t.last_name || ', ' || t.first_name, cl.name, sub.name
|
||||
FROM tt_assignment a
|
||||
JOIN tt_teacher t ON a.teacher_id = t.id
|
||||
JOIN tt_class cl ON a.class_id = cl.id
|
||||
JOIN tt_subject sub ON a.subject_id = sub.id
|
||||
WHERE t.created_by_user_id = $1
|
||||
ORDER BY cl.grade_level, cl.name, sub.name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableAssignment
|
||||
for rows.Next() {
|
||||
var a models.TimetableAssignment
|
||||
if err := rows.Scan(&a.ID, &a.TeacherID, &a.ClassID, &a.SubjectID, &a.CreatedAt, &a.TeacherName, &a.ClassName, &a.SubjectName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteAssignment(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `
|
||||
DELETE FROM tt_assignment a
|
||||
USING tt_teacher t
|
||||
WHERE a.id = $1 AND a.teacher_id = t.id AND t.created_by_user_id = $2
|
||||
`, id, userID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// TimetableService handles all CRUD for the school-wide timetable scheduler:
|
||||
// classes, periods, rooms, subjects, teachers, curriculum, assignments.
|
||||
type TimetableService struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewTimetableService(db *pgxpool.Pool) *TimetableService {
|
||||
return &TimetableService{db: db}
|
||||
}
|
||||
|
||||
// ---------- Classes ----------
|
||||
|
||||
func (s *TimetableService) CreateClass(ctx context.Context, userID string, req *models.CreateTimetableClassRequest) (*models.TimetableClass, error) {
|
||||
var c models.TimetableClass
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_class (created_by_user_id, name, grade_level, student_count, notes)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, created_by_user_id, name, grade_level, student_count, notes, created_at
|
||||
`, userID, req.Name, req.GradeLevel, req.StudentCount, req.Notes).Scan(
|
||||
&c.ID, &c.CreatedByUserID, &c.Name, &c.GradeLevel, &c.StudentCount, &c.Notes, &c.CreatedAt,
|
||||
)
|
||||
return &c, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListClasses(ctx context.Context, userID string) ([]models.TimetableClass, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, name, grade_level, student_count, COALESCE(notes,''), created_at
|
||||
FROM tt_class WHERE created_by_user_id = $1 ORDER BY grade_level, name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableClass
|
||||
for rows.Next() {
|
||||
var c models.TimetableClass
|
||||
if err := rows.Scan(&c.ID, &c.CreatedByUserID, &c.Name, &c.GradeLevel, &c.StudentCount, &c.Notes, &c.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteClass(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_class WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Periods ----------
|
||||
|
||||
func (s *TimetableService) CreatePeriod(ctx context.Context, userID string, req *models.CreateTimetablePeriodRequest) (*models.TimetablePeriod, error) {
|
||||
var p models.TimetablePeriod
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_period (created_by_user_id, day_of_week, period_index, start_time, end_time, is_break, label)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, created_by_user_id, day_of_week, period_index, start_time::text, end_time::text, is_break, COALESCE(label,''), created_at
|
||||
`, userID, req.DayOfWeek, req.PeriodIndex, req.StartTime, req.EndTime, req.IsBreak, req.Label).Scan(
|
||||
&p.ID, &p.CreatedByUserID, &p.DayOfWeek, &p.PeriodIndex, &p.StartTime, &p.EndTime, &p.IsBreak, &p.Label, &p.CreatedAt,
|
||||
)
|
||||
return &p, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListPeriods(ctx context.Context, userID string) ([]models.TimetablePeriod, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, day_of_week, period_index, start_time::text, end_time::text, is_break, COALESCE(label,''), created_at
|
||||
FROM tt_period WHERE created_by_user_id = $1 ORDER BY day_of_week, period_index
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetablePeriod
|
||||
for rows.Next() {
|
||||
var p models.TimetablePeriod
|
||||
if err := rows.Scan(&p.ID, &p.CreatedByUserID, &p.DayOfWeek, &p.PeriodIndex, &p.StartTime, &p.EndTime, &p.IsBreak, &p.Label, &p.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeletePeriod(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_period WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Rooms ----------
|
||||
|
||||
func (s *TimetableService) CreateRoom(ctx context.Context, userID string, req *models.CreateTimetableRoomRequest) (*models.TimetableRoom, error) {
|
||||
var r models.TimetableRoom
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_room (created_by_user_id, name, room_type, capacity, floor_level, has_elevator, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, created_by_user_id, name, COALESCE(room_type,''), capacity, floor_level, has_elevator, COALESCE(notes,''), created_at
|
||||
`, userID, req.Name, req.RoomType, req.Capacity, req.FloorLevel, req.HasElevator, req.Notes).Scan(
|
||||
&r.ID, &r.CreatedByUserID, &r.Name, &r.RoomType, &r.Capacity, &r.FloorLevel, &r.HasElevator, &r.Notes, &r.CreatedAt,
|
||||
)
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListRooms(ctx context.Context, userID string) ([]models.TimetableRoom, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, name, COALESCE(room_type,''), capacity, floor_level, has_elevator, COALESCE(notes,''), created_at
|
||||
FROM tt_room WHERE created_by_user_id = $1 ORDER BY name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableRoom
|
||||
for rows.Next() {
|
||||
var r models.TimetableRoom
|
||||
if err := rows.Scan(&r.ID, &r.CreatedByUserID, &r.Name, &r.RoomType, &r.Capacity, &r.FloorLevel, &r.HasElevator, &r.Notes, &r.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteRoom(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_room WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Subjects ----------
|
||||
|
||||
func (s *TimetableService) CreateSubject(ctx context.Context, userID string, req *models.CreateTimetableSubjectRequest) (*models.TimetableSubject, error) {
|
||||
var sub models.TimetableSubject
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_subject (created_by_user_id, name, short_code, color, is_main_subject, required_room_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_by_user_id, name, short_code, COALESCE(color,''), is_main_subject, COALESCE(required_room_type,''), created_at
|
||||
`, userID, req.Name, req.ShortCode, req.Color, req.IsMainSubject, req.RequiredRoomType).Scan(
|
||||
&sub.ID, &sub.CreatedByUserID, &sub.Name, &sub.ShortCode, &sub.Color, &sub.IsMainSubject, &sub.RequiredRoomType, &sub.CreatedAt,
|
||||
)
|
||||
return &sub, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListSubjects(ctx context.Context, userID string) ([]models.TimetableSubject, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, name, short_code, COALESCE(color,''), is_main_subject, COALESCE(required_room_type,''), created_at
|
||||
FROM tt_subject WHERE created_by_user_id = $1 ORDER BY name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableSubject
|
||||
for rows.Next() {
|
||||
var sub models.TimetableSubject
|
||||
if err := rows.Scan(&sub.ID, &sub.CreatedByUserID, &sub.Name, &sub.ShortCode, &sub.Color, &sub.IsMainSubject, &sub.RequiredRoomType, &sub.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, sub)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteSubject(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_subject WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ---------- Teachers ----------
|
||||
|
||||
func (s *TimetableService) CreateTeacher(ctx context.Context, userID string, req *models.CreateTimetableTeacherRequest) (*models.TimetableTeacher, error) {
|
||||
var t models.TimetableTeacher
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO tt_teacher (created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, COALESCE(notes,''), created_at
|
||||
`, userID, req.FirstName, req.LastName, req.ShortCode, req.EmploymentPercentage, req.MaxHoursWeek, req.Notes).Scan(
|
||||
&t.ID, &t.CreatedByUserID, &t.FirstName, &t.LastName, &t.ShortCode, &t.EmploymentPercentage, &t.MaxHoursWeek, &t.Notes, &t.CreatedAt,
|
||||
)
|
||||
return &t, err
|
||||
}
|
||||
|
||||
func (s *TimetableService) ListTeachers(ctx context.Context, userID string) ([]models.TimetableTeacher, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, created_by_user_id, first_name, last_name, short_code, employment_percentage, max_hours_week, COALESCE(notes,''), created_at
|
||||
FROM tt_teacher WHERE created_by_user_id = $1 ORDER BY last_name, first_name
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []models.TimetableTeacher
|
||||
for rows.Next() {
|
||||
var t models.TimetableTeacher
|
||||
if err := rows.Scan(&t.ID, &t.CreatedByUserID, &t.FirstName, &t.LastName, &t.ShortCode, &t.EmploymentPercentage, &t.MaxHoursWeek, &t.Notes, &t.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *TimetableService) DeleteTeacher(ctx context.Context, id, userID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM tt_teacher WHERE id = $1 AND created_by_user_id = $2`, id, userID)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
)
|
||||
|
||||
// validate is a singleton used to exercise the same struct tags Gin uses for
|
||||
// request validation. The DB tests live in integration tests against a real
|
||||
// database; this test pins the contract for the request DTOs.
|
||||
var validate = func() *validator.Validate {
|
||||
v := validator.New()
|
||||
v.SetTagName("binding")
|
||||
return v
|
||||
}()
|
||||
|
||||
func TestNewTimetableService_Constructs(t *testing.T) {
|
||||
s := NewTimetableService(nil)
|
||||
if s == nil {
|
||||
t.Fatal("expected non-nil service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimetableClassRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTimetableClassRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", models.CreateTimetableClassRequest{Name: "5a", GradeLevel: 5, StudentCount: 24}, false},
|
||||
{"missing name", models.CreateTimetableClassRequest{GradeLevel: 5}, true},
|
||||
{"grade too low", models.CreateTimetableClassRequest{Name: "0a", GradeLevel: 0}, true},
|
||||
{"grade too high", models.CreateTimetableClassRequest{Name: "14a", GradeLevel: 14}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimetablePeriodRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTimetablePeriodRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid monday first", models.CreateTimetablePeriodRequest{DayOfWeek: 1, PeriodIndex: 1, StartTime: "08:00", EndTime: "08:45"}, false},
|
||||
{"day too low", models.CreateTimetablePeriodRequest{DayOfWeek: 0, PeriodIndex: 1, StartTime: "08:00", EndTime: "08:45"}, true},
|
||||
{"day too high", models.CreateTimetablePeriodRequest{DayOfWeek: 8, PeriodIndex: 1, StartTime: "08:00", EndTime: "08:45"}, true},
|
||||
{"missing times", models.CreateTimetablePeriodRequest{DayOfWeek: 1, PeriodIndex: 1}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimetableTeacherRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTimetableTeacherRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid full-time", models.CreateTimetableTeacherRequest{FirstName: "Anna", LastName: "Schmidt", ShortCode: "SCH", EmploymentPercentage: 100, MaxHoursWeek: 28}, false},
|
||||
{"valid part-time", models.CreateTimetableTeacherRequest{FirstName: "Bea", LastName: "Mueller", ShortCode: "MUE", EmploymentPercentage: 50, MaxHoursWeek: 14}, false},
|
||||
{"missing names", models.CreateTimetableTeacherRequest{ShortCode: "XX"}, true},
|
||||
{"employment above 100", models.CreateTimetableTeacherRequest{FirstName: "X", LastName: "Y", ShortCode: "Z", EmploymentPercentage: 150}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimetableCurriculumRequest_Validation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
req models.CreateTimetableCurriculumRequest
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid", models.CreateTimetableCurriculumRequest{ClassID: "00000000-0000-0000-0000-000000000001", SubjectID: "00000000-0000-0000-0000-000000000002", WeeklyHours: 4}, false},
|
||||
{"non-uuid class", models.CreateTimetableCurriculumRequest{ClassID: "not-a-uuid", SubjectID: "00000000-0000-0000-0000-000000000002", WeeklyHours: 4}, true},
|
||||
{"hours below 1", models.CreateTimetableCurriculumRequest{ClassID: "00000000-0000-0000-0000-000000000001", SubjectID: "00000000-0000-0000-0000-000000000002", WeeklyHours: 0}, true},
|
||||
{"hours above 10", models.CreateTimetableCurriculumRequest{ClassID: "00000000-0000-0000-0000-000000000001", SubjectID: "00000000-0000-0000-0000-000000000002", WeeklyHours: 11}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validate.Struct(tt.req)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user