mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
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:
1 parent
0405106587
commit
668a54ae1d
15 files changed
+563
-5
No files matched your search
@@ -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)
|
||||
|
||||
@@ -36,6 +36,7 @@ type Repository interface {
|
||||
ListBookingsByTenantBetween(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]BookingRecord, error)
|
||||
CreateBooking(ctx context.Context, params CreateBookingParams) (CreatedBooking, error)
|
||||
AppendWaitlistEntry(ctx context.Context, params WaitlistEntryParams) error
|
||||
ResolveWaitlistEntry(ctx context.Context, tenantID, sessionID, customerEmail string) error
|
||||
CreateReminderJob(ctx context.Context, params ReminderJobParams) error
|
||||
ListDueReminderJobs(ctx context.Context, dueBefore time.Time, limit int) ([]ReminderJobRecord, error)
|
||||
MarkReminderJobDispatched(ctx context.Context, reminderJobID string, status string, dispatchedAt time.Time) error
|
||||
@@ -103,6 +104,7 @@ type Repository interface {
|
||||
|
||||
// Customer Booking Management
|
||||
GetBookingByReference(ctx context.Context, reference string) (BookingRecord, error)
|
||||
GetBookingByID(ctx context.Context, bookingID string) (BookingRecord, error)
|
||||
UpdateBookingStatus(ctx context.Context, bookingID string, status string) error
|
||||
RescheduleBooking(ctx context.Context, bookingID string, startsAt time.Time, endsAt time.Time) error
|
||||
|
||||
@@ -352,6 +354,7 @@ type BookingRecord struct {
|
||||
Reference string
|
||||
ManageToken string
|
||||
Notes string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type CreateBookingParams struct {
|
||||
@@ -719,11 +722,24 @@ func (r *PGRepository) GetBookingByReference(ctx context.Context, reference stri
|
||||
err := r.pool.QueryRow(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 reference = $1
|
||||
`, reference).Scan(&rec.ID, &rec.TenantID, &rec.ServiceID, &rec.ClassSessionID, &rec.StaffID, &rec.LocationID,
|
||||
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference, &rec.ManageToken, &rec.Notes)
|
||||
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference, &rec.ManageToken, &rec.Notes, &rec.CreatedAt)
|
||||
return rec, err
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetBookingByID(ctx context.Context, bookingID string) (BookingRecord, error) {
|
||||
var rec BookingRecord
|
||||
err := r.pool.QueryRow(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, ''), created_at
|
||||
FROM bookings
|
||||
WHERE id = $1
|
||||
`, bookingID).Scan(&rec.ID, &rec.TenantID, &rec.ServiceID, &rec.ClassSessionID, &rec.StaffID, &rec.LocationID,
|
||||
&rec.CustomerName, &rec.CustomerEmail, &rec.CustomerPhone, &rec.StartsAt, &rec.EndsAt, &rec.Status, &rec.Reference, &rec.ManageToken, &rec.Notes, &rec.CreatedAt)
|
||||
return rec, err
|
||||
}
|
||||
|
||||
@@ -1343,6 +1359,7 @@ func (r *MemoryRepository) CreateBooking(_ context.Context, params CreateBooking
|
||||
Reference: params.Reference,
|
||||
ManageToken: params.ManageToken,
|
||||
Notes: params.Notes,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
})
|
||||
return created, nil
|
||||
}
|
||||
@@ -1352,6 +1369,13 @@ func (r *MemoryRepository) AppendWaitlistEntry(_ context.Context, _ WaitlistEntr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ResolveWaitlistEntry(_ context.Context, _, _, _ string) error {
|
||||
if r.waitlistSize > 0 {
|
||||
r.waitlistSize--
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) CreateReminderJob(_ context.Context, params ReminderJobParams) error {
|
||||
reference := params.BookingID
|
||||
startsAt := time.Now().UTC()
|
||||
@@ -1756,6 +1780,15 @@ func (r *MemoryRepository) GetBookingByReference(_ context.Context, reference st
|
||||
return BookingRecord{}, pgx.ErrNoRows
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) GetBookingByID(_ context.Context, bookingID string) (BookingRecord, error) {
|
||||
for _, b := range r.bookings {
|
||||
if b.ID == bookingID {
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
return BookingRecord{}, pgx.ErrNoRows
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) UpdateBookingStatus(_ context.Context, bookingID string, status string) error {
|
||||
for i, b := range r.bookings {
|
||||
if b.ID == bookingID {
|
||||
|
||||
Reference in new issue
Block a user