2025-12-03 19:29:37 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
2026-02-22 15:06:18 +00:00
|
|
|
"errors"
|
|
|
|
|
"net/http"
|
2026-02-22 13:07:30 +00:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
"strconv"
|
2025-12-16 18:34:08 +00:00
|
|
|
"strings"
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
"veza-backend-api/internal/core/marketplace"
|
|
|
|
|
"veza-backend-api/internal/response"
|
2026-02-22 13:14:27 +00:00
|
|
|
"veza-backend-api/internal/utils"
|
2025-12-16 16:23:49 +00:00
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"github.com/google/uuid"
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
"go.uber.org/zap"
|
2025-12-03 19:29:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MarketplaceHandler gère les opérations de la marketplace
|
|
|
|
|
type MarketplaceHandler struct {
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
service marketplace.MarketplaceService
|
|
|
|
|
commonHandler *CommonHandler
|
2026-02-22 13:07:30 +00:00
|
|
|
uploadDir string
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewMarketplaceHandler crée une nouvelle instance de MarketplaceHandler
|
2026-02-22 13:07:30 +00:00
|
|
|
func NewMarketplaceHandler(service marketplace.MarketplaceService, logger *zap.Logger, uploadDir string) *MarketplaceHandler {
|
|
|
|
|
if uploadDir == "" {
|
|
|
|
|
uploadDir = "uploads"
|
|
|
|
|
}
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
return &MarketplaceHandler{
|
|
|
|
|
service: service,
|
|
|
|
|
commonHandler: NewCommonHandler(logger),
|
2026-02-22 13:07:30 +00:00
|
|
|
uploadDir: uploadDir,
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateProductRequest DTO pour la création de produit
|
|
|
|
|
// GO-013: Validation améliorée avec tags go-validator
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
2026-02-22 13:06:20 +00:00
|
|
|
// v0.401 M1: BPM, MusicalKey, Category
|
2025-12-03 19:29:37 +00:00
|
|
|
type CreateProductRequest struct {
|
2025-12-16 18:34:08 +00:00
|
|
|
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"`
|
2026-02-22 13:06:20 +00:00
|
|
|
// v0.401 M1
|
|
|
|
|
BPM *int `json:"bpm,omitempty" binding:"omitempty,min=1,max=300" validate:"omitempty,min=1,max=300"`
|
|
|
|
|
MusicalKey string `json:"musical_key,omitempty" binding:"omitempty,max=10" validate:"omitempty,max=10"`
|
|
|
|
|
Category string `json:"category,omitempty" binding:"omitempty,oneof=sample beat preset pack" validate:"omitempty,oneof=sample beat preset pack"`
|
2026-02-22 13:16:24 +00:00
|
|
|
// v0.401 M2: Product licenses (streaming, personal, commercial, exclusive)
|
|
|
|
|
Licenses []struct {
|
|
|
|
|
LicenseType string `json:"license_type" binding:"required,oneof=streaming personal commercial exclusive"`
|
|
|
|
|
PriceCents int `json:"price_cents" binding:"required,min=0"`
|
|
|
|
|
TermsText string `json:"terms_text,omitempty"`
|
|
|
|
|
} `json:"licenses,omitempty"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-12-06 16:39:04 +00:00
|
|
|
// @Success 201 {object} marketplace.Product
|
|
|
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
|
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
2025-12-03 19:29:37 +00:00
|
|
|
// @Router /api/v1/marketplace/products [post]
|
|
|
|
|
func (h *MarketplaceHandler) CreateProduct(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
var req CreateProductRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
product := &marketplace.Product{
|
|
|
|
|
SellerID: userID,
|
|
|
|
|
Title: req.Title,
|
2026-02-22 13:14:27 +00:00
|
|
|
Description: utils.SanitizeHTML(req.Description, 5000),
|
2025-12-03 19:29:37 +00:00
|
|
|
Price: req.Price,
|
|
|
|
|
ProductType: req.ProductType,
|
|
|
|
|
LicenseType: marketplace.LicenseType(req.LicenseType),
|
|
|
|
|
Status: marketplace.ProductStatusActive, // Direct active for MVP
|
2026-02-22 13:06:20 +00:00
|
|
|
BPM: req.BPM,
|
|
|
|
|
MusicalKey: req.MusicalKey,
|
|
|
|
|
Category: req.Category,
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if req.TrackID != "" {
|
|
|
|
|
trackUUID, err := uuid.Parse(req.TrackID)
|
|
|
|
|
if err != nil {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.BadRequest(c, "Invalid track_id format")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
product.TrackID = &trackUUID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := h.service.CreateProduct(c.Request.Context(), product); err != nil {
|
|
|
|
|
if err == marketplace.ErrInvalidSeller {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.Forbidden(c, "You do not own this track")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrTrackNotFound {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.NotFound(c, "Track not found")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:39:04 +00:00
|
|
|
response.InternalServerError(c, "Failed to create product")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:16:24 +00:00
|
|
|
// v0.401 M2: Create product licenses if provided
|
|
|
|
|
if len(req.Licenses) > 0 {
|
|
|
|
|
licInputs := make([]marketplace.ProductLicenseInput, 0, len(req.Licenses))
|
|
|
|
|
for _, l := range req.Licenses {
|
|
|
|
|
licInputs = append(licInputs, marketplace.ProductLicenseInput{
|
|
|
|
|
LicenseType: l.LicenseType,
|
|
|
|
|
PriceCents: l.PriceCents,
|
|
|
|
|
TermsText: l.TermsText,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if _, err := h.service.SetProductLicenses(c.Request.Context(), product.ID, userID, licInputs); err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to create product licenses")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reload product with licenses for response
|
|
|
|
|
if p, err := h.service.GetProduct(c.Request.Context(), product.ID); err == nil {
|
|
|
|
|
product = p
|
|
|
|
|
}
|
2025-12-06 16:39:04 +00:00
|
|
|
response.Created(c, product)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateOrderRequest DTO pour la création de commande
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-001: Ajout tags validate pour validation systématique
|
2026-02-22 15:06:18 +00:00
|
|
|
// v0.402 P2: promo_code optionnel
|
2025-12-03 19:29:37 +00:00
|
|
|
type CreateOrderRequest struct {
|
|
|
|
|
Items []struct {
|
2025-12-16 18:34:08 +00:00
|
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
|
|
|
} `json:"items" binding:"required,min=1" validate:"required,min=1"`
|
2026-02-22 15:06:18 +00:00
|
|
|
PromoCode string `json:"promo_code,omitempty" binding:"omitempty,max=50" validate:"omitempty,max=50"`
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-12-06 16:39:04 +00:00
|
|
|
// @Success 201 {object} marketplace.Order
|
|
|
|
|
// @Failure 400 {object} response.APIResponse "Validation Error"
|
|
|
|
|
// @Failure 401 {object} response.APIResponse "Unauthorized"
|
2025-12-03 19:29:37 +00:00
|
|
|
// @Router /api/v1/marketplace/orders [post]
|
|
|
|
|
func (h *MarketplaceHandler) CreateOrder(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
buyerID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
var req CreateOrderRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var items []marketplace.NewOrderItem
|
|
|
|
|
for _, item := range req.Items {
|
|
|
|
|
pid, err := uuid.Parse(item.ProductID)
|
|
|
|
|
if err != nil {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.BadRequest(c, "Invalid product_id: "+item.ProductID)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
items = append(items, marketplace.NewOrderItem{ProductID: pid})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:06:18 +00:00
|
|
|
promoCode := strings.TrimSpace(req.PromoCode)
|
|
|
|
|
resp, err := h.service.CreateOrder(c.Request.Context(), buyerID, items, promoCode)
|
2025-12-03 19:29:37 +00:00
|
|
|
if err != nil {
|
2025-12-16 18:34:08 +00:00
|
|
|
// MOD-P1-004: Détecter les erreurs de validation client et retourner 400 au lieu de 500
|
2026-02-22 15:06:18 +00:00
|
|
|
if errors.Is(err, marketplace.ErrPromoCodeInvalid) {
|
|
|
|
|
response.BadRequest(c, "Invalid or expired promo code")
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 18:34:08 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-02-14 20:45:15 +00:00
|
|
|
if strings.Contains(errStr, "payment creation failed") {
|
|
|
|
|
response.BadRequest(c, "Payment service unavailable")
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 18:34:08 +00:00
|
|
|
// Erreurs serveur (DB, IO, etc.) → 500
|
|
|
|
|
response.InternalServerError(c, "Failed to create order")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 20:45:15 +00:00
|
|
|
response.Created(c, resp)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-12-06 16:39:04 +00:00
|
|
|
// @Failure 403 {object} response.APIResponse "No license"
|
|
|
|
|
// @Failure 404 {object} response.APIResponse "Not Found"
|
2025-12-03 19:29:37 +00:00
|
|
|
// @Router /api/v1/marketplace/download/{product_id} [get]
|
|
|
|
|
func (h *MarketplaceHandler) GetDownloadURL(c *gin.Context) {
|
2025-12-16 16:23:49 +00:00
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
productIDStr := c.Param("product_id")
|
2025-12-06 16:21:59 +00:00
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
productID, err := uuid.Parse(productIDStr)
|
|
|
|
|
if err != nil {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.BadRequest(c, "Invalid product_id")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url, err := h.service.GetDownloadURL(c.Request.Context(), userID, productID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == marketplace.ErrNoLicense {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.Forbidden(c, "No valid license for this product")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrTrackNotFound {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.NotFound(c, "Track file not found")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-06 16:39:04 +00:00
|
|
|
response.InternalServerError(c, "Failed to get download URL")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:39:04 +00:00
|
|
|
response.Success(c, gin.H{"url": url})
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:07:30 +00:00
|
|
|
// UploadProductPreview handles audio preview upload for a product (v0.401 M1)
|
|
|
|
|
// POST /marketplace/products/:id/preview
|
|
|
|
|
func (h *MarketplaceHandler) UploadProductPreview(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
file, err := c.FormFile("file")
|
|
|
|
|
if err != nil || file == nil {
|
|
|
|
|
response.BadRequest(c, "Missing or invalid file")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-13 23:44:46 +00:00
|
|
|
// SECURITY(REM-005): Validate file size (max 50MB for audio previews)
|
|
|
|
|
const maxPreviewSize = 50 << 20 // 50MB
|
|
|
|
|
if file.Size > maxPreviewSize {
|
|
|
|
|
response.BadRequest(c, "File too large. Maximum 50MB")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-22 13:07:30 +00:00
|
|
|
ext := strings.ToLower(filepath.Ext(file.Filename))
|
|
|
|
|
if ext != ".mp3" && ext != ".wav" && ext != ".m4a" && ext != ".ogg" {
|
|
|
|
|
response.BadRequest(c, "Invalid file type. Allowed: mp3, wav, m4a, ogg")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-13 23:44:46 +00:00
|
|
|
// SECURITY(REM-005): Sanitize filename to prevent path traversal — use UUID instead of user-supplied name.
|
|
|
|
|
safeFilename := uuid.New().String() + ext
|
2026-02-22 13:07:30 +00:00
|
|
|
previewDir := filepath.Join(h.uploadDir, "products", "previews", productID.String())
|
|
|
|
|
if err := os.MkdirAll(previewDir, 0755); err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to create preview directory")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-13 23:44:46 +00:00
|
|
|
destPath := filepath.Join(previewDir, safeFilename)
|
2026-02-22 13:07:30 +00:00
|
|
|
if err := c.SaveUploadedFile(file, destPath); err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to save preview")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-13 23:44:46 +00:00
|
|
|
relativePath := "products/previews/" + productID.String() + "/" + safeFilename
|
2026-02-22 13:07:30 +00:00
|
|
|
preview, err := h.service.AddProductPreview(c.Request.Context(), productID, userID, relativePath, nil)
|
|
|
|
|
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 add preview")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Created(c, preview)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:06:18 +00:00
|
|
|
// CreateReviewRequest body for POST /products/:id/reviews (v0.403 R1)
|
|
|
|
|
type CreateReviewRequest struct {
|
|
|
|
|
Rating int `json:"rating" binding:"required,min=1,max=5" validate:"required,min=1,max=5"`
|
|
|
|
|
Comment string `json:"comment" binding:"max=2000" validate:"omitempty,max=2000"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateReview creates a review for a product (v0.403 R1). Buyer must have purchased the product.
|
|
|
|
|
func (h *MarketplaceHandler) CreateReview(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var req CreateReviewRequest
|
|
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
review, err := h.service.CreateReview(c.Request.Context(), productID, userID, req.Rating, req.Comment)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == marketplace.ErrProductNotFound {
|
|
|
|
|
response.NotFound(c, "Product not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrReviewNotPurchased {
|
|
|
|
|
response.Forbidden(c, "You must purchase the product before reviewing")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrReviewAlreadyExists {
|
|
|
|
|
response.Error(c, http.StatusConflict, "You have already reviewed this product")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.InternalServerError(c, "Failed to create review")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Created(c, review)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListReviews returns paginated reviews for a product (v0.403 R1)
|
|
|
|
|
func (h *MarketplaceHandler) ListReviews(c *gin.Context) {
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
2026-03-12 05:13:38 +00:00
|
|
|
limit = clampLimit(limit) // SECURITY(MEDIUM-004)
|
2026-02-22 15:06:18 +00:00
|
|
|
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
|
|
|
|
reviews, err := h.service.ListReviews(c.Request.Context(), productID, limit, offset)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to list reviews")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"reviews": reviews})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:14:27 +00:00
|
|
|
// GetProduct returns a single product by ID (v0.401 M1 - includes previews and images)
|
|
|
|
|
// GET /marketplace/products/:id
|
|
|
|
|
func (h *MarketplaceHandler) GetProduct(c *gin.Context) {
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
product, err := h.service.GetProduct(c.Request.Context(), productID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == marketplace.ErrProductNotFound {
|
|
|
|
|
response.NotFound(c, "Product not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.InternalServerError(c, "Failed to get product")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, product)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StreamProductPreview streams the first audio preview for a product (v0.401 M1)
|
|
|
|
|
// GET /marketplace/products/:id/preview
|
|
|
|
|
func (h *MarketplaceHandler) StreamProductPreview(c *gin.Context) {
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
product, err := h.service.GetProduct(c.Request.Context(), productID)
|
|
|
|
|
if err != nil || len(product.Previews) == 0 {
|
|
|
|
|
response.NotFound(c, "Preview not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
preview := product.Previews[0]
|
|
|
|
|
fullPath := filepath.Join(h.uploadDir, preview.FilePath)
|
|
|
|
|
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
|
|
|
|
response.NotFound(c, "Preview file not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Header("Content-Disposition", "inline")
|
|
|
|
|
c.File(fullPath)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:08:13 +00:00
|
|
|
// UpdateProductImagesRequest body for PUT /products/:id/images
|
|
|
|
|
type UpdateProductImagesRequest struct {
|
|
|
|
|
Images []struct {
|
|
|
|
|
URL string `json:"url" binding:"required" validate:"required,max=512"`
|
|
|
|
|
SortOrder int `json:"sort_order"`
|
|
|
|
|
} `json:"images" binding:"required" validate:"required"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateProductImages updates product images (v0.401 M1)
|
|
|
|
|
// PUT /marketplace/products/:id/images
|
|
|
|
|
func (h *MarketplaceHandler) UpdateProductImages(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
productID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid product id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var req UpdateProductImagesRequest
|
|
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
images := make([]marketplace.ProductImageInput, len(req.Images))
|
|
|
|
|
for i, img := range req.Images {
|
|
|
|
|
images[i] = marketplace.ProductImageInput{URL: img.URL, SortOrder: img.SortOrder}
|
|
|
|
|
}
|
|
|
|
|
result, err := h.service.UpdateProductImages(c.Request.Context(), productID, userID, images)
|
|
|
|
|
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 images")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, result)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// @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"
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
// @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"
|
2025-12-03 19:29:37 +00:00
|
|
|
// @Success 200 {array} marketplace.Product
|
|
|
|
|
// @Router /api/v1/marketplace/products [get]
|
|
|
|
|
func (h *MarketplaceHandler) ListProducts(c *gin.Context) {
|
|
|
|
|
filters := make(map[string]interface{})
|
2025-12-06 16:21:59 +00:00
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
if status := c.Query("status"); status != "" {
|
|
|
|
|
filters["status"] = status
|
|
|
|
|
}
|
|
|
|
|
if sellerID := c.Query("seller_id"); sellerID != "" {
|
2026-02-20 16:02:54 +00:00
|
|
|
if sellerID == "me" {
|
|
|
|
|
uid, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
response.Unauthorized(c, "Authentication required to filter by own products")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
filters["seller_id"] = uid.String()
|
|
|
|
|
} else {
|
|
|
|
|
filters["seller_id"] = sellerID
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
if q := c.Query("q"); q != "" {
|
|
|
|
|
filters["search"] = q
|
|
|
|
|
}
|
|
|
|
|
if pType := c.Query("type"); pType != "" {
|
|
|
|
|
filters["product_type"] = pType
|
|
|
|
|
}
|
2026-02-22 13:08:41 +00:00
|
|
|
if bpmStr := c.Query("bpm"); bpmStr != "" {
|
|
|
|
|
if val, err := strconv.Atoi(bpmStr); err == nil && val > 0 {
|
|
|
|
|
filters["bpm"] = val
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if musicalKey := c.Query("musical_key"); musicalKey != "" {
|
|
|
|
|
filters["musical_key"] = musicalKey
|
|
|
|
|
}
|
|
|
|
|
if category := c.Query("category"); category != "" {
|
|
|
|
|
filters["category"] = category
|
|
|
|
|
}
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
products, err := h.service.ListProducts(c.Request.Context(), filters)
|
|
|
|
|
if err != nil {
|
2025-12-06 16:39:04 +00:00
|
|
|
response.InternalServerError(c, "Failed to list products")
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:39:04 +00:00
|
|
|
response.Success(c, products)
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
2025-12-24 13:49:41 +00:00
|
|
|
|
|
|
|
|
// 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
|
2026-02-22 13:06:20 +00:00
|
|
|
// v0.401 M1: BPM, MusicalKey, Category
|
2025-12-24 13:49:41 +00:00
|
|
|
type UpdateProductRequest struct {
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
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"`
|
2025-12-24 13:49:41 +00:00
|
|
|
Price *float64 `json:"price,omitempty" binding:"omitempty,min=0,gt=0" validate:"omitempty,min=0,gt=0"`
|
feat: Visual masterpiece - true light mode & premium UI
🎨 **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!
2026-01-11 01:32:21 +00:00
|
|
|
Status *string `json:"status,omitempty" binding:"omitempty,oneof=draft active archived" validate:"omitempty,oneof=draft active archived"`
|
2026-02-22 13:06:20 +00:00
|
|
|
BPM *int `json:"bpm,omitempty" binding:"omitempty,min=1,max=300" validate:"omitempty,min=1,max=300"`
|
|
|
|
|
MusicalKey *string `json:"musical_key,omitempty" binding:"omitempty,max=10" validate:"omitempty,max=10"`
|
|
|
|
|
Category *string `json:"category,omitempty" binding:"omitempty,oneof=sample beat preset pack" validate:"omitempty,oneof=sample beat preset pack"`
|
2026-02-22 13:16:24 +00:00
|
|
|
// v0.401 M2: Product licenses
|
|
|
|
|
Licenses *[]struct {
|
|
|
|
|
LicenseType string `json:"license_type" binding:"required,oneof=streaming personal commercial exclusive"`
|
|
|
|
|
PriceCents int `json:"price_cents" binding:"required,min=0"`
|
|
|
|
|
TermsText string `json:"terms_text,omitempty"`
|
|
|
|
|
} `json:"licenses,omitempty"`
|
2025-12-24 13:49:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-02-22 13:14:27 +00:00
|
|
|
updates["description"] = utils.SanitizeHTML(*req.Description, 5000)
|
2025-12-24 13:49:41 +00:00
|
|
|
}
|
|
|
|
|
if req.Price != nil {
|
|
|
|
|
updates["price"] = *req.Price
|
|
|
|
|
}
|
|
|
|
|
if req.Status != nil {
|
|
|
|
|
updates["status"] = *req.Status
|
|
|
|
|
}
|
2026-02-22 13:06:20 +00:00
|
|
|
if req.BPM != nil {
|
|
|
|
|
updates["bpm"] = *req.BPM
|
|
|
|
|
}
|
|
|
|
|
if req.MusicalKey != nil {
|
|
|
|
|
updates["musical_key"] = *req.MusicalKey
|
|
|
|
|
}
|
|
|
|
|
if req.Category != nil {
|
|
|
|
|
updates["category"] = *req.Category
|
|
|
|
|
}
|
2025-12-24 13:49:41 +00:00
|
|
|
|
2026-02-22 13:16:24 +00:00
|
|
|
// v0.401 M2: Update product licenses if provided
|
|
|
|
|
if req.Licenses != nil {
|
|
|
|
|
licInputs := make([]marketplace.ProductLicenseInput, 0, len(*req.Licenses))
|
|
|
|
|
for _, l := range *req.Licenses {
|
|
|
|
|
licInputs = append(licInputs, marketplace.ProductLicenseInput{
|
|
|
|
|
LicenseType: l.LicenseType,
|
|
|
|
|
PriceCents: l.PriceCents,
|
|
|
|
|
TermsText: l.TermsText,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if _, err := h.service.SetProductLicenses(c.Request.Context(), productID, userID, licInputs); err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to update product licenses")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(updates) == 0 && req.Licenses == nil {
|
2025-12-24 13:49:41 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:16:24 +00:00
|
|
|
// Reload with licenses when they were updated
|
|
|
|
|
if req.Licenses != nil {
|
|
|
|
|
if p, err := h.service.GetProduct(c.Request.Context(), productID); err == nil {
|
|
|
|
|
product = p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-24 13:49:41 +00:00
|
|
|
response.Success(c, product)
|
|
|
|
|
}
|
2025-12-24 13:50:39 +00:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2025-12-24 14:00:32 +00:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-02-20 16:02:13 +00:00
|
|
|
|
2026-02-22 15:18:01 +00:00
|
|
|
// RefundOrderRequest body for POST /orders/:id/refund (v0.403 R2)
|
|
|
|
|
type RefundOrderRequest struct {
|
|
|
|
|
Reason string `json:"reason" binding:"max=500" validate:"omitempty,max=500"`
|
|
|
|
|
Details string `json:"details" binding:"max=2000" validate:"omitempty,max=2000"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RefundOrder initiates a refund for an order (v0.403 R2). Allowed: buyer, seller of products, or admin.
|
|
|
|
|
func (h *MarketplaceHandler) RefundOrder(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
orderID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid order id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var req RefundOrderRequest
|
|
|
|
|
_ = c.ShouldBindJSON(&req)
|
|
|
|
|
reason := req.Reason
|
|
|
|
|
if reason == "" {
|
|
|
|
|
reason = "Requested by customer"
|
|
|
|
|
}
|
|
|
|
|
if err := h.service.RefundOrder(c.Request.Context(), orderID, userID, reason); err != nil {
|
|
|
|
|
if err == marketplace.ErrOrderNotFound {
|
|
|
|
|
response.NotFound(c, "Order not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrOrderNotRefundable {
|
|
|
|
|
response.BadRequest(c, "Order cannot be refunded")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrRefundNotAvailable {
|
|
|
|
|
response.BadRequest(c, "Refunds are not available")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err == marketplace.ErrRefundForbidden {
|
|
|
|
|
response.Forbidden(c, "You are not allowed to refund this order")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.InternalServerError(c, "Failed to process refund")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"message": "Refund processed"})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 15:11:42 +00:00
|
|
|
// GetOrderInvoice returns a PDF invoice for an order (v0.403 F1)
|
|
|
|
|
func (h *MarketplaceHandler) GetOrderInvoice(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
orderID, err := uuid.Parse(c.Param("id"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.BadRequest(c, "Invalid order id")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
pdf, err := h.service.GenerateInvoice(c.Request.Context(), orderID, userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == marketplace.ErrOrderNotFound {
|
|
|
|
|
response.NotFound(c, "Order not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.InternalServerError(c, "Failed to generate invoice")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.Header("Content-Type", "application/pdf")
|
|
|
|
|
c.Header("Content-Disposition", "attachment; filename=\"invoice-"+orderID.String()[:8]+".pdf\"")
|
|
|
|
|
c.Data(200, "application/pdf", pdf)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 13:16:24 +00:00
|
|
|
// GetMyLicenses returns all licenses purchased by the authenticated user (v0.401 M2)
|
|
|
|
|
func (h *MarketplaceHandler) GetMyLicenses(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
licenses, err := h.service.GetUserLicenses(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to fetch licenses")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
items := make([]gin.H, 0, len(licenses))
|
|
|
|
|
for _, lic := range licenses {
|
|
|
|
|
product, _ := h.service.GetProduct(c.Request.Context(), lic.ProductID)
|
|
|
|
|
order, _ := h.service.GetOrder(c.Request.Context(), lic.OrderID, userID)
|
|
|
|
|
downloadURL := ""
|
|
|
|
|
if url, err := h.service.GetDownloadURL(c.Request.Context(), userID, lic.ProductID); err == nil {
|
|
|
|
|
downloadURL = url
|
|
|
|
|
}
|
|
|
|
|
item := gin.H{
|
|
|
|
|
"license": lic,
|
2026-03-05 22:03:43 +00:00
|
|
|
"product": product,
|
|
|
|
|
"order": order,
|
2026-02-22 13:16:24 +00:00
|
|
|
"download_url": downloadURL,
|
|
|
|
|
}
|
|
|
|
|
items = append(items, item)
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"licenses": items})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 16:02:13 +00:00
|
|
|
// GetSellStats returns seller stats (revenue, sales count) for the authenticated user
|
|
|
|
|
func (h *MarketplaceHandler) GetSellStats(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
revenue, salesCount, err := h.service.GetSellerStats(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to fetch seller stats")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{
|
|
|
|
|
"revenue": revenue,
|
|
|
|
|
"sales": salesCount,
|
|
|
|
|
"sales_count": salesCount,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-22 13:21:21 +00:00
|
|
|
|
|
|
|
|
// GetSellStatsEvolution returns revenue/sales evolution by period (v0.401 M3)
|
|
|
|
|
func (h *MarketplaceHandler) GetSellStatsEvolution(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
period := c.DefaultQuery("period", "day")
|
|
|
|
|
if period != "day" && period != "week" && period != "month" {
|
|
|
|
|
period = "day"
|
|
|
|
|
}
|
|
|
|
|
data, err := h.service.GetSellerStatsEvolution(c.Request.Context(), userID, period)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to fetch stats evolution")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"evolution": data})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetSellTopProducts returns top products by revenue (v0.401 M3)
|
|
|
|
|
func (h *MarketplaceHandler) GetSellTopProducts(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
limit := 10
|
|
|
|
|
if l := c.Query("limit"); l != "" {
|
|
|
|
|
if v, err := strconv.Atoi(l); err == nil && v > 0 && v <= 50 {
|
|
|
|
|
limit = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
data, err := h.service.GetSellerTopProducts(c.Request.Context(), userID, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to fetch top products")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"top_products": data})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetSellSales returns recent sales for the seller (v0.401 M3)
|
|
|
|
|
func (h *MarketplaceHandler) GetSellSales(c *gin.Context) {
|
|
|
|
|
userID, ok := GetUserIDUUID(c)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
limit := 20
|
|
|
|
|
if l := c.Query("limit"); l != "" {
|
|
|
|
|
if v, err := strconv.Atoi(l); err == nil && v > 0 && v <= 100 {
|
|
|
|
|
limit = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
data, err := h.service.GetSellerSales(c.Request.Context(), userID, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
response.InternalServerError(c, "Failed to fetch sales")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
response.Success(c, gin.H{"sales": data})
|
|
|
|
|
}
|