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) }