220 lines
6.3 KiB
Go
220 lines
6.3 KiB
Go
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",
|
|
}})
|
|
}
|