package orders import ( "errors" "net/http" "github.com/gin-gonic/gin" "github.com/google/uuid" "backend/internal/modules/pricing" "backend/internal/modules/products" ) type Handler struct { service *Service } func NewHandler(service *Service) *Handler { return &Handler{service: service} } type itemRequest struct { ProductID uuid.UUID `json:"product_id" binding:"required"` PriceTierID uuid.UUID `json:"price_tier_id" binding:"required"` Multiplier int64 `json:"multiplier" binding:"required,gte=1"` } type createRequest struct { CustomerName string `json:"customer_name" binding:"required"` CustomerEmail string `json:"customer_email" binding:"required,email"` CustomerPhone string `json:"customer_phone"` Notes string `json:"notes"` Items []itemRequest `json:"items" binding:"required,min=1,dive"` } type orderItemResponse struct { ProductID *uuid.UUID `json:"product_id,omitempty"` ProductName string `json:"product_name"` UnitSymbol string `json:"unit_symbol"` TierQuantity float64 `json:"tier_quantity"` Multiplier int64 `json:"multiplier"` UnitPriceCents int64 `json:"unit_price_cents"` TotalCents int64 `json:"total_cents"` } type orderResponse struct { ID uuid.UUID `json:"id"` CustomerName string `json:"customer_name"` CustomerEmail string `json:"customer_email"` CustomerPhone string `json:"customer_phone"` Status string `json:"status"` TotalCents int64 `json:"total_cents"` Notes string `json:"notes"` Items []orderItemResponse `json:"items,omitempty"` } func toResponse(order *Order, items []*OrderItem) orderResponse { resp := orderResponse{ ID: order.ID, CustomerName: order.CustomerName, CustomerEmail: order.CustomerEmail, CustomerPhone: order.CustomerPhone, Status: order.Status, TotalCents: order.TotalCents, Notes: order.Notes, } for _, item := range items { resp.Items = append(resp.Items, orderItemResponse{ ProductID: item.ProductID, ProductName: item.ProductName, UnitSymbol: item.UnitSymbol, TierQuantity: item.TierQuantity, Multiplier: item.Multiplier, UnitPriceCents: item.UnitPriceCents, TotalCents: item.TotalCents, }) } return resp } // Create is public: a visitor (connected or not, per spec section 17) can // place an order without an account. func (h *Handler) Create(c *gin.Context) { var req createRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()}) return } items := make([]ItemInput, 0, len(req.Items)) for _, item := range req.Items { items = append(items, ItemInput{ ProductID: item.ProductID, PriceTierID: item.PriceTierID, Multiplier: item.Multiplier, }) } order, orderItems, err := h.service.Create(c.Request.Context(), CreateInput{ CustomerName: req.CustomerName, CustomerEmail: req.CustomerEmail, CustomerPhone: req.CustomerPhone, Notes: req.Notes, Items: items, }) if err != nil { h.respondCreateError(c, err) return } c.JSON(http.StatusCreated, toResponse(order, orderItems)) } func (h *Handler) respondCreateError(c *gin.Context, err error) { switch { case errors.Is(err, ErrEmptyOrder), errors.Is(err, ErrProductMismatch): c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) case errors.Is(err, pricing.ErrNotFound): c.JSON(http.StatusBadRequest, gin.H{"error": "unknown price tier"}) case errors.Is(err, products.ErrNotFound): c.JSON(http.StatusBadRequest, gin.H{"error": "unknown product"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create order"}) } } func (h *Handler) ListAdmin(c *gin.Context) { list, err := h.service.List(c.Request.Context(), c.Query("status")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list orders"}) return } resp := make([]orderResponse, 0, len(list)) for _, order := range list { resp = append(resp, toResponse(order, nil)) } c.JSON(http.StatusOK, gin.H{"orders": resp}) } func (h *Handler) GetAdmin(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } order, items, err := h.service.Get(c.Request.Context(), id) if err != nil { if errors.Is(err, ErrNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "order not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load order"}) return } c.JSON(http.StatusOK, toResponse(order, items)) } type updateStatusRequest struct { Status string `json:"status" binding:"required"` } func (h *Handler) UpdateStatus(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return } var req updateStatusRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()}) return } order, err := h.service.UpdateStatus(c.Request.Context(), id, req.Status) if err != nil { switch { case errors.Is(err, ErrNotFound): c.JSON(http.StatusNotFound, gin.H{"error": "order not found"}) case errors.Is(err, ErrInvalidStatus): c.JSON(http.StatusBadRequest, gin.H{"error": "invalid status"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update order status"}) } return } c.JSON(http.StatusOK, toResponse(order, nil)) }