package handler import ( "net/http" "strconv" "github.com/gin-gonic/gin" appservice "github.com/genex/trading-service/internal/application/service" "github.com/genex/trading-service/internal/domain/vo" ) // TradeHandler handles user-facing trading HTTP endpoints. type TradeHandler struct { tradeService *appservice.TradeService } // NewTradeHandler creates a new TradeHandler with injected trade service. func NewTradeHandler(tradeService *appservice.TradeService) *TradeHandler { return &TradeHandler{tradeService: tradeService} } // PlaceOrderReq is the request body for placing an order. type PlaceOrderReq struct { CouponID string `json:"couponId" binding:"required"` Side string `json:"side" binding:"required,oneof=buy sell"` Type string `json:"type" binding:"required,oneof=limit market"` Price float64 `json:"price"` Quantity int `json:"quantity" binding:"required,min=1"` } // PlaceOrder handles POST /api/v1/trades/orders func (h *TradeHandler) PlaceOrder(c *gin.Context) { var req PlaceOrderReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } // Validate and convert to value objects side, err := vo.NewOrderSide(req.Side) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } orderType, err := vo.NewOrderType(req.Type) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } price, err := vo.NewPrice(req.Price) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } quantity, err := vo.NewQuantity(req.Quantity) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } userID := c.GetString("userId") output, err := h.tradeService.PlaceOrder(c.Request.Context(), appservice.PlaceOrderInput{ UserID: userID, CouponID: req.CouponID, Side: side, Type: orderType, Price: price, Quantity: quantity, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": -1, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{ "order": output.Order, "trades": output.Trades, }}) } // CancelOrder handles DELETE /api/v1/trades/orders/:id func (h *TradeHandler) CancelOrder(c *gin.Context) { couponID := c.Query("couponId") orderID := c.Param("id") sideStr := c.Query("side") side, err := vo.NewOrderSide(sideStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } err = h.tradeService.CancelOrder(c.Request.Context(), couponID, orderID, side) if err != nil { c.JSON(http.StatusNotFound, gin.H{"code": -1, "message": "Order not found"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": nil}) } // GetOrderBook handles GET /api/v1/trades/orderbook/:couponId func (h *TradeHandler) GetOrderBook(c *gin.Context) { couponID := c.Param("couponId") depth, _ := strconv.Atoi(c.DefaultQuery("depth", "20")) bids, asks := h.tradeService.GetOrderBookSnapshot(couponID, depth) c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{ "couponId": couponID, "bids": bids, "asks": asks, }}) } // MyOrders handles GET /api/v1/trades/my/orders — paginated list of current user's orders. func (h *TradeHandler) MyOrders(c *gin.Context) { userID := c.GetString("userId") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) status := c.Query("status") if page < 1 { page = 1 } if limit < 1 || limit > 100 { limit = 20 } orders, total, err := h.tradeService.GetOrdersByUserPaginated(c.Request.Context(), userID, status, page, limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": -1, "message": err.Error()}) return } type orderItem struct { ID string `json:"id"` CouponID string `json:"couponId"` CouponName string `json:"couponName"` Type string `json:"type"` Quantity int `json:"quantity"` Price float64 `json:"price"` TotalAmount float64 `json:"totalAmount"` Status string `json:"status"` CreatedAt string `json:"createdAt"` } items := make([]orderItem, len(orders)) for i, o := range orders { items[i] = orderItem{ ID: o.ID, CouponID: o.CouponID, CouponName: o.CouponID, // coupon name would come from coupon service; use ID as placeholder Type: o.Side.String(), Quantity: o.Quantity.Int(), Price: o.Price.Float64(), TotalAmount: o.Price.Float64() * float64(o.Quantity.Int()), Status: string(o.Status), CreatedAt: o.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), } } c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{ "orders": items, "total": total, "page": page, "limit": limit, }}) } // TransferCouponReq is the request body for transferring a coupon. type TransferCouponReq struct { RecipientID string `json:"recipientId"` RecipientPhone string `json:"recipientPhone"` } // TransferCoupon handles POST /api/v1/trades/coupons/:id/transfer — transfer coupon ownership. func (h *TradeHandler) TransferCoupon(c *gin.Context) { couponID := c.Param("id") userID := c.GetString("userId") var req TransferCouponReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } recipientID := req.RecipientID if recipientID == "" && req.RecipientPhone == "" { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": "recipientId or recipientPhone is required"}) return } // If recipientPhone is provided but no recipientId, use phone as placeholder ID. // In production, this would look up the user by phone via user-service. if recipientID == "" { recipientID = "phone:" + req.RecipientPhone } if recipientID == userID { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": "cannot transfer to yourself"}) return } err := h.tradeService.TransferCoupon(c.Request.Context(), couponID, userID, recipientID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": -1, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{ "couponId": couponID, "recipientId": recipientID, "status": "transferred", }}) }