feat: scheduled report digests + per-session capacity (M33)

POST /v1/internal/jobs/reports/dispatch emails each tenant owner a
trailing-window digest (bookings, revenue, per-service and per-staff
breakdown) localized to the tenant locale; zero-activity tenants are
skipped. Metrics cores extracted to tenant-scoped variants so the job
needs no principal.

PATCH /v1/catalog/class-sessions/:id sets a session's seat count with
a confirmed-bookings floor — shrinking below it needs allowOverCapacity,
matching the waitlist-promotion convention. Session rows gain an inline
capacity editor with an overbook confirm state.

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

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-22 21:29:44 +02:00
1 parent e6f1ec38a4
commit e680e0c2b8
14 files changed
+808 -10

No files matched your search

+13
View File
@@ -2,6 +2,8 @@ package db
import (
"context"
"github.com/jackc/pgx/v5"
)
const classTemplateColumns = "id, tenant_id, title, duration_minutes, capacity, created_at"
@@ -132,3 +134,14 @@ func (r *PGRepository) DeleteClassSession(ctx context.Context, sessionID string)
_, err := r.pool.Exec(ctx, `DELETE FROM class_sessions WHERE id = $1`, sessionID)
return err
}
func (r *PGRepository) UpdateClassSessionCapacity(ctx context.Context, sessionID string, capacity int) (ClassSessionRecord, error) {
tag, err := r.pool.Exec(ctx, `UPDATE class_sessions SET capacity = $2 WHERE id = $1`, sessionID, capacity)
if err != nil {
return ClassSessionRecord{}, err
}
if tag.RowsAffected() == 0 {
return ClassSessionRecord{}, pgx.ErrNoRows
}
return r.GetClassSessionByID(ctx, sessionID)
}
+13
View File
@@ -30,6 +30,9 @@ type Repository interface {
GetClassSessionByID(ctx context.Context, sessionID string) (ClassSessionRecord, error)
CreateClassSession(ctx context.Context, params CreateClassSessionParams) (ClassSessionRecord, error)
DeleteClassSession(ctx context.Context, sessionID string) error
// UpdateClassSessionCapacity sets the session's seat count; the caller
// checks tenancy and the booked-seats floor.
UpdateClassSessionCapacity(ctx context.Context, sessionID string, capacity int) (ClassSessionRecord, error)
ListClassTemplatesByTenant(ctx context.Context, tenantID string) ([]ClassTemplateRecord, error)
GetClassTemplateByID(ctx context.Context, templateID string) (ClassTemplateRecord, error)
CreateClassTemplate(ctx context.Context, params CreateClassTemplateParams) (ClassTemplateRecord, error)
@@ -1539,6 +1542,16 @@ func (r *MemoryRepository) DeleteClassSession(_ context.Context, sessionID strin
return pgx.ErrNoRows
}
func (r *MemoryRepository) UpdateClassSessionCapacity(_ context.Context, sessionID string, capacity int) (ClassSessionRecord, error) {
for i := range r.classSessions {
if r.classSessions[i].ID == sessionID {
r.classSessions[i].Capacity = int32(capacity)
return r.classSessions[i], nil
}
}
return ClassSessionRecord{}, pgx.ErrNoRows
}
func (r *MemoryRepository) ListClassSchedulesByTenant(_ context.Context, tenantID string) ([]ClassScheduleRecord, error) {
if tenantID != r.tenant.ID {
return nil, nil