mcp and ucp
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
package ucp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
||||
orderMessages "git.k6n.net/mats/go-cart-actor/proto/order"
|
||||
)
|
||||
|
||||
// OrderServer is the UCP REST adapter over the order grain pool.
|
||||
type OrderServer struct {
|
||||
applier OrderReadApplier
|
||||
}
|
||||
|
||||
// NewOrderServer builds a UCP REST adapter over an order grain pool.
|
||||
func NewOrderServer(applier OrderReadApplier) *OrderServer {
|
||||
return &OrderServer{applier: applier}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UCP Order endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// handleGetOrder handles GET /orders/{id}.
|
||||
func (s *OrderServer) handleGetOrder(w http.ResponseWriter, r *http.Request) {
|
||||
g, err := s.loadOrder(w, r)
|
||||
if err != nil || g == nil {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), g))
|
||||
}
|
||||
|
||||
// handleCancelOrder handles POST /orders/{id}/cancel.
|
||||
func (s *OrderServer) handleCancelOrder(w http.ResponseWriter, r *http.Request) {
|
||||
g, err := s.loadOrder(w, r)
|
||||
if err != nil || g == nil {
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
res, applyErr := s.applier.Apply(r.Context(), g.Id, &orderMessages.CancelOrder{
|
||||
Reason: req.Reason,
|
||||
AtMs: time.Now().UnixMilli(),
|
||||
})
|
||||
if applyErr != nil || res == nil || mutationRejected(res) {
|
||||
msg := "failed to cancel order"
|
||||
if res != nil && len(res.Mutations) > 0 && res.Mutations[0].Error != nil {
|
||||
msg += ": " + res.Mutations[0].Error.Error()
|
||||
} else if applyErr != nil {
|
||||
msg += ": " + applyErr.Error()
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, msg)
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := s.applier.Get(r.Context(), g.Id)
|
||||
if updated != nil {
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), updated))
|
||||
} else {
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), g))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreateFulfillment handles POST /orders/{id}/fulfillments.
|
||||
func (s *OrderServer) handleCreateFulfillment(w http.ResponseWriter, r *http.Request) {
|
||||
g, err := s.loadOrder(w, r)
|
||||
if err != nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req 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,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fid, _ := order.NewOrderId()
|
||||
msg := &orderMessages.CreateFulfillment{
|
||||
Id: "f_" + fid.String(),
|
||||
Carrier: req.Carrier,
|
||||
TrackingNumber: req.TrackingNumber,
|
||||
TrackingUri: req.TrackingURI,
|
||||
AtMs: time.Now().UnixMilli(),
|
||||
}
|
||||
for _, l := range req.Lines {
|
||||
msg.Lines = append(msg.Lines, &orderMessages.FulfillmentLine{Reference: l.Reference, Quantity: l.Quantity})
|
||||
}
|
||||
|
||||
res, applyErr := s.applier.Apply(r.Context(), g.Id, msg)
|
||||
if applyErr != nil || res == nil || mutationRejected(res) {
|
||||
msg := "failed to create fulfillment"
|
||||
if res != nil && len(res.Mutations) > 0 && res.Mutations[0].Error != nil {
|
||||
msg += ": " + res.Mutations[0].Error.Error()
|
||||
} else if applyErr != nil {
|
||||
msg += ": " + applyErr.Error()
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, msg)
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := s.applier.Get(r.Context(), g.Id)
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), updated))
|
||||
}
|
||||
|
||||
// handleRequestReturn handles POST /orders/{id}/returns.
|
||||
func (s *OrderServer) handleRequestReturn(w http.ResponseWriter, r *http.Request) {
|
||||
g, err := s.loadOrder(w, r)
|
||||
if err != nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Lines []struct {
|
||||
Reference string `json:"reference"`
|
||||
Quantity int32 `json:"quantity"`
|
||||
} `json:"lines,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
rid, _ := order.NewOrderId()
|
||||
msg := &orderMessages.RequestReturn{
|
||||
Id: "r_" + rid.String(),
|
||||
Reason: req.Reason,
|
||||
AtMs: time.Now().UnixMilli(),
|
||||
}
|
||||
for _, l := range req.Lines {
|
||||
msg.Lines = append(msg.Lines, &orderMessages.FulfillmentLine{Reference: l.Reference, Quantity: l.Quantity})
|
||||
}
|
||||
|
||||
res, applyErr := s.applier.Apply(r.Context(), g.Id, msg)
|
||||
if applyErr != nil || res == nil || mutationRejected(res) {
|
||||
msg := "failed to request return"
|
||||
if res != nil && len(res.Mutations) > 0 && res.Mutations[0].Error != nil {
|
||||
msg += ": " + res.Mutations[0].Error.Error()
|
||||
} else if applyErr != nil {
|
||||
msg += ": " + applyErr.Error()
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, msg)
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := s.applier.Get(r.Context(), g.Id)
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), updated))
|
||||
}
|
||||
|
||||
// handleIssueRefund handles POST /orders/{id}/refunds.
|
||||
func (s *OrderServer) handleIssueRefund(w http.ResponseWriter, r *http.Request) {
|
||||
g, err := s.loadOrder(w, r)
|
||||
if err != nil || g == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Amount int64 `json:"amount,omitempty"` // minor units; omit for full remaining
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
amount := req.Amount
|
||||
if amount <= 0 {
|
||||
amount = g.CapturedAmount - g.RefundedAmount
|
||||
}
|
||||
if amount <= 0 {
|
||||
writeError(w, http.StatusBadRequest, "no captured amount to refund")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := order.OrderId(g.Id).String()
|
||||
res, applyErr := s.applier.Apply(r.Context(), g.Id, &orderMessages.IssueRefund{
|
||||
Provider: "ucp",
|
||||
Amount: amount,
|
||||
Reference: "ref-ucp-" + idStr,
|
||||
AtMs: time.Now().UnixMilli(),
|
||||
})
|
||||
if applyErr != nil || res == nil || mutationRejected(res) {
|
||||
msg := "failed to issue refund"
|
||||
if res != nil && len(res.Mutations) > 0 && res.Mutations[0].Error != nil {
|
||||
msg += ": " + res.Mutations[0].Error.Error()
|
||||
} else if applyErr != nil {
|
||||
msg += ": " + applyErr.Error()
|
||||
}
|
||||
writeError(w, http.StatusUnprocessableEntity, msg)
|
||||
return
|
||||
}
|
||||
|
||||
updated, _ := s.applier.Get(r.Context(), g.Id)
|
||||
writeJSON(w, http.StatusOK, orderGrainToResponse(r.PathValue("id"), updated))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// mutationRejected returns true if the Apply result indicates the mutation was
|
||||
// rejected (business-rule violation rather than infrastructure error).
|
||||
func mutationRejected(res *actor.MutationResult[order.OrderGrain]) bool {
|
||||
return len(res.Mutations) > 0 && res.Mutations[0].Error != nil
|
||||
}
|
||||
|
||||
// loadOrder parses the id from the path and returns the grain, or writes an
|
||||
// error response and returns nil.
|
||||
func (s *OrderServer) loadOrder(w http.ResponseWriter, r *http.Request) (*order.OrderGrain, error) {
|
||||
raw := r.PathValue("id")
|
||||
if raw == "" {
|
||||
writeError(w, http.StatusBadRequest, "missing order id")
|
||||
return nil, nil
|
||||
}
|
||||
id, ok := order.ParseOrderId(raw)
|
||||
if !ok {
|
||||
writeError(w, http.StatusBadRequest, "invalid order id")
|
||||
return nil, nil
|
||||
}
|
||||
g, err := s.applier.Get(r.Context(), uint64(id))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "order not found")
|
||||
return nil, nil
|
||||
}
|
||||
if g.Status == order.StatusNew {
|
||||
writeError(w, http.StatusNotFound, "order not found")
|
||||
return nil, nil
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// orderGrainToResponse converts an OrderGrain to an OrderResponse.
|
||||
func orderGrainToResponse(id string, g *order.OrderGrain) OrderResponse {
|
||||
resp := OrderResponse{
|
||||
OrderId: id,
|
||||
Reference: g.OrderReference,
|
||||
Status: string(g.Status),
|
||||
Currency: g.Currency,
|
||||
TotalAmount: g.TotalAmount,
|
||||
TotalTax: g.TotalTax,
|
||||
Lines: make([]OrderLineResp, 0, len(g.Lines)),
|
||||
Payments: make([]OrderPaymentResp, 0, len(g.Payments)),
|
||||
Fulfillments: make([]OrderFulfillmentResp, 0, len(g.Fulfillments)),
|
||||
Returns: make([]OrderReturnResp, 0, len(g.Returns)),
|
||||
Refunds: make([]OrderRefundResp, 0, len(g.Refunds)),
|
||||
}
|
||||
|
||||
for _, l := range g.Lines {
|
||||
resp.Lines = append(resp.Lines, OrderLineResp{
|
||||
Reference: l.Reference,
|
||||
Sku: l.Sku,
|
||||
Name: l.Name,
|
||||
Quantity: l.Quantity,
|
||||
UnitPrice: l.UnitPrice,
|
||||
TaxRate: l.TaxRate,
|
||||
TotalAmount: l.TotalAmount,
|
||||
TotalTax: l.TotalTax,
|
||||
Fulfilled: l.Fulfilled,
|
||||
})
|
||||
}
|
||||
|
||||
for _, p := range g.Payments {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
resp.Payments = append(resp.Payments, OrderPaymentResp{
|
||||
Provider: p.Provider,
|
||||
Authorized: p.Authorized,
|
||||
Captured: p.Captured,
|
||||
Refunded: p.Refunded,
|
||||
AuthRef: p.AuthRef,
|
||||
CaptureRef: p.CaptureRef,
|
||||
})
|
||||
}
|
||||
|
||||
for _, f := range g.Fulfillments {
|
||||
lines := make([]OrderLineEntry, 0, len(f.Lines))
|
||||
for _, l := range f.Lines {
|
||||
lines = append(lines, OrderLineEntry{Reference: l.Reference, Quantity: l.Quantity})
|
||||
}
|
||||
resp.Fulfillments = append(resp.Fulfillments, OrderFulfillmentResp{
|
||||
Id: f.ID,
|
||||
Carrier: f.Carrier,
|
||||
TrackingNumber: f.TrackingNumber,
|
||||
TrackingURI: f.TrackingURI,
|
||||
Lines: lines,
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range g.Returns {
|
||||
lines := make([]OrderLineEntry, 0, len(r.Lines))
|
||||
for _, l := range r.Lines {
|
||||
lines = append(lines, OrderLineEntry{Reference: l.Reference, Quantity: l.Quantity})
|
||||
}
|
||||
resp.Returns = append(resp.Returns, OrderReturnResp{
|
||||
Id: r.ID,
|
||||
Reason: r.Reason,
|
||||
Lines: lines,
|
||||
})
|
||||
}
|
||||
|
||||
for _, r := range g.Refunds {
|
||||
resp.Refunds = append(resp.Refunds, OrderRefundResp{
|
||||
Provider: r.Provider,
|
||||
Amount: r.Amount,
|
||||
Reference: r.Reference,
|
||||
})
|
||||
}
|
||||
|
||||
resp.CapturedAmount = g.CapturedAmount
|
||||
resp.RefundedAmount = g.RefundedAmount
|
||||
resp.CustomerEmail = g.CustomerEmail
|
||||
resp.CustomerName = g.CustomerName
|
||||
resp.CartId = g.CartId
|
||||
resp.PlacedAt = g.PlacedAt
|
||||
resp.UpdatedAt = g.UpdatedAt
|
||||
|
||||
return resp
|
||||
}
|
||||
Reference in New Issue
Block a user