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:
mats committed 2026-08-26 16:42:05 +02:00
1 parent 7f73d6885b
commit b16820bf62
5 files changed
+542 -10

No files matched your search

+145
View File
@@ -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
}