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
@@ -26,6 +26,7 @@ type CustomerService struct {
|
||||
}
|
||||
|
||||
type CustomerNotifier interface {
|
||||
SendBookingConfirmation(ctx context.Context, data notifications.BookingEmailData) error
|
||||
SendBookingReschedule(ctx context.Context, data notifications.BookingEmailData) error
|
||||
SendBookingCancellation(ctx context.Context, data notifications.BookingEmailData) error
|
||||
}
|
||||
@@ -42,6 +43,10 @@ func NewCustomerService(repo db.Repository, notifier CustomerNotifier, frontendU
|
||||
|
||||
type customerNoopNotifier struct{}
|
||||
|
||||
func (n *customerNoopNotifier) SendBookingConfirmation(ctx context.Context, data notifications.BookingEmailData) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *customerNoopNotifier) SendBookingReschedule(ctx context.Context, data notifications.BookingEmailData) error {
|
||||
return nil
|
||||
}
|
||||
@@ -194,6 +199,15 @@ func (s *CustomerService) CancelBooking(ctx context.Context, reference string, t
|
||||
// Send cancellation email (async)
|
||||
go s.sendCancellationEmail(booking)
|
||||
|
||||
// A freed class seat promotes the earliest waitlisted booking.
|
||||
if booking.ClassSessionID != nil {
|
||||
if promoted, err := PromoteWaitlist(ctx, s.repo, booking.TenantID, *booking.ClassSessionID); err == nil {
|
||||
for _, promotedBooking := range promoted {
|
||||
go s.sendPromotionEmail(promotedBooking)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -270,3 +284,37 @@ func (s *CustomerService) sendCancellationEmail(booking db.BookingRecord) {
|
||||
|
||||
s.notifier.SendBookingCancellation(ctx, emailData)
|
||||
}
|
||||
|
||||
// sendPromotionEmail confirms a booking that moved off the class waitlist.
|
||||
func (s *CustomerService) sendPromotionEmail(booking db.BookingRecord) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
tenant, err := s.repo.GetTenantByID(ctx, booking.TenantID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
brand, _ := s.repo.GetBrandProfile(ctx, tenant.ID)
|
||||
brandColor := brand.PrimaryColor
|
||||
if brandColor == "" {
|
||||
brandColor = "#a65c3e"
|
||||
}
|
||||
serviceName, locationName := s.descriptors(ctx, booking)
|
||||
|
||||
s.notifier.SendBookingConfirmation(ctx, notifications.BookingEmailData{
|
||||
TenantID: tenant.ID,
|
||||
TenantName: tenant.Name,
|
||||
TenantSlug: tenant.Slug,
|
||||
BrandColor: brandColor,
|
||||
CustomerName: booking.CustomerName,
|
||||
CustomerEmail: booking.CustomerEmail,
|
||||
Service: serviceName,
|
||||
Location: locationName,
|
||||
Reference: booking.Reference,
|
||||
StartsAt: booking.StartsAt,
|
||||
EndsAt: booking.EndsAt,
|
||||
Timezone: tenant.Timezone,
|
||||
Locale: tenant.Locale,
|
||||
ManagementURL: s.managementURL(booking),
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user