Bump go-cart-actor: scheduling cluster service + booking-hold cart/checkout seam
Deploys cmd/scheduling as a 3-replica actor cluster (shared RWX commerce-data event logs, gRPC mesh via POD_IP + actor-pool label, edge /api/scheduling pass-through, fail-closed mcpauth on admin/MCP). cart/checkout get SCHEDULING_SERVICE_URL so line removal releases holds; backoffice gets SCHEDULING_URL for the admin proxy. Platform seam: order.Line.Properties carries booking_* fields through order.created; scheduling.booking_confirmed/ cancelled event types. Compose + prep-dev-data wire the schedules dir. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
1 parent
7f73d6885b
commit
b16820bf62
5 files changed
+542
-10
No files matched your search
@@ -0,0 +1,145 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user