232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/flow"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
|
messages "git.k6n.net/mats/go-cart-actor/proto/order"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
func nowMs() int64 { return time.Now().UnixMilli() }
|
|
|
|
// applyOne applies a single mutation and surfaces the handler error (the pool
|
|
// returns a top-level error only for unregistered mutations; a rejected
|
|
// transition is carried per-mutation in the result).
|
|
func applyOne(ctx context.Context, app *orderedApplier, id uint64, msg proto.Message) error {
|
|
res, err := app.Apply(ctx, id, msg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if res != nil {
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
return m.Error
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// capturedRef returns the capture reference of the first captured payment, which
|
|
// the payment provider needs to issue a refund against.
|
|
func capturedRef(g *order.OrderGrain) string {
|
|
for _, p := range g.Payments {
|
|
if p.Captured > 0 && p.CaptureRef != "" {
|
|
return p.CaptureRef
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// loadOrder parses the id and returns the current grain, writing 400/404 itself
|
|
// and returning ok=false when the caller should stop.
|
|
func (s *server) loadOrder(w http.ResponseWriter, r *http.Request) (order.OrderId, *order.OrderGrain, bool) {
|
|
id, ok := order.ParseOrderId(r.PathValue("id"))
|
|
if !ok {
|
|
writeErr(w, http.StatusBadRequest, fmt.Errorf("invalid order id"))
|
|
return 0, nil, false
|
|
}
|
|
g, err := s.applier.Get(r.Context(), uint64(id))
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err)
|
|
return 0, nil, false
|
|
}
|
|
if g.Status == order.StatusNew {
|
|
writeErr(w, http.StatusNotFound, fmt.Errorf("order not found"))
|
|
return 0, nil, false
|
|
}
|
|
return id, g, true
|
|
}
|
|
|
|
// applyAndRespond applies a mutation and returns the updated order. A rejected
|
|
// state transition (handler error) maps to 409 Conflict.
|
|
func (s *server) applyAndRespond(w http.ResponseWriter, r *http.Request, id order.OrderId, msg proto.Message) {
|
|
res, err := s.applier.Apply(r.Context(), uint64(id), msg)
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
for _, m := range res.Mutations {
|
|
if m.Error != nil {
|
|
writeErr(w, http.StatusConflict, m.Error)
|
|
return
|
|
}
|
|
}
|
|
grain, err := s.applier.Get(r.Context(), uint64(id))
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"orderId": id.String(), "order": grain})
|
|
}
|
|
|
|
// --- lifecycle (b) --------------------------------------------------------
|
|
|
|
type fulfillReq struct {
|
|
Carrier string `json:"carrier,omitempty"`
|
|
TrackingNumber string `json:"trackingNumber,omitempty"`
|
|
TrackingURI string `json:"trackingUri,omitempty"`
|
|
Lines []struct {
|
|
Reference string `json:"reference"`
|
|
Quantity int32 `json:"quantity"`
|
|
} `json:"lines"`
|
|
}
|
|
|
|
func (s *server) handleFulfill(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := s.loadOrder(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req fulfillReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
fid, _ := order.NewOrderId()
|
|
msg := &messages.CreateFulfillment{
|
|
Id: "f_" + fid.String(),
|
|
Carrier: req.Carrier,
|
|
TrackingNumber: req.TrackingNumber,
|
|
TrackingUri: req.TrackingURI,
|
|
AtMs: nowMs(),
|
|
}
|
|
for _, l := range req.Lines {
|
|
msg.Lines = append(msg.Lines, &messages.FulfillmentLine{Reference: l.Reference, Quantity: l.Quantity})
|
|
}
|
|
s.applyAndRespond(w, r, id, msg)
|
|
}
|
|
|
|
func (s *server) handleComplete(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := s.loadOrder(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
s.applyAndRespond(w, r, id, &messages.CompleteOrder{AtMs: nowMs()})
|
|
}
|
|
|
|
func (s *server) handleCancel(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := s.loadOrder(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
s.applyAndRespond(w, r, id, &messages.CancelOrder{Reason: req.Reason, AtMs: nowMs()})
|
|
}
|
|
|
|
type returnReq struct {
|
|
Reason string `json:"reason,omitempty"`
|
|
Lines []struct {
|
|
Reference string `json:"reference"`
|
|
Quantity int32 `json:"quantity"`
|
|
} `json:"lines"`
|
|
}
|
|
|
|
func (s *server) handleReturn(w http.ResponseWriter, r *http.Request) {
|
|
id, _, ok := s.loadOrder(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req returnReq
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
rid, _ := order.NewOrderId()
|
|
msg := &messages.RequestReturn{Id: "r_" + rid.String(), Reason: req.Reason, AtMs: nowMs()}
|
|
for _, l := range req.Lines {
|
|
msg.Lines = append(msg.Lines, &messages.FulfillmentLine{Reference: l.Reference, Quantity: l.Quantity})
|
|
}
|
|
s.applyAndRespond(w, r, id, msg)
|
|
}
|
|
|
|
func (s *server) handleRefund(w http.ResponseWriter, r *http.Request) {
|
|
id, g, ok := s.loadOrder(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
var req struct {
|
|
Amount int64 `json:"amount"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
amount := req.Amount
|
|
if amount <= 0 {
|
|
amount = g.CapturedAmount - g.RefundedAmount // default: full remaining
|
|
}
|
|
captureRef := capturedRef(g)
|
|
ref, err := s.provider.Refund(r.Context(), captureRef, amount)
|
|
if err != nil {
|
|
writeErr(w, http.StatusBadGateway, fmt.Errorf("refund failed: %w", err))
|
|
return
|
|
}
|
|
s.applyAndRespond(w, r, id, &messages.IssueRefund{
|
|
Provider: ref.Provider,
|
|
Amount: ref.Amount,
|
|
Reference: ref.Reference,
|
|
AtMs: nowMs(),
|
|
})
|
|
}
|
|
|
|
// --- saga / flow admin (a) ------------------------------------------------
|
|
|
|
func (s *server) handleCapabilities(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, s.freg.Capabilities())
|
|
}
|
|
|
|
func (s *server) handleListFlows(w http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(w, http.StatusOK, s.flows.list())
|
|
}
|
|
|
|
func (s *server) handleGetFlow(w http.ResponseWriter, r *http.Request) {
|
|
def, ok := s.flows.get(r.PathValue("name"))
|
|
if !ok {
|
|
writeErr(w, http.StatusNotFound, fmt.Errorf("flow not found"))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, def)
|
|
}
|
|
|
|
func (s *server) handleSaveFlow(w http.ResponseWriter, r *http.Request) {
|
|
name := r.PathValue("name")
|
|
var def flow.Definition
|
|
if err := json.NewDecoder(r.Body).Decode(&def); err != nil {
|
|
writeErr(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
if err := s.flows.save(name, &def); err != nil {
|
|
// Validation failures (unknown action/hook/predicate) are client errors.
|
|
writeErr(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, &def)
|
|
}
|