🎨 **True Light/Dark Mode** - Implemented proper light mode with inverted color scheme - Smooth theme transitions (0.3s ease) - Light mode colors: white backgrounds, dark text, vibrant accents - System theme detection with proper class application 🌈 **Enhanced Theme System** - 4 color themes work in both light and dark modes - Cyber (cyan/magenta), Ocean (blue/teal), Forest (green/lime), Sunset (orange/purple) - Theme-specific glassmorphism effects - Proper contrast in light mode ✨ **Premium Animations** - Float, glow-pulse, slide-in, scale-in, rotate-in animations - Smooth page transitions - Hover effects with depth (lift, glow, scale) - Micro-interactions on all interactive elements 🎯 **Visual Polish** - Enhanced glassmorphism for light/dark modes - Custom scrollbar with theme colors - Beautiful text selection - Focus indicators for accessibility - Premium utility classes 🔧 **Technical Improvements** - Updated UIStore to properly apply light/dark classes - Added data-theme attribute for CSS targeting - Smooth scroll behavior - Optimized transitions The app is now a visual masterpiece with perfect light/dark mode support!
430 lines
14 KiB
Go
430 lines
14 KiB
Go
package handlers
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"veza-backend-api/internal/core/marketplace"
|
|
"veza-backend-api/internal/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// MarketplaceHandler gère les opérations de la marketplace
|
|
type MarketplaceHandler struct {
|
|
service marketplace.MarketplaceService
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewMarketplaceHandler crée une nouvelle instance de MarketplaceHandler
|
|
func NewMarketplaceHandler(service marketplace.MarketplaceService, logger *zap.Logger) *MarketplaceHandler {
|
|
return &MarketplaceHandler{
|
|
service: service,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// CreateProductRequest DTO pour la création de produit
|
|
// GO-013: Validation améliorée avec tags go-validator
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type CreateProductRequest struct {
|
|
Title string `json:"title" binding:"required,min=3,max=200" validate:"required,min=3,max=200"`
|
|
Description string `json:"description" binding:"max=2000" validate:"omitempty,max=2000"`
|
|
Price float64 `json:"price" binding:"required,min=0,gt=0" validate:"required,min=0,gt=0"`
|
|
ProductType string `json:"product_type" binding:"required,oneof=track pack service" validate:"required,oneof=track pack service"`
|
|
TrackID string `json:"track_id,omitempty" binding:"omitempty,uuid" validate:"omitempty,uuid"` // UUID string
|
|
LicenseType string `json:"license_type,omitempty" binding:"omitempty,oneof=standard exclusive commercial" validate:"omitempty,oneof=standard exclusive commercial"`
|
|
}
|
|
|
|
// CreateProduct gère la création d'un produit
|
|
// @Summary Create a new product
|
|
// @Description Create a product (Track, Pack, Service) for sale
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param product body CreateProductRequest true "Product info"
|
|
// @Success 201 {object} marketplace.Product
|
|
// @Success 201 {object} marketplace.Product
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
|
// @Router /api/v1/marketplace/products [post]
|
|
func (h *MarketplaceHandler) CreateProduct(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
var req CreateProductRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
product := &marketplace.Product{
|
|
SellerID: userID,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Price: req.Price,
|
|
ProductType: req.ProductType,
|
|
LicenseType: marketplace.LicenseType(req.LicenseType),
|
|
Status: marketplace.ProductStatusActive, // Direct active for MVP
|
|
}
|
|
|
|
if req.TrackID != "" {
|
|
trackUUID, err := uuid.Parse(req.TrackID)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid track_id format")
|
|
return
|
|
}
|
|
product.TrackID = &trackUUID
|
|
}
|
|
|
|
if err := h.service.CreateProduct(c.Request.Context(), product); err != nil {
|
|
if err == marketplace.ErrInvalidSeller {
|
|
response.Forbidden(c, "You do not own this track")
|
|
return
|
|
}
|
|
if err == marketplace.ErrTrackNotFound {
|
|
response.NotFound(c, "Track not found")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "Failed to create product")
|
|
return
|
|
}
|
|
|
|
response.Created(c, product)
|
|
}
|
|
|
|
// CreateOrderRequest DTO pour la création de commande
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type CreateOrderRequest struct {
|
|
Items []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
} `json:"items" binding:"required,min=1" validate:"required,min=1"`
|
|
}
|
|
|
|
// CreateOrder gère l'achat de produits
|
|
// @Summary Create a new order
|
|
// @Description Purchase products
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param order body CreateOrderRequest true "Order items"
|
|
// @Success 201 {object} marketplace.Order
|
|
// @Success 201 {object} marketplace.Order
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
|
// @Router /api/v1/marketplace/orders [post]
|
|
func (h *MarketplaceHandler) CreateOrder(c *gin.Context) {
|
|
buyerID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
var req CreateOrderRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
var items []marketplace.NewOrderItem
|
|
for _, item := range req.Items {
|
|
pid, err := uuid.Parse(item.ProductID)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid product_id: "+item.ProductID)
|
|
return
|
|
}
|
|
items = append(items, marketplace.NewOrderItem{ProductID: pid})
|
|
}
|
|
|
|
order, err := h.service.CreateOrder(c.Request.Context(), buyerID, items)
|
|
if err != nil {
|
|
// MOD-P1-004: Détecter les erreurs de validation client et retourner 400 au lieu de 500
|
|
errStr := err.Error()
|
|
if strings.Contains(errStr, "not found") || strings.Contains(errStr, "not active") {
|
|
// Erreurs de validation client (produit non trouvé, produit non actif)
|
|
response.BadRequest(c, err.Error())
|
|
return
|
|
}
|
|
// Erreurs serveur (DB, IO, etc.) → 500
|
|
response.InternalServerError(c, "Failed to create order")
|
|
return
|
|
}
|
|
|
|
response.Created(c, order)
|
|
}
|
|
|
|
// GetDownloadURL récupère l'URL de téléchargement pour un achat
|
|
// @Summary Get download URL
|
|
// @Description Get a secure download URL for a purchased product
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param product_id path string true "Product ID"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 403 {object} response.APIResponse "No license"
|
|
// @Failure 404 {object} response.APIResponse "Not Found"
|
|
// @Router /api/v1/marketplace/download/{product_id} [get]
|
|
func (h *MarketplaceHandler) GetDownloadURL(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
productIDStr := c.Param("product_id")
|
|
|
|
productID, err := uuid.Parse(productIDStr)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid product_id")
|
|
return
|
|
}
|
|
|
|
url, err := h.service.GetDownloadURL(c.Request.Context(), userID, productID)
|
|
if err != nil {
|
|
if err == marketplace.ErrNoLicense {
|
|
response.Forbidden(c, "No valid license for this product")
|
|
return
|
|
}
|
|
if err == marketplace.ErrTrackNotFound {
|
|
response.NotFound(c, "Track file not found")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "Failed to get download URL")
|
|
return
|
|
}
|
|
|
|
response.Success(c, gin.H{"url": url})
|
|
}
|
|
|
|
// ListProducts liste les produits
|
|
// @Summary List products
|
|
// @Description List marketplace products with filters
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param status query string false "Product status"
|
|
// @Param seller_id query string false "Seller ID"
|
|
// @Param q query string false "Search query"
|
|
// @Param type query string false "Product type (track, pack, service)"
|
|
// @Param min_price query number false "Minimum price"
|
|
// @Param max_price query number false "Maximum price"
|
|
// @Param page query int false "Page number"
|
|
// @Param limit query int false "Items per page"
|
|
// @Success 200 {array} marketplace.Product
|
|
// @Router /api/v1/marketplace/products [get]
|
|
func (h *MarketplaceHandler) ListProducts(c *gin.Context) {
|
|
filters := make(map[string]interface{})
|
|
|
|
if status := c.Query("status"); status != "" {
|
|
filters["status"] = status
|
|
}
|
|
if sellerID := c.Query("seller_id"); sellerID != "" {
|
|
filters["seller_id"] = sellerID
|
|
}
|
|
if q := c.Query("q"); q != "" {
|
|
filters["search"] = q
|
|
}
|
|
if pType := c.Query("type"); pType != "" {
|
|
filters["product_type"] = pType
|
|
}
|
|
|
|
if minPriceStr := c.Query("min_price"); minPriceStr != "" {
|
|
if val, err := strconv.ParseFloat(minPriceStr, 64); err == nil {
|
|
filters["min_price"] = val
|
|
}
|
|
}
|
|
if maxPriceStr := c.Query("max_price"); maxPriceStr != "" {
|
|
if val, err := strconv.ParseFloat(maxPriceStr, 64); err == nil {
|
|
filters["max_price"] = val
|
|
}
|
|
}
|
|
|
|
if pageStr := c.Query("page"); pageStr != "" {
|
|
if val, err := strconv.Atoi(pageStr); err == nil && val > 0 {
|
|
// Calculate offset assuming default limit if not provided
|
|
// But limit parsing comes next, so let's store page and conform later or just pass page?
|
|
// Service expects offset. Let's wait for limit.
|
|
filters["page_number"] = val // Temporary storage
|
|
}
|
|
}
|
|
|
|
limit := 20
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
if val, err := strconv.Atoi(limitStr); err == nil && val > 0 {
|
|
limit = val
|
|
filters["limit"] = val
|
|
}
|
|
}
|
|
|
|
// Handle pagination offset
|
|
if pageVal, ok := filters["page_number"].(int); ok {
|
|
filters["offset"] = (pageVal - 1) * limit
|
|
delete(filters, "page_number") // cleanup
|
|
} else if offsetStr := c.Query("offset"); offsetStr != "" {
|
|
if val, err := strconv.Atoi(offsetStr); err == nil {
|
|
filters["offset"] = val
|
|
}
|
|
}
|
|
|
|
products, err := h.service.ListProducts(c.Request.Context(), filters)
|
|
if err != nil {
|
|
response.InternalServerError(c, "Failed to list products")
|
|
return
|
|
}
|
|
|
|
response.Success(c, products)
|
|
}
|
|
|
|
// UpdateProductRequest DTO pour la mise à jour de produit
|
|
// BE-API-037: Implement marketplace product update endpoint
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
|
type UpdateProductRequest struct {
|
|
Title *string `json:"title,omitempty" binding:"omitempty,min=3,max=200" validate:"omitempty,min=3,max=200"`
|
|
Description *string `json:"description,omitempty" binding:"omitempty,max=2000" validate:"omitempty,max=2000"`
|
|
Price *float64 `json:"price,omitempty" binding:"omitempty,min=0,gt=0" validate:"omitempty,min=0,gt=0"`
|
|
Status *string `json:"status,omitempty" binding:"omitempty,oneof=draft active archived" validate:"omitempty,oneof=draft active archived"`
|
|
}
|
|
|
|
// UpdateProduct gère la mise à jour d'un produit
|
|
// BE-API-037: PUT /api/v1/marketplace/products/:id to update product details
|
|
// @Summary Update a product
|
|
// @Description Update product details (only seller can update)
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Product ID"
|
|
// @Param product body UpdateProductRequest true "Product updates"
|
|
// @Success 200 {object} marketplace.Product
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
|
// @Failure 403 {object} response.APIResponse "Forbidden - Not product owner"
|
|
// @Failure 404 {object} response.APIResponse "Product not found"
|
|
// @Router /api/v1/marketplace/products/{id} [put]
|
|
func (h *MarketplaceHandler) UpdateProduct(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
productIDStr := c.Param("id")
|
|
productID, err := uuid.Parse(productIDStr)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid product id")
|
|
return
|
|
}
|
|
|
|
var req UpdateProductRequest
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
// Build updates map
|
|
updates := make(map[string]interface{})
|
|
if req.Title != nil {
|
|
updates["title"] = *req.Title
|
|
}
|
|
if req.Description != nil {
|
|
updates["description"] = *req.Description
|
|
}
|
|
if req.Price != nil {
|
|
updates["price"] = *req.Price
|
|
}
|
|
if req.Status != nil {
|
|
updates["status"] = *req.Status
|
|
}
|
|
|
|
if len(updates) == 0 {
|
|
response.BadRequest(c, "No fields to update")
|
|
return
|
|
}
|
|
|
|
product, err := h.service.UpdateProduct(c.Request.Context(), productID, userID, updates)
|
|
if err != nil {
|
|
if err == marketplace.ErrProductNotFound {
|
|
response.NotFound(c, "Product not found")
|
|
return
|
|
}
|
|
if err == marketplace.ErrInvalidSeller {
|
|
response.Forbidden(c, "You do not own this product")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "Failed to update product")
|
|
return
|
|
}
|
|
|
|
response.Success(c, product)
|
|
}
|
|
|
|
// ListOrders gère la récupération de la liste des commandes de l'utilisateur
|
|
// BE-API-038: GET /api/v1/marketplace/orders to list user's orders
|
|
// @Summary List user orders
|
|
// @Description Get all orders for the authenticated user
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {array} marketplace.Order
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
|
// @Failure 500 {object} response.APIResponse "Internal Error"
|
|
// @Router /api/v1/marketplace/orders [get]
|
|
func (h *MarketplaceHandler) ListOrders(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
orders, err := h.service.ListOrders(c.Request.Context(), userID)
|
|
if err != nil {
|
|
response.InternalServerError(c, "Failed to list orders")
|
|
return
|
|
}
|
|
|
|
response.Success(c, orders)
|
|
}
|
|
|
|
// GetOrder gère la récupération des détails d'une commande
|
|
// BE-API-039: GET /api/v1/marketplace/orders/:id to get order details
|
|
// @Summary Get order details
|
|
// @Description Get details of a specific order (only order owner can access)
|
|
// @Tags Marketplace
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Order ID"
|
|
// @Success 200 {object} marketplace.Order
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
|
// @Failure 403 {object} response.APIResponse "Forbidden - Not order owner"
|
|
// @Failure 404 {object} response.APIResponse "Order not found"
|
|
// @Router /api/v1/marketplace/orders/{id} [get]
|
|
func (h *MarketplaceHandler) GetOrder(c *gin.Context) {
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
orderIDStr := c.Param("id")
|
|
orderID, err := uuid.Parse(orderIDStr)
|
|
if err != nil {
|
|
response.BadRequest(c, "Invalid order id")
|
|
return
|
|
}
|
|
|
|
order, err := h.service.GetOrder(c.Request.Context(), orderID, userID)
|
|
if err != nil {
|
|
if err == marketplace.ErrOrderNotFound {
|
|
response.NotFound(c, "Order not found")
|
|
return
|
|
}
|
|
response.InternalServerError(c, "Failed to get order")
|
|
return
|
|
}
|
|
|
|
response.Success(c, order)
|
|
}
|