package handlers import ( "math" "net/http" "strconv" "time" "sale/internal/models" "sale/internal/schemas" "sale/internal/utils" "github.com/gin-gonic/gin" ) type CartHandler struct{} func NewCartHandler() *CartHandler { return &CartHandler{} } func (h *CartHandler) List(c *gin.Context) { userID := c.GetUint("user_id") var carts []models.Cart utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts) c.JSON(http.StatusOK, gin.H{"data": carts}) } func (h *CartHandler) Add(c *gin.Context) { userID := c.GetUint("user_id") var req schemas.AddToCartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var cart models.Cart result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&cart) if result.Error == nil { cart.Quantity += req.Quantity utils.DB.Save(&cart) } else { cart = models.Cart{ UserID: userID, ProductID: req.ProductID, Quantity: req.Quantity, } utils.DB.Create(&cart) } utils.DB.Preload("Product").First(&cart, cart.ID) c.JSON(http.StatusOK, gin.H{"data": cart}) } func (h *CartHandler) Update(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var cart models.Cart if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&cart).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Cart item not found"}) return } var req schemas.UpdateCartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cart.Quantity = req.Quantity utils.DB.Save(&cart) utils.DB.Preload("Product").First(&cart, cart.ID) c.JSON(http.StatusOK, gin.H{"data": cart}) } func (h *CartHandler) Delete(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Cart{}).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cart item"}) return } c.JSON(http.StatusOK, gin.H{"message": "Cart item deleted successfully"}) } type OrderHandler struct{} func NewOrderHandler() *OrderHandler { return &OrderHandler{} } func checkOrderExpired(order *models.Order) { if order.Status != models.OrderStatusPendingPayment || order.PaymentExpiresAt == nil { return } if time.Now().After(*order.PaymentExpiresAt) { tx := utils.DB.Begin() tx.Model(order).Update("status", models.OrderStatusCancelled) for _, item := range order.OrderItems { var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", product.CreditCost*item.Quantity)) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } } tx.Commit() order.Status = models.OrderStatusCancelled } } func checkOrdersExpired(orders []models.Order) { for i := range orders { checkOrderExpired(&orders[i]) } } func (h *OrderHandler) List(c *gin.Context) { userID := c.GetUint("user_id") role, _ := c.Get("role") var orders []models.Order query := utils.DB.Model(&models.Order{}) if role == "user" { query = query.Where("user_id = ?", userID) } else if role == "supplier" { query = query.Where("supplier_id = ?", userID) } query.Preload("OrderItems.Product").Preload("ShippingAddress"). Order("created_at DESC").Find(&orders) checkOrdersExpired(orders) c.JSON(http.StatusOK, gin.H{"data": orders}) } func (h *OrderHandler) GetByID(c *gin.Context) { userID := c.GetUint("user_id") role, _ := c.Get("role") id, _ := strconv.Atoi(c.Param("id")) var order models.Order query := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User") if role == "user" { query = query.Where("user_id = ?", userID) } else if role == "supplier" { query = query.Where("supplier_id = ?", userID) } if err := query.First(&order, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } checkOrderExpired(&order) c.JSON(http.StatusOK, gin.H{"data": order}) } func (h *OrderHandler) Create(c *gin.Context) { userID := c.GetUint("user_id") var req schemas.CreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var carts []models.Cart cartQuery := utils.DB.Where("user_id = ?", userID).Preload("Product") if len(req.CartItemIDs) > 0 { cartQuery = cartQuery.Where("id IN ?", req.CartItemIDs) } cartQuery.Find(&carts) if len(carts) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Cart is empty"}) return } var subtotal float64 var totalQuantity int var orderItems []models.OrderItem supplierMap := make(map[uint]bool) for _, cart := range carts { if !cart.Product.IsActive { c.JSON(http.StatusBadRequest, gin.H{"error": "Product " + cart.Product.Name + " is no longer available"}) return } if cart.Product.MinPurchase > 0 && cart.Quantity < cart.Product.MinPurchase { c.JSON(http.StatusBadRequest, gin.H{"error": cart.Product.Name + " minimum purchase is " + strconv.Itoa(cart.Product.MinPurchase)}) return } if cart.Product.MaxPurchase != nil && *cart.Product.MaxPurchase > 0 && cart.Quantity > *cart.Product.MaxPurchase { c.JSON(http.StatusBadRequest, gin.H{"error": cart.Product.Name + " maximum purchase is " + strconv.Itoa(*cart.Product.MaxPurchase)}) return } if cart.Product.RequireCredit { var user models.User utils.DB.First(&user, userID) if user.PurchaseCredits < cart.Product.CreditCost*cart.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits for " + cart.Product.Name}) return } } var inventory models.Inventory if err := utils.DB.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil { if inventory.Quantity < cart.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for " + cart.Product.Name}) return } } subtotal += cart.Product.Price * float64(cart.Quantity) totalQuantity += cart.Quantity orderItems = append(orderItems, models.OrderItem{ ProductID: cart.ProductID, Quantity: cart.Quantity, Price: cart.Product.Price, }) if inventory.SupplierID != 0 { supplierMap[inventory.SupplierID] = true } } var supplierID *uint for sid := range supplierMap { sid := sid supplierID = &sid break } var shippingFeeFirstWeight, shippingFeePerGram, serviceFeeRate, taxRate float64 var setting models.SystemSetting if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil { shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil { shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil { serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "tax_rate").First(&setting).Error; err == nil { taxRate, _ = strconv.ParseFloat(setting.Value, 64) } shippingFee := shippingFeeFirstWeight if subtotal >= 99 || shippingFeeFirstWeight == 0 { shippingFee = 0 } else if totalQuantity > 500 { shippingFee += shippingFeePerGram * float64(totalQuantity-500) } serviceFee := subtotal * serviceFeeRate / 100 tax := subtotal * taxRate / 100 totalAmount := subtotal + shippingFee + serviceFee + tax order := models.Order{ UserID: userID, SupplierID: supplierID, Subtotal: subtotal, ShippingFee: shippingFee, ServiceFee: serviceFee, Tax: tax, TotalAmount: totalAmount, Status: models.OrderStatusPendingPayment, ShippingAddressID: &req.ShippingAddressID, PaymentMethod: req.PaymentMethod, } tx := utils.DB.Begin() if err := tx.Create(&order).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"}) return } for i := range orderItems { orderItems[i].OrderID = order.ID } if err := tx.Create(&orderItems).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order items"}) return } for _, cart := range carts { if cart.Product.RequireCredit { if err := tx.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", cart.Product.CreditCost*cart.Quantity)).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to deduct credits"}) return } } if cart.Product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity)) } var inventory models.Inventory if err := tx.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil { if inventory.Quantity >= cart.Quantity { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity-cart.Quantity) } } } cartIDs := make([]uint, len(carts)) for i, cart := range carts { cartIDs[i] = cart.ID } if err := tx.Where("id IN ? AND user_id = ?", cartIDs, userID).Delete(&models.Cart{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear cart"}) return } tx.Commit() utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID) c.JSON(http.StatusCreated, gin.H{"data": order}) } func (h *OrderHandler) Refund(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status == models.OrderStatusShipped || order.Status == models.OrderStatusCompleted { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot refund a shipped or completed order"}) return } if order.RefundStatus == models.RefundStatusPending { c.JSON(http.StatusBadRequest, gin.H{"error": "Refund already requested"}) return } var req schemas.RefundRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } utils.DB.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusPending, "refund_reason": req.Reason, "status": models.OrderStatusRefunding, }) c.JSON(http.StatusOK, gin.H{"message": "Refund request submitted successfully"}) } func (h *OrderHandler) ConfirmOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingConfirm { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"}) return } utils.DB.Model(&order).Update("status", models.OrderStatusPendingShip) c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"}) } func (h *OrderHandler) ShipOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingShip { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be shipped"}) return } var req schemas.ShipOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } utils.DB.Model(&order).Updates(map[string]interface{}{ "status": models.OrderStatusShipped, "tracking_number": req.TrackingNumber, "shipping_photo": req.ShippingPhoto, "express_photo": req.ExpressPhoto, "customs_photo": req.CustomsPhoto, }) c.JSON(http.StatusOK, gin.H{"message": "Order shipped successfully"}) } func (h *OrderHandler) AdminList(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) var total int64 utils.DB.Model(&models.Order{}).Count(&total) var orders []models.Order offset := (page - 1) * pageSize utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User"). Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders) checkOrdersExpired(orders) totalPages := int(math.Ceil(float64(total) / float64(pageSize))) c.JSON(http.StatusOK, gin.H{ "data": orders, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_pages": totalPages, }, }) } func (h *OrderHandler) ProcessRefund(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Preload("OrderItems").First(&order, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } var req schemas.ProcessRefundRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Status == "approved" { var setting models.SystemSetting feeRate := 0.0 if err := utils.DB.Where("`key` = ?", "payment_channel_fee_rate").First(&setting).Error; err == nil { feeRate, _ = strconv.ParseFloat(setting.Value, 64) } refundAmount := order.TotalAmount * (1 - feeRate/100) tx := utils.DB.Begin() tx.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusCompleted, "refund_amount": refundAmount, "status": models.OrderStatusRefunded, }) for _, item := range order.OrderItems { var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", product.CreditCost*item.Quantity)) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } } tx.Commit() } else { utils.DB.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusRejected, "status": models.OrderStatusPendingConfirm, }) } c.JSON(http.StatusOK, gin.H{"message": "Refund processed successfully"}) } func (h *OrderHandler) Export(c *gin.Context) { var orders []models.Order utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User"). Order("created_at DESC").Find(&orders) c.JSON(http.StatusOK, gin.H{"data": orders}) } func (h *OrderHandler) CancelOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingPayment && order.Status != models.OrderStatusPendingConfirm { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be cancelled"}) return } tx := utils.DB.Begin() tx.Model(&order).Update("status", models.OrderStatusCancelled) for _, item := range order.OrderItems { var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", product.CreditCost*item.Quantity)) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } } tx.Commit() c.JSON(http.StatusOK, gin.H{"message": "Order cancelled successfully"}) } func (h *OrderHandler) ConfirmReceipt(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusShipped { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"}) return } utils.DB.Model(&order).Update("status", models.OrderStatusCompleted) c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"}) }