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

+157
View File
@@ -0,0 +1,157 @@
// Package scheduling defines the platform-wide scheduling and timeslot seam:
// wire shapes, DTOs, domain events, and client interfaces for booking services,
// appointments, and capacity-based timeslots.
//
// Like platform/inventory and platform/order, this package depends only on the
// stdlib and shared platform types (money, uid, event).
package scheduling
import (
"encoding/json"
"errors"
"time"
)
var (
// ErrSlotUnavailable is returned when a requested slot has no remaining capacity.
ErrSlotUnavailable = errors.New("scheduling: requested timeslot is not available")
// ErrHoldExpired is returned when attempting to confirm or extend an expired hold.
ErrHoldExpired = errors.New("scheduling: hold has expired")
// ErrBookingNotFound is returned when the specified booking is not found.
ErrBookingNotFound = errors.New("scheduling: booking not found")
// ErrResourceNotFound is returned when the specified resource or schedule is not found.
ErrResourceNotFound = errors.New("scheduling: resource not found")
// ErrOutsideWorkingHours is returned when a hold/booking window does not
// lie inside the resource's configured working hours (weekly rules or a
// date override). Guards the hold/direct-booking/reschedule appliers so
// API clients cannot reserve capacity outside bookable windows.
ErrOutsideWorkingHours = errors.New("scheduling: window is outside working hours")
)
// Slot represents a discrete bookable window of time.
type Slot struct {
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Capacity int `json:"capacity"`
Remaining int `json:"remaining"`
}
// ResourceConfig defines a bookable resource (staff member, room, bay, equipment).
type ResourceConfig struct {
ID string `json:"id"`
Name string `json:"name"`
LocationID string `json:"location_id,omitempty"`
Timezone string `json:"timezone"`
Capacity int `json:"capacity"` // max concurrent bookings per slot (default 1)
SlotDuration time.Duration `json:"slot_duration"` // standard duration if not overridden
BufferBefore time.Duration `json:"buffer_before"` // buffer time before appointments
BufferAfter time.Duration `json:"buffer_after"` // buffer time after appointments
MinLeadTime time.Duration `json:"min_lead_time"` // minimum advance notice required
MaxAdvanceTime time.Duration `json:"max_advance_time"` // maximum booking window into the future
}
// WorkingHours defines recurring weekly schedule rules (e.g. Mon-Fri 09:00-17:00).
type WorkingHours struct {
DayOfWeek int `json:"day_of_week"` // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
StartTime string `json:"start_time"` // "09:00" (HH:MM)
EndTime string `json:"end_time"` // "17:00" (HH:MM)
}
// ScheduleOverride defines a specific blackout (holiday, sick leave) or custom opening.
type ScheduleOverride struct {
Date string `json:"date"` // "2026-09-01" (YYYY-MM-DD)
IsClosed bool `json:"is_closed"` // true for complete closure / blackout
CustomStart string `json:"custom_start,omitempty"` // custom opening if not closed
CustomEnd string `json:"custom_end,omitempty"`
Reason string `json:"reason,omitempty"`
}
// HoldRequest is the payload to request a temporary TTL reservation on a slot.
type HoldRequest struct {
ResourceID string `json:"resource_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Quantity int `json:"quantity"` // number of capacity units (default 1)
CartID string `json:"cart_id,omitempty"`
TTLSeconds int `json:"ttl_seconds,omitempty"` // hold duration in seconds (default 900 = 15m)
}
// HoldResult is returned when a slot hold is successfully placed.
type HoldResult struct {
HoldID string `json:"hold_id"`
ResourceID string `json:"resource_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Quantity int `json:"quantity"`
ExpiresAt time.Time `json:"expires_at"`
}
// BookingStatus represents the lifecycle state of a booking.
type BookingStatus string
const (
BookingStatusConfirmed BookingStatus = "confirmed"
BookingStatusCancelled BookingStatus = "cancelled"
BookingStatusCompleted BookingStatus = "completed"
BookingStatusNoShow BookingStatus = "no_show"
)
// BookingDetails holds a confirmed booking record.
type BookingDetails struct {
BookingID string `json:"booking_id"`
HoldID string `json:"hold_id,omitempty"`
OrderID string `json:"order_id,omitempty"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name,omitempty"`
SKU string `json:"sku,omitempty"`
ServiceName string `json:"service_name,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Quantity int `json:"quantity"`
CustomerEmail string `json:"customer_email,omitempty"`
CustomerName string `json:"customer_name,omitempty"`
CustomerPhone string `json:"customer_phone,omitempty"`
Notes string `json:"notes,omitempty"`
Status BookingStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
}
// BookingConfirmedEvent is the payload for event.SchedulingBookingConfirmed.
type BookingConfirmedEvent struct {
BookingID string `json:"booking_id"`
OrderID string `json:"order_id"`
ResourceID string `json:"resource_id"`
SKU string `json:"sku,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Quantity int `json:"quantity"`
CustomerEmail string `json:"customer_email,omitempty"`
CustomerName string `json:"customer_name,omitempty"`
}
// Encode serializes the event payload.
func (e BookingConfirmedEvent) Encode() ([]byte, error) { return json.Marshal(e) }
// DecodeBookingConfirmed parses the event payload.
func DecodeBookingConfirmed(raw []byte) (BookingConfirmedEvent, error) {
var e BookingConfirmedEvent
err := json.Unmarshal(raw, &e)
return e, err
}
// BookingCancelledEvent is the payload for event.SchedulingBookingCancelled.
type BookingCancelledEvent struct {
BookingID string `json:"booking_id"`
OrderID string `json:"order_id,omitempty"`
Reason string `json:"reason,omitempty"`
}
// Encode serializes the event payload.
func (e BookingCancelledEvent) Encode() ([]byte, error) { return json.Marshal(e) }
// DecodeBookingCancelled parses the event payload.
func DecodeBookingCancelled(raw []byte) (BookingCancelledEvent, error) {
var e BookingCancelledEvent
err := json.Unmarshal(raw, &e)
return e, err
}