feat(marketplace): add migration 098 product_licenses, ProductLicense model, GET /licenses/mine
This commit is contained in:
parent
31a27e4724
commit
1fef428ce0
5 changed files with 181 additions and 4 deletions
|
|
@ -68,6 +68,7 @@ func (r *APIRouter) setupMarketplaceRoutes(router *gin.RouterGroup) {
|
|||
protected.GET("/orders/:id", marketHandler.GetOrder)
|
||||
protected.POST("/orders", marketHandler.CreateOrder)
|
||||
protected.GET("/download/:product_id", marketHandler.GetDownloadURL)
|
||||
protected.GET("/licenses/mine", marketHandler.GetMyLicenses)
|
||||
|
||||
marketplaceExtHandler := handlers.NewMarketplaceExtHandler(marketService, r.logger)
|
||||
protected.GET("/wishlist", marketplaceExtHandler.GetWishlist)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ type Product struct {
|
|||
|
||||
// Relations
|
||||
Previews []ProductPreview `gorm:"foreignKey:ProductID" json:"previews,omitempty"`
|
||||
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
|
||||
Images []ProductImage `gorm:"foreignKey:ProductID" json:"images,omitempty"`
|
||||
Licenses []ProductLicense `gorm:"foreignKey:ProductID" json:"licenses,omitempty"`
|
||||
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
|
@ -86,6 +87,23 @@ func (pi *ProductImage) BeforeCreate(tx *gorm.DB) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// ProductLicense représente un type de licence proposé pour un produit (v0.401 M2)
|
||||
type ProductLicense struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
|
||||
ProductID uuid.UUID `gorm:"type:uuid;not null" json:"product_id"`
|
||||
LicenseType string `gorm:"column:license_type;not null;size:50" json:"license_type"` // streaming, personal, commercial, exclusive
|
||||
PriceCents int `gorm:"column:price_cents;not null" json:"price_cents"`
|
||||
TermsText string `gorm:"column:terms_text;type:text" json:"terms_text,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (pl *ProductLicense) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if pl.ID == uuid.Nil {
|
||||
pl.ID = uuid.New()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Product) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if p.ID == uuid.Nil {
|
||||
p.ID = uuid.New()
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ type MarketplaceService interface {
|
|||
// v0.401 M1: Product previews and images
|
||||
AddProductPreview(ctx context.Context, productID uuid.UUID, sellerID uuid.UUID, filePath string, durationSec *int) (*ProductPreview, error)
|
||||
UpdateProductImages(ctx context.Context, productID uuid.UUID, sellerID uuid.UUID, images []ProductImageInput) ([]ProductImage, error)
|
||||
|
||||
// v0.401 M2: Product licenses
|
||||
GetProductLicenses(ctx context.Context, productID uuid.UUID) ([]ProductLicense, error)
|
||||
SetProductLicenses(ctx context.Context, productID uuid.UUID, sellerID uuid.UUID, licenses []ProductLicenseInput) ([]ProductLicense, error)
|
||||
}
|
||||
|
||||
// ProductImageInput represents input for adding/updating product images
|
||||
|
|
@ -80,6 +84,13 @@ type ProductImageInput struct {
|
|||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
|
||||
// ProductLicenseInput represents input for product license (v0.401 M2)
|
||||
type ProductLicenseInput struct {
|
||||
LicenseType string `json:"license_type"` // streaming, personal, commercial, exclusive
|
||||
PriceCents int `json:"price_cents"`
|
||||
TermsText string `json:"terms_text,omitempty"`
|
||||
}
|
||||
|
||||
// Service implémente MarketplaceService
|
||||
type Service struct {
|
||||
db *gorm.DB
|
||||
|
|
@ -153,12 +164,12 @@ func (s *Service) CreateProduct(ctx context.Context, product *Product) error {
|
|||
})
|
||||
}
|
||||
|
||||
// GetProduct retrieves a product by ID with preloaded previews and images (v0.401 M1)
|
||||
// GetProduct retrieves a product by ID with preloaded previews, images, licenses (v0.401 M1/M2)
|
||||
func (s *Service) GetProduct(ctx context.Context, id uuid.UUID) (*Product, error) {
|
||||
var product Product
|
||||
if err := s.db.Preload("Previews").Preload("Images", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("product_images.sort_order ASC")
|
||||
}).First(&product, "id = ?", id).Error; err != nil {
|
||||
}).Preload("Licenses").First(&product, "id = ?", id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrProductNotFound
|
||||
}
|
||||
|
|
@ -664,3 +675,46 @@ func (s *Service) UpdateProductImages(ctx context.Context, productID uuid.UUID,
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetProductLicenses returns all license types offered for a product (v0.401 M2)
|
||||
func (s *Service) GetProductLicenses(ctx context.Context, productID uuid.UUID) ([]ProductLicense, error) {
|
||||
var licenses []ProductLicense
|
||||
if err := s.db.WithContext(ctx).Where("product_id = ?", productID).Find(&licenses).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// SetProductLicenses replaces all licenses for a product (v0.401 M2)
|
||||
func (s *Service) SetProductLicenses(ctx context.Context, productID uuid.UUID, sellerID uuid.UUID, licenses []ProductLicenseInput) ([]ProductLicense, error) {
|
||||
var product Product
|
||||
if err := s.db.First(&product, "id = ?", productID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrProductNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if product.SellerID != sellerID {
|
||||
return nil, ErrInvalidSeller
|
||||
}
|
||||
if err := s.db.Where("product_id = ?", productID).Delete(&ProductLicense{}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]ProductLicense, 0, len(licenses))
|
||||
for _, in := range licenses {
|
||||
if in.LicenseType == "" || in.PriceCents < 0 {
|
||||
continue
|
||||
}
|
||||
pl := &ProductLicense{
|
||||
ProductID: productID,
|
||||
LicenseType: in.LicenseType,
|
||||
PriceCents: in.PriceCents,
|
||||
TermsText: in.TermsText,
|
||||
}
|
||||
if err := s.db.Create(pl).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, *pl)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@ type CreateProductRequest struct {
|
|||
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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// CreateProduct gère la création d'un produit
|
||||
|
|
@ -111,6 +117,26 @@ func (h *MarketplaceHandler) CreateProduct(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
response.Created(c, product)
|
||||
}
|
||||
|
||||
|
|
@ -467,6 +493,12 @@ type UpdateProductRequest struct {
|
|||
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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// UpdateProduct gère la mise à jour d'un produit
|
||||
|
|
@ -528,7 +560,23 @@ func (h *MarketplaceHandler) UpdateProduct(c *gin.Context) {
|
|||
updates["category"] = *req.Category
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
// 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 {
|
||||
response.BadRequest(c, "No fields to update")
|
||||
return
|
||||
}
|
||||
|
|
@ -547,6 +595,13 @@ func (h *MarketplaceHandler) UpdateProduct(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
response.Success(c, product)
|
||||
}
|
||||
|
||||
|
|
@ -618,6 +673,36 @@ func (h *MarketplaceHandler) GetOrder(c *gin.Context) {
|
|||
response.Success(c, order)
|
||||
}
|
||||
|
||||
// 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,
|
||||
"product": product,
|
||||
"order": order,
|
||||
"download_url": downloadURL,
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
response.Success(c, gin.H{"licenses": items})
|
||||
}
|
||||
|
||||
// GetSellStats returns seller stats (revenue, sales count) for the authenticated user
|
||||
func (h *MarketplaceHandler) GetSellStats(c *gin.Context) {
|
||||
userID, ok := GetUserIDUUID(c)
|
||||
|
|
|
|||
19
veza-backend-api/migrations/098_product_licenses.sql
Normal file
19
veza-backend-api/migrations/098_product_licenses.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- v0.401 M2: Product licenses (multiple license types per product with price and terms)
|
||||
-- Types: streaming, personal, commercial, exclusive
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'products') THEN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'product_licenses') THEN
|
||||
CREATE TABLE product_licenses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
product_id UUID NOT NULL REFERENCES products(id) ON DELETE CASCADE,
|
||||
license_type VARCHAR(50) NOT NULL,
|
||||
price_cents INTEGER NOT NULL,
|
||||
terms_text TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX idx_product_licenses_product_id ON product_licenses(product_id);
|
||||
END IF;
|
||||
END IF;
|
||||
END $$;
|
||||
Loading…
Reference in a new issue