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>
158 lines
6.7 KiB
Go
158 lines
6.7 KiB
Go
// 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
|
|
}
|