feat(bookings): dashboard cancellation and waitlist promotion

- POST /v1/catalog/bookings/{id}/cancel lets businesses cancel
  tenant-owned bookings; two-step confirm in the detail modal
- Shared PromoteWaitlist helper promotes earliest waitlisted bookings
  when a class seat frees, on both dashboard and customer cancel paths;
  promoted customers get a confirmation email, waitlist ledger marked
  resolved
- waitlisted status surfaced in bookings UI: stats card, filter, warning
  badge, demo seed, CS+EN strings
- BookingRecord gains created_at for fair ordering; GetBookingByID and
  ResolveWaitlistEntry added to the repository surface

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-18 11:21:46 +02:00
1 parent 0405106587
commit 668a54ae1d
15 files changed
+563 -5

No files matched your search

+13 -1
View File
@@ -9,7 +9,7 @@ func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID
rows, err := r.pool.Query(ctx, `
SELECT id, tenant_id, service_id, class_session_id, staff_id, location_id,
customer_name, customer_email, customer_phone, starts_at, ends_at, status, reference,
COALESCE(manage_token, ''), COALESCE(notes, '')
COALESCE(manage_token, ''), COALESCE(notes, ''), created_at
FROM bookings
WHERE tenant_id = $1 AND starts_at < $3 AND ends_at > $2
ORDER BY starts_at ASC
@@ -38,6 +38,7 @@ func (r *PGRepository) ListBookingsByTenantBetween(ctx context.Context, tenantID
&record.Reference,
&record.ManageToken,
&record.Notes,
&record.CreatedAt,
); err != nil {
return nil, err
}
@@ -84,6 +85,17 @@ func (r *PGRepository) AppendWaitlistEntry(ctx context.Context, params WaitlistE
return err
}
// ResolveWaitlistEntry marks the pending ledger row for a promoted booking.
// Entries carry no booking_id, so the session + customer email pair is the
// practical key.
func (r *PGRepository) ResolveWaitlistEntry(ctx context.Context, tenantID, sessionID, customerEmail string) error {
_, err := r.pool.Exec(ctx, `
UPDATE waitlist_entries SET status = 'resolved'
WHERE tenant_id = $1 AND class_session_id = $2 AND customer_email = $3 AND status = 'pending'
`, tenantID, sessionID, customerEmail)
return err
}
func (r *PGRepository) CreateReminderJob(ctx context.Context, params ReminderJobParams) error {
_, err := r.pool.Exec(ctx, `
INSERT INTO reminder_jobs (tenant_id, booking_id, channel, scheduled_for)