Files
Bookra/apps/backend/internal/db/classes.go
T
Tomas DvorakandDevin ad60167a31 feat(classes): weekly recurring schedules with lazy session materialization
class_schedules rules generate concrete session rows for a rolling 28-day
horizon — no cron; materialize runs idempotently on dashboard and public
reads via a partial unique index on (schedule_id, starts_at). Rule delete
stops future sessions; existing sessions keep bookings and overrides.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-18 17:31:17 +02:00

135 lines
3.9 KiB
Go

package db
import (
"context"
)
const classTemplateColumns = "id, tenant_id, title, duration_minutes, capacity, created_at"
const classSessionColumns = "cs.id, cs.tenant_id, cs.template_id, cs.location_id, ct.title, cs.starts_at, cs.ends_at, cs.capacity, cs.schedule_id"
func (r *PGRepository) ListClassTemplatesByTenant(ctx context.Context, tenantID string) ([]ClassTemplateRecord, error) {
rows, err := r.pool.Query(ctx, `
SELECT `+classTemplateColumns+`
FROM class_templates
WHERE tenant_id = $1
ORDER BY created_at ASC
`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
var records []ClassTemplateRecord
for rows.Next() {
var record ClassTemplateRecord
if err := rows.Scan(
&record.ID,
&record.TenantID,
&record.Title,
&record.DurationMinutes,
&record.Capacity,
&record.CreatedAt,
); err != nil {
return nil, err
}
records = append(records, record)
}
return records, rows.Err()
}
func (r *PGRepository) GetClassTemplateByID(ctx context.Context, templateID string) (ClassTemplateRecord, error) {
var record ClassTemplateRecord
err := r.pool.QueryRow(ctx, `
SELECT `+classTemplateColumns+`
FROM class_templates
WHERE id = $1
`, templateID).Scan(
&record.ID,
&record.TenantID,
&record.Title,
&record.DurationMinutes,
&record.Capacity,
&record.CreatedAt,
)
return record, err
}
func (r *PGRepository) CreateClassTemplate(ctx context.Context, params CreateClassTemplateParams) (ClassTemplateRecord, error) {
var record ClassTemplateRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO class_templates (tenant_id, title, duration_minutes, capacity)
VALUES ($1, $2, $3, $4)
RETURNING `+classTemplateColumns+`
`, params.TenantID, params.Title, params.DurationMinutes, params.Capacity).Scan(
&record.ID,
&record.TenantID,
&record.Title,
&record.DurationMinutes,
&record.Capacity,
&record.CreatedAt,
)
return record, err
}
func (r *PGRepository) UpdateClassTemplate(ctx context.Context, templateID string, params CreateClassTemplateParams) (ClassTemplateRecord, error) {
var record ClassTemplateRecord
err := r.pool.QueryRow(ctx, `
UPDATE class_templates
SET title = $2, duration_minutes = $3, capacity = $4
WHERE id = $1
RETURNING `+classTemplateColumns+`
`, templateID, params.Title, params.DurationMinutes, params.Capacity).Scan(
&record.ID,
&record.TenantID,
&record.Title,
&record.DurationMinutes,
&record.Capacity,
&record.CreatedAt,
)
return record, err
}
func (r *PGRepository) DeleteClassTemplate(ctx context.Context, templateID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM class_templates WHERE id = $1`, templateID)
return err
}
func (r *PGRepository) GetClassSessionByID(ctx context.Context, sessionID string) (ClassSessionRecord, error) {
var record ClassSessionRecord
err := r.pool.QueryRow(ctx, `
SELECT `+classSessionColumns+`
FROM class_sessions cs
INNER JOIN class_templates ct ON ct.id = cs.template_id
WHERE cs.id = $1
`, sessionID).Scan(
&record.ID,
&record.TenantID,
&record.TemplateID,
&record.LocationID,
&record.Title,
&record.StartsAt,
&record.EndsAt,
&record.Capacity,
&record.ScheduleID,
)
return record, err
}
func (r *PGRepository) CreateClassSession(ctx context.Context, params CreateClassSessionParams) (ClassSessionRecord, error) {
var record ClassSessionRecord
err := r.pool.QueryRow(ctx, `
INSERT INTO class_sessions (tenant_id, template_id, location_id, starts_at, ends_at, capacity, schedule_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`, params.TenantID, params.TemplateID, params.LocationID, params.StartsAt, params.EndsAt, params.Capacity, params.ScheduleID).Scan(&record.ID)
if err != nil {
return record, err
}
return r.GetClassSessionByID(ctx, record.ID)
}
func (r *PGRepository) DeleteClassSession(ctx context.Context, sessionID string) error {
_, err := r.pool.Exec(ctx, `DELETE FROM class_sessions WHERE id = $1`, sessionID)
return err
}