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 }