Files
MyClub/internal/controllers/eshop/cart_controller.go
T
Tomas Dvorak dfc079288f hot fix #1
2026-01-26 08:13:18 +01:00

265 lines
7.5 KiB
Go

package eshop
import (
"net/http"
"fotbal-club/internal/config"
"fotbal-club/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type CartController struct {
DB *gorm.DB
Config *config.Config
}
func NewCartController(db *gorm.DB, cfg *config.Config) *CartController {
return &CartController{
DB: db,
Config: cfg,
}
}
type cartContext struct {
UserID *uint
SessionToken string
}
func (ctrl *CartController) getCartContext(c *gin.Context) cartContext {
var res cartContext
if uidVal, ok := c.Get("userID"); ok {
switch v := uidVal.(type) {
case uint:
res.UserID = &v
case int:
u := uint(v)
res.UserID = &u
case int64:
u := uint(v)
res.UserID = &u
}
}
res.SessionToken = c.GetHeader("X-Session-Token")
if res.SessionToken == "" {
if cookie, err := c.Request.Cookie("eshop_session_token"); err == nil {
res.SessionToken = cookie.Value
}
}
return res
}
func (ctrl *CartController) findOrCreateCart(c *gin.Context) (*models.EshopCart, error) {
cc := ctrl.getCartContext(c)
q := ctrl.DB.Where("completed = ?", false)
if cc.UserID != nil {
q = q.Where("user_id = ?", *cc.UserID)
} else if cc.SessionToken != "" {
q = q.Where("session_token = ?", cc.SessionToken)
}
var cart models.EshopCart
if err := q.Preload("Items").Preload("Items.Product").First(&cart).Error; err != nil {
if err != gorm.ErrRecordNotFound {
return nil, err
}
// Create new cart
cart = models.EshopCart{
UserID: cc.UserID,
SessionToken: cc.SessionToken,
Currency: ctrl.Config.StripeCurrency,
}
if cart.Currency == "" {
cart.Currency = "CZK"
}
if err := ctrl.DB.Create(&cart).Error; err != nil {
return nil, err
}
}
return &cart, nil
}
// GetCart returns the current cart
func (ctrl *CartController) GetCart(c *gin.Context) {
cartObj, err := ctrl.findOrCreateCart(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load cart"})
return
}
// Ensure items are fully loaded
if err := ctrl.DB.
Preload("Items").
Preload("Items.Product").
Preload("Items.Variant").
First(cartObj, cartObj.ID).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load cart items"})
return
}
c.JSON(http.StatusOK, cartObj)
}
// AddItem adds an item to the cart
func (ctrl *CartController) AddItem(c *gin.Context) {
var body struct {
ProductID uint `json:"product_id"`
VariantID *uint `json:"variant_id"`
Quantity int `json:"quantity"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format: " + err.Error()})
return
}
// Validate required fields
if body.ProductID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Product ID is required"})
return
}
if body.Quantity <= 0 || body.Quantity > 100 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Quantity must be between 1 and 100"})
return
}
// Check if product exists and is active
var product models.EshopProduct
if err := ctrl.DB.Where("id = ? AND active = ?", body.ProductID, true).First(&product).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Product not found or not available"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load product"})
}
return
}
// Validate variant if provided
if body.VariantID != nil {
var variant models.EshopProductVariant
if err := ctrl.DB.Where("id = ? AND product_id = ?", *body.VariantID, body.ProductID).First(&variant).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Product variant not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load variant"})
}
return
}
// Check stock (negative values mean unlimited)
if variant.StockQty >= 0 && variant.StockQty < body.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for this variant"})
return
}
}
cartObj, err := ctrl.findOrCreateCart(c)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load cart"})
return
}
// Upsert cart item
var item models.EshopCartItem
q := ctrl.DB.Where("cart_id = ? AND product_id = ?", cartObj.ID, body.ProductID)
if body.VariantID != nil {
q = q.Where("variant_id = ?", *body.VariantID)
}
if err := q.First(&item).Error; err != nil {
if err != gorm.ErrRecordNotFound {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cart"})
return
}
item = models.EshopCartItem{
CartID: cartObj.ID,
ProductID: body.ProductID,
VariantID: body.VariantID,
Quantity: body.Quantity,
UnitPriceCents: product.PriceCents,
Currency: product.Currency,
}
if item.Currency == "" {
item.Currency = cartObj.Currency
}
if err := ctrl.DB.Create(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add item"})
return
}
} else {
// Update quantity
item.Quantity += body.Quantity
if item.Quantity <= 0 {
if err := ctrl.DB.Delete(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cart"})
return
}
} else {
if err := ctrl.DB.Save(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cart"})
return
}
}
}
c.Status(http.StatusNoContent)
}
// UpdateItem updates quantity of a cart item
func (ctrl *CartController) UpdateItem(c *gin.Context) {
id := c.Param("id")
var body struct {
Quantity int `json:"quantity"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.Quantity < 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
return
}
var item models.EshopCartItem
if err := ctrl.DB.First(&item, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Cart item not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load cart item"})
return
}
// Check ownership (simplified) - in real app check if item belongs to user's cart
// Here we assume if they know the ID, they can edit it (or rely on middleware/session match in findOrCreateCart flow if we enforced it strictly)
// Ideally we should check if item.CartID belongs to current session/user.
// For MVP let's leave it as is, or add a check:
cc := ctrl.getCartContext(c)
var cart models.EshopCart
if err := ctrl.DB.First(&cart, item.CartID).Error; err == nil {
if cc.UserID != nil && (cart.UserID == nil || *cart.UserID != *cc.UserID) {
// user mismatch
c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"})
return
}
if cc.UserID == nil && cc.SessionToken != "" && cart.SessionToken != cc.SessionToken {
// token mismatch
c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"})
return
}
}
if body.Quantity == 0 {
if err := ctrl.DB.Delete(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cart"})
return
}
} else {
item.Quantity = body.Quantity
if err := ctrl.DB.Save(&item).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update cart"})
return
}
}
c.Status(http.StatusNoContent)
}
// RemoveItem removes an item from the cart
func (ctrl *CartController) RemoveItem(c *gin.Context) {
id := c.Param("id")
if err := ctrl.DB.Delete(&models.EshopCartItem{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to remove item"})
return
}
c.Status(http.StatusNoContent)
}