mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 23:51:17 +00:00
feat: series-aware schedule edits + waitlist ledger removal (M32)
PATCH /v1/catalog/class-schedules/:id patches a recurring rule and, with applyToFuture, retimes materialized future sessions to the new local start time on their existing dates — moving confirmed and waitlisted bookings with them and re-arming reminders. Zones-page schedule rows gain an inline editor (CS/EN). Also drops the vestigial waitlist_entries table (migration 00020): positions derive from bookings.created_at, so the ledger was written but never read. 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
55fc0d06e3
commit
65a693cf38
17 files changed
+840
-230
No files matched your search
@@ -518,13 +518,10 @@ func (s *CustomerService) MoveBookingToSession(ctx context.Context, reference st
|
||||
if err != nil {
|
||||
return domain.MoveBookingResponse{}, err
|
||||
}
|
||||
confirmed, waitlisted := 0, 0
|
||||
confirmed := 0
|
||||
for _, other := range targetBookings {
|
||||
switch other.Status {
|
||||
case "confirmed":
|
||||
if other.Status == "confirmed" {
|
||||
confirmed++
|
||||
case "waitlisted":
|
||||
waitlisted++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,30 +531,15 @@ func (s *CustomerService) MoveBookingToSession(ctx context.Context, reference st
|
||||
newStatus = "waitlisted"
|
||||
}
|
||||
|
||||
// Release the old spot first so the seat/queue position is free before
|
||||
// the retarget lands.
|
||||
if booking.Status == "waitlisted" {
|
||||
_ = s.repo.ResolveWaitlistEntry(ctx, booking.TenantID, currentSession.ID, booking.CustomerEmail)
|
||||
}
|
||||
if err := s.repo.UpdateBookingSession(ctx, booking.ID, target.ID, target.StartsAt, target.EndsAt, newStatus); err != nil {
|
||||
return domain.MoveBookingResponse{}, err
|
||||
}
|
||||
if newStatus == "waitlisted" {
|
||||
if err := s.repo.AppendWaitlistEntry(ctx, db.WaitlistEntryParams{
|
||||
TenantID: booking.TenantID,
|
||||
ClassSessionID: target.ID,
|
||||
CustomerName: booking.CustomerName,
|
||||
CustomerEmail: booking.CustomerEmail,
|
||||
Position: waitlisted + 1,
|
||||
}); err != nil {
|
||||
return domain.MoveBookingResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Re-arm reminders for the new window (confirmed only; a queue spot
|
||||
// re-arms on promotion through the shared confirm path).
|
||||
if err := s.repo.DeletePendingReminderJobsForBooking(ctx, booking.ID); err == nil && newStatus == "confirmed" {
|
||||
_ = armReminderJobs(ctx, s.repo, booking.TenantID, booking.ID, target.StartsAt, booking.CustomerPhone)
|
||||
if newStatus == "confirmed" {
|
||||
booking.StartsAt, booking.EndsAt = target.StartsAt, target.EndsAt
|
||||
RearmReminderJobs(ctx, s.repo, booking.TenantID, booking)
|
||||
}
|
||||
|
||||
// A confirmed move frees a seat on the old session — drain its queue.
|
||||
|
||||
@@ -164,17 +164,6 @@ func seedMoveBooking(t *testing.T, repo *db.MemoryRepository, sessionID, referen
|
||||
if err != nil {
|
||||
t.Fatalf("get session: %v", err)
|
||||
}
|
||||
if status == "waitlisted" {
|
||||
if err := repo.AppendWaitlistEntry(ctx, db.WaitlistEntryParams{
|
||||
TenantID: moveTenantID,
|
||||
ClassSessionID: sessionID,
|
||||
CustomerName: "Move Customer",
|
||||
CustomerEmail: "[email protected]",
|
||||
Position: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("seed waitlist entry: %v", err)
|
||||
}
|
||||
}
|
||||
created, err := repo.CreateBooking(ctx, db.CreateBookingParams{
|
||||
TenantID: moveTenantID,
|
||||
ClassSessionID: &sessionID,
|
||||
|
||||
@@ -50,7 +50,7 @@ func PromoteWaitlist(ctx context.Context, repo db.Repository, tenantID, sessionI
|
||||
if session.Capacity > 0 && confirmed >= int(session.Capacity) {
|
||||
break
|
||||
}
|
||||
if err := confirmWaitlistedBooking(ctx, repo, tenantID, sessionID, booking); err != nil {
|
||||
if err := confirmWaitlistedBooking(ctx, repo, tenantID, booking); err != nil {
|
||||
return promoted, err
|
||||
}
|
||||
confirmed++
|
||||
@@ -104,24 +104,30 @@ func PromoteBooking(ctx context.Context, repo db.Repository, tenantID, bookingID
|
||||
}
|
||||
}
|
||||
|
||||
if err := confirmWaitlistedBooking(ctx, repo, tenantID, sessionID, booking); err != nil {
|
||||
if err := confirmWaitlistedBooking(ctx, repo, tenantID, booking); err != nil {
|
||||
return db.BookingRecord{}, err
|
||||
}
|
||||
booking.Status = "confirmed"
|
||||
return booking, nil
|
||||
}
|
||||
|
||||
// confirmWaitlistedBooking flips a waitlisted booking to confirmed, resolves
|
||||
// the ledger entry, and re-arms reminder jobs — a booking promoted after its
|
||||
// reminder already dispatched-as-skipped would otherwise go unreminded.
|
||||
func confirmWaitlistedBooking(ctx context.Context, repo db.Repository, tenantID, sessionID string, booking db.BookingRecord) error {
|
||||
// confirmWaitlistedBooking flips a waitlisted booking to confirmed and
|
||||
// re-arms reminder jobs — a booking promoted after its reminder already
|
||||
// dispatched-as-skipped would otherwise go unreminded.
|
||||
func confirmWaitlistedBooking(ctx context.Context, repo db.Repository, tenantID string, booking db.BookingRecord) error {
|
||||
if err := repo.UpdateBookingStatus(ctx, booking.ID, "confirmed"); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = repo.ResolveWaitlistEntry(ctx, tenantID, sessionID, booking.CustomerEmail)
|
||||
RearmReminderJobs(ctx, repo, tenantID, booking)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RearmReminderJobs clears pending reminders for a booking and re-arms them
|
||||
// for its (possibly new) start time. Used after promotion, session moves, and
|
||||
// schedule retimes so reminders always match the booking's live window.
|
||||
func RearmReminderJobs(ctx context.Context, repo db.Repository, tenantID string, booking db.BookingRecord) {
|
||||
_ = repo.DeletePendingReminderJobsForBooking(ctx, booking.ID)
|
||||
_ = armReminderJobs(ctx, repo, tenantID, booking.ID, booking.StartsAt, booking.CustomerPhone)
|
||||
return nil
|
||||
}
|
||||
|
||||
// armReminderJobs schedules the email reminder — and the SMS reminder when the
|
||||
|
||||
@@ -365,19 +365,6 @@ func (s *Service) Create(ctx context.Context, request domain.CreateBookingReques
|
||||
return domain.CreateBookingResponse{}, err
|
||||
}
|
||||
|
||||
if status == "waitlisted" && request.ClassSessionID != nil {
|
||||
waitlistPosition := int(countClassBookings(existing, *request.ClassSessionID)) + 1
|
||||
if err := s.repo.AppendWaitlistEntry(ctx, db.WaitlistEntryParams{
|
||||
TenantID: tenant.ID,
|
||||
ClassSessionID: *request.ClassSessionID,
|
||||
CustomerName: customerName,
|
||||
CustomerEmail: customerEmail,
|
||||
Position: waitlistPosition,
|
||||
}); err != nil {
|
||||
return domain.CreateBookingResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := armReminderJobs(ctx, s.repo, tenant.ID, created.ID, startsAt, request.CustomerPhone); err != nil {
|
||||
return domain.CreateBookingResponse{}, err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user