// Package httpapi exposes the order endpoints a storefront (e.g. 独角数卡) calls: // create a payment (get a receiving address) and poll its status. package httpapi import ( "encoding/json" "errors" "net/http" "time" "github.com/wangjia/pangolin/pay/internal/pay" "github.com/wangjia/pangolin/pay/internal/store" ) type Handler struct{ svc *pay.Service } // New wires the routes (Go 1.22 method+wildcard patterns) and applies CORS for // the whitelisted browser origins (comma-separated; empty = no CORS headers). func New(svc *pay.Service, corsOrigins string) http.Handler { h := &Handler{svc: svc} mux := http.NewServeMux() mux.HandleFunc("POST /order", h.createOrder) mux.HandleFunc("GET /order/{orderNo}", h.getOrder) mux.HandleFunc("GET /_test", h.testPage) // same-origin manual test harness (Phase E) mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) return withCORS(mux, parseOrigins(corsOrigins)) } type createReq struct { UserRef string `json:"user_ref"` SKU string `json:"sku"` Amount int64 `json:"amount"` // base price, micro-USDT (1e-6) } type orderResp struct { OrderNo string `json:"order_no"` Address string `json:"address"` ExpectAmount int64 `json:"expect_amount"` Status string `json:"status"` ExpiresAt string `json:"expires_at"` TxID string `json:"tx_id,omitempty"` } func toResp(o *store.Order) orderResp { return orderResp{ OrderNo: o.OrderNo, Address: o.Address, ExpectAmount: o.ExpectAmount, Status: string(o.Status), ExpiresAt: o.ExpiresAt.UTC().Format(time.RFC3339), TxID: o.TxID, } } func writeJSON(w http.ResponseWriter, code int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func (h *Handler) createOrder(w http.ResponseWriter, r *http.Request) { var req createReq if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } o, err := h.svc.CreateOrder(r.Context(), req.UserRef, req.SKU, req.Amount) if errors.Is(err, pay.ErrUserHasActiveOrder) { writeJSON(w, http.StatusConflict, map[string]string{"error": "user already has an active order"}) return } if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } writeJSON(w, http.StatusCreated, toResp(o)) } func (h *Handler) getOrder(w http.ResponseWriter, r *http.Request) { o, err := h.svc.GetOrder(r.Context(), r.PathValue("orderNo")) if errors.Is(err, store.ErrNotFound) { writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) return } if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal"}) return } writeJSON(w, http.StatusOK, toResp(o)) }