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,221 @@
|
||||
package scheduling
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPSchedulingPolicy implements SchedulingPolicy by calling the scheduling service over HTTP.
|
||||
type HTTPSchedulingPolicy struct {
|
||||
baseURL string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewHTTPSchedulingPolicy creates a client against the scheduling HTTP endpoint (e.g. "http://scheduling:8080").
|
||||
func NewHTTPSchedulingPolicy(baseURL string, client *http.Client) *HTTPSchedulingPolicy {
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 5 * time.Second}
|
||||
}
|
||||
return &HTTPSchedulingPolicy{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) GetAvailability(ctx context.Context, resourceID string, from, to time.Time, durationMinutes int) ([]Slot, error) {
|
||||
u := fmt.Sprintf("%s/api/scheduling/resources/%s/availability?from=%s&to=%s",
|
||||
c.baseURL,
|
||||
url.PathEscape(resourceID),
|
||||
url.QueryEscape(from.Format(time.RFC3339)),
|
||||
url.QueryEscape(to.Format(time.RFC3339)),
|
||||
)
|
||||
if durationMinutes > 0 {
|
||||
u += "&duration=" + strconv.Itoa(durationMinutes)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scheduling client: GetAvailability failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, ErrResourceNotFound
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("scheduling client: unexpected status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var slots []Slot
|
||||
if err := json.NewDecoder(resp.Body).Decode(&slots); err != nil {
|
||||
return nil, fmt.Errorf("scheduling client: decode slots: %w", err)
|
||||
}
|
||||
return slots, nil
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) HoldSlot(ctx context.Context, reqData HoldRequest) (HoldResult, error) {
|
||||
data, err := json.Marshal(reqData)
|
||||
if err != nil {
|
||||
return HoldResult{}, err
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("%s/api/scheduling/holds", c.baseURL)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return HoldResult{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: HoldSlot failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
return HoldResult{}, ErrSlotUnavailable
|
||||
}
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: HoldSlot status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var res HoldResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: decode hold result: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) ReleaseHold(ctx context.Context, holdID string) error {
|
||||
u := fmt.Sprintf("%s/api/scheduling/holds/%s/release", c.baseURL, url.PathEscape(holdID))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scheduling client: ReleaseHold failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("scheduling client: ReleaseHold status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) ExtendHold(ctx context.Context, holdID string, extensionSeconds int) (HoldResult, error) {
|
||||
payload := map[string]int{"extension_seconds": extensionSeconds}
|
||||
data, _ := json.Marshal(payload)
|
||||
|
||||
u := fmt.Sprintf("%s/api/scheduling/holds/%s/extend", c.baseURL, url.PathEscape(holdID))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return HoldResult{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: ExtendHold failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusGone {
|
||||
return HoldResult{}, ErrHoldExpired
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: ExtendHold status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var res HoldResult
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return HoldResult{}, fmt.Errorf("scheduling client: decode extend result: %w", err)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) ConfirmBooking(ctx context.Context, holdID, orderID, customerEmail, customerName, notes string) (BookingDetails, error) {
|
||||
payload := map[string]string{
|
||||
"hold_id": holdID,
|
||||
"order_id": orderID,
|
||||
"customer_email": customerEmail,
|
||||
"customer_name": customerName,
|
||||
"notes": notes,
|
||||
}
|
||||
data, _ := json.Marshal(payload)
|
||||
|
||||
u := fmt.Sprintf("%s/api/scheduling/bookings", c.baseURL)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return BookingDetails{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return BookingDetails{}, fmt.Errorf("scheduling client: ConfirmBooking failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusGone || resp.StatusCode == http.StatusConflict {
|
||||
return BookingDetails{}, ErrHoldExpired
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return BookingDetails{}, fmt.Errorf("scheduling client: ConfirmBooking status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var details BookingDetails
|
||||
if err := json.NewDecoder(resp.Body).Decode(&details); err != nil {
|
||||
return BookingDetails{}, fmt.Errorf("scheduling client: decode booking details: %w", err)
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (c *HTTPSchedulingPolicy) CancelBooking(ctx context.Context, bookingID, reason string) error {
|
||||
payload := map[string]string{"reason": reason}
|
||||
data, _ := json.Marshal(payload)
|
||||
|
||||
u := fmt.Sprintf("%s/api/scheduling/bookings/%s/cancel", c.baseURL, url.PathEscape(bookingID))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, u, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scheduling client: CancelBooking failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return ErrBookingNotFound
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("scheduling client: CancelBooking status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user