180 lines
5.9 KiB
Go
180 lines
5.9 KiB
Go
package order
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/flow"
|
|
)
|
|
|
|
type InventoryReservationLine struct {
|
|
SKU string `json:"sku"`
|
|
LocationID string `json:"locationId"`
|
|
Quantity uint32 `json:"quantity"`
|
|
}
|
|
|
|
type InventoryReservationRequest struct {
|
|
HolderID string `json:"holderId"`
|
|
TTL time.Duration `json:"-"`
|
|
Lines []InventoryReservationLine `json:"lines"`
|
|
}
|
|
|
|
type InventoryReservationService interface {
|
|
Reserve(ctx context.Context, req InventoryReservationRequest) error
|
|
Release(ctx context.Context, req InventoryReservationRequest) error
|
|
}
|
|
|
|
type HTTPInventoryReservationService struct {
|
|
baseURL string
|
|
client *http.Client
|
|
}
|
|
|
|
func NewHTTPInventoryReservationService(baseURL string, client *http.Client) (*HTTPInventoryReservationService, error) {
|
|
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
|
if baseURL == "" {
|
|
return nil, fmt.Errorf("inventory reservation service: missing base url")
|
|
}
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 10 * time.Second}
|
|
}
|
|
return &HTTPInventoryReservationService{baseURL: baseURL, client: client}, nil
|
|
}
|
|
|
|
func (s *HTTPInventoryReservationService) Reserve(ctx context.Context, req InventoryReservationRequest) error {
|
|
return s.post(ctx, "/reservations", req)
|
|
}
|
|
|
|
func (s *HTTPInventoryReservationService) Release(ctx context.Context, req InventoryReservationRequest) error {
|
|
return s.post(ctx, "/reservations/release", req)
|
|
}
|
|
|
|
func (s *HTTPInventoryReservationService) post(ctx context.Context, path string, req InventoryReservationRequest) error {
|
|
body := map[string]any{
|
|
"holderId": req.HolderID,
|
|
"lines": req.Lines,
|
|
}
|
|
if req.TTL > 0 {
|
|
body["ttlSeconds"] = int64(req.TTL / time.Second)
|
|
}
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.baseURL+path, bytes.NewReader(raw))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
resp, err := s.client.Do(httpReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 300 {
|
|
return fmt.Errorf("inventory reservation service: %s returned %d", path, resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type reserveInventoryParams struct {
|
|
HolderID string `json:"holderId,omitempty"`
|
|
TTLSeconds int64 `json:"ttlSeconds,omitempty"`
|
|
Lines []struct {
|
|
Reference string `json:"reference"`
|
|
Quantity int32 `json:"quantity"`
|
|
LocationID string `json:"locationId,omitempty"`
|
|
} `json:"lines"`
|
|
}
|
|
|
|
func RegisterInventoryReservationActions(reg *flow.Registry, app Applier, svc InventoryReservationService) {
|
|
reg.ActionWithMeta("reserve_inventory", func(ctx context.Context, st *flow.State, params json.RawMessage) error {
|
|
if svc == nil {
|
|
return fmt.Errorf("reserve_inventory: no reservation service configured")
|
|
}
|
|
req, err := buildInventoryReservationRequest(ctx, app, st, params, "reserve_inventory")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return svc.Reserve(ctx, req)
|
|
}, flow.CapabilityMeta{
|
|
Description: "Reserve inventory for specific order lines through the inventory service.",
|
|
ExampleParams: json.RawMessage(`{"ttlSeconds":900,"lines":[{"reference":"l1","quantity":1,"locationId":"se"}]}`),
|
|
})
|
|
|
|
reg.ActionWithMeta("release_inventory", func(ctx context.Context, st *flow.State, params json.RawMessage) error {
|
|
if svc == nil {
|
|
return fmt.Errorf("release_inventory: no reservation service configured")
|
|
}
|
|
req, err := buildInventoryReservationRequest(ctx, app, st, params, "release_inventory")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return svc.Release(ctx, req)
|
|
}, flow.CapabilityMeta{
|
|
Description: "Release a prior inventory reservation for specific order lines.",
|
|
ExampleParams: json.RawMessage(`{"lines":[{"reference":"l1","quantity":1,"locationId":"se"}]}`),
|
|
})
|
|
}
|
|
|
|
func buildInventoryReservationRequest(ctx context.Context, app Applier, st *flow.State, params json.RawMessage, action string) (InventoryReservationRequest, error) {
|
|
var p reserveInventoryParams
|
|
if err := json.Unmarshal(params, &p); err != nil {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: bad params: %w", action, err)
|
|
}
|
|
if len(p.Lines) == 0 {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: missing lines", action)
|
|
}
|
|
o, err := app.Get(ctx, st.ID)
|
|
if err != nil {
|
|
return InventoryReservationRequest{}, err
|
|
}
|
|
holderID := p.HolderID
|
|
if holderID == "" {
|
|
holderID = "order:" + OrderId(st.ID).String()
|
|
}
|
|
req := InventoryReservationRequest{HolderID: holderID}
|
|
if p.TTLSeconds > 0 {
|
|
req.TTL = time.Duration(p.TTLSeconds) * time.Second
|
|
}
|
|
for _, lineReq := range p.Lines {
|
|
if lineReq.Reference == "" {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: line missing reference", action)
|
|
}
|
|
if lineReq.Quantity <= 0 {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: line %q has invalid quantity %d", action, lineReq.Reference, lineReq.Quantity)
|
|
}
|
|
line := o.findLine(lineReq.Reference)
|
|
if line == nil {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: unknown line %q", action, lineReq.Reference)
|
|
}
|
|
if line.Sku == "" {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: line %q has no sku", action, lineReq.Reference)
|
|
}
|
|
remaining := line.Quantity - line.Fulfilled
|
|
if int(lineReq.Quantity) > remaining {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: line %q quantity %d exceeds remaining fulfillable %d", action, lineReq.Reference, lineReq.Quantity, remaining)
|
|
}
|
|
locationID := strings.TrimSpace(lineReq.LocationID)
|
|
if locationID == "" {
|
|
locationID = strings.TrimSpace(line.Location)
|
|
}
|
|
if locationID == "" {
|
|
locationID = strings.TrimSpace(o.Country)
|
|
}
|
|
if locationID == "" {
|
|
return InventoryReservationRequest{}, fmt.Errorf("%s: line %q has no location", action, lineReq.Reference)
|
|
}
|
|
req.Lines = append(req.Lines, InventoryReservationLine{
|
|
SKU: line.Sku,
|
|
LocationID: locationID,
|
|
Quantity: uint32(lineReq.Quantity),
|
|
})
|
|
}
|
|
return req, nil
|
|
}
|