package scheduling import ( "context" "sync" "time" ) // SchedulingPolicy is the in-process interface seam for scheduling operations. // Callers (cart, checkout, backoffice) depend on this interface rather than // concrete network transports or internal storage. type SchedulingPolicy interface { // GetAvailability queries available slots for a resource between from and to. GetAvailability(ctx context.Context, resourceID string, from, to time.Time, durationMinutes int) ([]Slot, error) // HoldSlot requests a temporary TTL hold on a slot. HoldSlot(ctx context.Context, req HoldRequest) (HoldResult, error) // ReleaseHold explicitly cancels a temporary hold. ReleaseHold(ctx context.Context, holdID string) error // ExtendHold extends the TTL on an active hold. ExtendHold(ctx context.Context, holdID string, extensionSeconds int) (HoldResult, error) // ConfirmBooking confirms a booking from an existing hold. ConfirmBooking(ctx context.Context, holdID string, orderID string, customerEmail, customerName, notes string) (BookingDetails, error) // CancelBooking cancels a confirmed booking. CancelBooking(ctx context.Context, bookingID, reason string) error } // InMemorySchedulingPolicy is an in-memory test double for unit testing without network dependencies. type InMemorySchedulingPolicy struct { mu sync.RWMutex holds map[string]HoldResult bookings map[string]BookingDetails availability map[string][]Slot } // NewInMemorySchedulingPolicy creates a test double. func NewInMemorySchedulingPolicy() *InMemorySchedulingPolicy { return &InMemorySchedulingPolicy{ holds: make(map[string]HoldResult), bookings: make(map[string]BookingDetails), availability: make(map[string][]Slot), } } func (m *InMemorySchedulingPolicy) SetSlots(resourceID string, slots []Slot) { m.mu.Lock() defer m.mu.Unlock() m.availability[resourceID] = slots } func (m *InMemorySchedulingPolicy) GetAvailability(ctx context.Context, resourceID string, from, to time.Time, durationMinutes int) ([]Slot, error) { m.mu.RLock() defer m.mu.RUnlock() slots, ok := m.availability[resourceID] if !ok { return []Slot{}, nil } var res []Slot for _, s := range slots { if !s.StartTime.Before(from) && !s.EndTime.After(to) && s.Remaining > 0 { res = append(res, s) } } return res, nil } func (m *InMemorySchedulingPolicy) HoldSlot(ctx context.Context, req HoldRequest) (HoldResult, error) { m.mu.Lock() defer m.mu.Unlock() ttl := req.TTLSeconds if ttl <= 0 { ttl = 900 } hold := HoldResult{ HoldID: "hold_" + time.Now().Format("20060102150405.000000"), ResourceID: req.ResourceID, StartTime: req.StartTime, EndTime: req.EndTime, Quantity: req.Quantity, ExpiresAt: time.Now().Add(time.Duration(ttl) * time.Second), } m.holds[hold.HoldID] = hold return hold, nil } func (m *InMemorySchedulingPolicy) ReleaseHold(ctx context.Context, holdID string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.holds, holdID) return nil } func (m *InMemorySchedulingPolicy) ExtendHold(ctx context.Context, holdID string, extensionSeconds int) (HoldResult, error) { m.mu.Lock() defer m.mu.Unlock() h, ok := m.holds[holdID] if !ok { return HoldResult{}, ErrHoldExpired } h.ExpiresAt = h.ExpiresAt.Add(time.Duration(extensionSeconds) * time.Second) m.holds[holdID] = h return h, nil } func (m *InMemorySchedulingPolicy) ConfirmBooking(ctx context.Context, holdID string, orderID string, customerEmail, customerName, notes string) (BookingDetails, error) { m.mu.Lock() defer m.mu.Unlock() h, ok := m.holds[holdID] if !ok { return BookingDetails{}, ErrHoldExpired } delete(m.holds, holdID) b := BookingDetails{ BookingID: "bk_" + time.Now().Format("20060102150405.000000"), HoldID: holdID, OrderID: orderID, ResourceID: h.ResourceID, StartTime: h.StartTime, EndTime: h.EndTime, Quantity: h.Quantity, CustomerEmail: customerEmail, CustomerName: customerName, Notes: notes, Status: BookingStatusConfirmed, CreatedAt: time.Now(), } m.bookings[b.BookingID] = b return b, nil } func (m *InMemorySchedulingPolicy) CancelBooking(ctx context.Context, bookingID, reason string) error { m.mu.Lock() defer m.mu.Unlock() b, ok := m.bookings[bookingID] if !ok { return ErrBookingNotFound } b.Status = BookingStatusCancelled m.bookings[bookingID] = b return nil }