first
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
package products
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
type galleryItemResponse struct {
|
||||
MediaID uuid.UUID `json:"media_id"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
type productResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
CategoryID *uuid.UUID `json:"category_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ShortDescription string `json:"short_description"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
PrimaryMediaID *uuid.UUID `json:"primary_media_id,omitempty"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
func toResponse(p *Product) productResponse {
|
||||
return productResponse{
|
||||
ID: p.ID,
|
||||
CategoryID: p.CategoryID,
|
||||
Name: p.Name,
|
||||
Slug: p.Slug,
|
||||
ShortDescription: p.ShortDescription,
|
||||
Description: p.Description,
|
||||
IsActive: p.IsActive,
|
||||
IsFeatured: p.IsFeatured,
|
||||
PrimaryMediaID: p.PrimaryMediaID,
|
||||
Position: p.Position,
|
||||
}
|
||||
}
|
||||
|
||||
func parseCategoryFilter(c *gin.Context) *uuid.UUID {
|
||||
raw := c.Query("category_id")
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
id, err := uuid.Parse(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &id
|
||||
}
|
||||
|
||||
func (h *Handler) ListAdmin(c *gin.Context) {
|
||||
list, err := h.service.List(c.Request.Context(), false, parseCategoryFilter(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list products"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"products": toResponseList(list)})
|
||||
}
|
||||
|
||||
func (h *Handler) ListPublic(c *gin.Context) {
|
||||
list, err := h.service.List(c.Request.Context(), true, parseCategoryFilter(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list products"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"products": toResponseList(list)})
|
||||
}
|
||||
|
||||
func toResponseList(list []*Product) []productResponse {
|
||||
resp := make([]productResponse, 0, len(list))
|
||||
for _, p := range list {
|
||||
resp = append(resp, toResponse(p))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func (h *Handler) GetAdmin(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
p, err := h.service.Get(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
h.respondGetError(c, err)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toResponse(p))
|
||||
}
|
||||
|
||||
func (h *Handler) GetPublicBySlug(c *gin.Context) {
|
||||
p, err := h.service.GetBySlug(c.Request.Context(), c.Param("slug"))
|
||||
if err != nil {
|
||||
h.respondGetError(c, err)
|
||||
return
|
||||
}
|
||||
if !p.IsActive {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toResponse(p))
|
||||
}
|
||||
|
||||
func (h *Handler) respondGetError(c *gin.Context, err error) {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load product"})
|
||||
}
|
||||
|
||||
type upsertRequest struct {
|
||||
CategoryID *uuid.UUID `json:"category_id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Slug string `json:"slug" binding:"required"`
|
||||
ShortDescription string `json:"short_description"`
|
||||
Description string `json:"description"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsFeatured bool `json:"is_featured"`
|
||||
PrimaryMediaID *uuid.UUID `json:"primary_media_id"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
func (req upsertRequest) toInput() UpsertInput {
|
||||
return UpsertInput{
|
||||
CategoryID: req.CategoryID,
|
||||
Name: req.Name,
|
||||
Slug: req.Slug,
|
||||
ShortDescription: req.ShortDescription,
|
||||
Description: req.Description,
|
||||
IsActive: req.IsActive,
|
||||
IsFeatured: req.IsFeatured,
|
||||
PrimaryMediaID: req.PrimaryMediaID,
|
||||
Position: req.Position,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
var req upsertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
p, err := h.service.Create(c.Request.Context(), req.toInput())
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrSlugTaken) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "slug already in use"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create product"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, toResponse(p))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var req upsertRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
p, err := h.service.Update(c.Request.Context(), id, req.toInput())
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrNotFound):
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
|
||||
case errors.Is(err, ErrSlugTaken):
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "slug already in use"})
|
||||
default:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update product"})
|
||||
}
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toResponse(p))
|
||||
}
|
||||
|
||||
type updatePositionRequest struct {
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
// UpdatePosition lets the admin reorder the catalog display order (e.g.
|
||||
// move up/move down in the admin list) without resending the whole
|
||||
// product payload.
|
||||
func (h *Handler) UpdatePosition(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var req updatePositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
p, err := h.service.UpdatePosition(c.Request.Context(), id, req.Position)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update position"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toResponse(p))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(c.Request.Context(), id); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete product"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type addGalleryRequest struct {
|
||||
MediaID uuid.UUID `json:"media_id" binding:"required"`
|
||||
Position int `json:"position"`
|
||||
}
|
||||
|
||||
func (h *Handler) AddGalleryItem(c *gin.Context) {
|
||||
productID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
var req addGalleryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "validation error", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.service.AddGalleryItem(c.Request.Context(), productID, req.MediaID, req.Position); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to attach media"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (h *Handler) RemoveGalleryItem(c *gin.Context) {
|
||||
productID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
mediaID, err := uuid.Parse(c.Param("mediaId"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid media id"})
|
||||
return
|
||||
}
|
||||
if err := h.service.RemoveGalleryItem(c.Request.Context(), productID, mediaID); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "gallery item not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to detach media"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) ListGallery(c *gin.Context) {
|
||||
productID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListGallery(c.Request.Context(), productID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list gallery"})
|
||||
return
|
||||
}
|
||||
resp := make([]galleryItemResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
resp = append(resp, galleryItemResponse{MediaID: item.MediaID, Position: item.Position})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"gallery": resp})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Package products owns the product catalog entity itself (identity,
|
||||
// description, category, media). Quantity-based pricing lives in the
|
||||
// pricing module, which references products by ID -- keeping "what a
|
||||
// product is" separate from "how it's priced", per spec section 13/16.
|
||||
package products
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Product struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
|
||||
CategoryID *uuid.UUID `gorm:"type:uuid"`
|
||||
Name string `gorm:"not null"`
|
||||
Slug string `gorm:"uniqueIndex;not null"`
|
||||
ShortDescription string `gorm:"not null;default:''"`
|
||||
Description string `gorm:"not null;default:''"`
|
||||
IsActive bool `gorm:"not null;default:true"`
|
||||
IsFeatured bool `gorm:"not null;default:false"`
|
||||
PrimaryMediaID *uuid.UUID `gorm:"type:uuid"`
|
||||
Position int `gorm:"not null;default:0"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (Product) TableName() string { return "products" }
|
||||
|
||||
// GalleryItem attaches a media item to a product's gallery, ordered by
|
||||
// Position. A product's primary image (PrimaryMediaID) is separate from
|
||||
// the gallery so it can be picked from unrelated media too.
|
||||
type GalleryItem struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
|
||||
ProductID uuid.UUID `gorm:"type:uuid;not null;index"`
|
||||
MediaID uuid.UUID `gorm:"type:uuid;not null"`
|
||||
Position int `gorm:"not null;default:0"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func (GalleryItem) TableName() string { return "product_media" }
|
||||
@@ -0,0 +1,132 @@
|
||||
package products
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("product not found")
|
||||
ErrSlugTaken = errors.New("slug already in use")
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
Create(ctx context.Context, p *Product) error
|
||||
FindByID(ctx context.Context, id uuid.UUID) (*Product, error)
|
||||
FindBySlug(ctx context.Context, slug string) (*Product, error)
|
||||
List(ctx context.Context, activeOnly bool, categoryID *uuid.UUID) ([]*Product, error)
|
||||
Update(ctx context.Context, p *Product) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
AddGalleryItem(ctx context.Context, item *GalleryItem) error
|
||||
RemoveGalleryItem(ctx context.Context, productID, mediaID uuid.UUID) error
|
||||
ListGallery(ctx context.Context, productID uuid.UUID) ([]*GalleryItem, error)
|
||||
}
|
||||
|
||||
type gormRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) Repository {
|
||||
return &gormRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *gormRepository) Create(ctx context.Context, p *Product) error {
|
||||
if err := r.db.WithContext(ctx).Create(p).Error; err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
return ErrSlugTaken
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) FindByID(ctx context.Context, id uuid.UUID) (*Product, error) {
|
||||
var p Product
|
||||
err := r.db.WithContext(ctx).Where("id = ?", id).First(&p).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) FindBySlug(ctx context.Context, slug string) (*Product, error) {
|
||||
var p Product
|
||||
err := r.db.WithContext(ctx).Where("slug = ?", slug).First(&p).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) List(ctx context.Context, activeOnly bool, categoryID *uuid.UUID) ([]*Product, error) {
|
||||
q := r.db.WithContext(ctx).Order("position asc, created_at desc")
|
||||
if activeOnly {
|
||||
q = q.Where("is_active = ?", true)
|
||||
}
|
||||
if categoryID != nil {
|
||||
q = q.Where("category_id = ?", *categoryID)
|
||||
}
|
||||
var list []*Product
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) Update(ctx context.Context, p *Product) error {
|
||||
err := r.db.WithContext(ctx).Save(p).Error
|
||||
if isUniqueViolation(err) {
|
||||
return ErrSlugTaken
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *gormRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
res := r.db.WithContext(ctx).Delete(&Product{}, "id = ?", id)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) AddGalleryItem(ctx context.Context, item *GalleryItem) error {
|
||||
return r.db.WithContext(ctx).Create(item).Error
|
||||
}
|
||||
|
||||
func (r *gormRepository) RemoveGalleryItem(ctx context.Context, productID, mediaID uuid.UUID) error {
|
||||
res := r.db.WithContext(ctx).Delete(&GalleryItem{}, "product_id = ? AND media_id = ?", productID, mediaID)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *gormRepository) ListGallery(ctx context.Context, productID uuid.UUID) ([]*GalleryItem, error) {
|
||||
var list []*GalleryItem
|
||||
if err := r.db.WithContext(ctx).Where("product_id = ?", productID).Order("position asc").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func isUniqueViolation(err error) bool {
|
||||
var pgErr *pgconn.PgError
|
||||
return errors.As(err, &pgErr) && pgErr.Code == "23505"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package products
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func RegisterAdminRoutes(rg *gin.RouterGroup, h *Handler, requireAdmin gin.HandlerFunc) {
|
||||
group := rg.Group("/admin/products", requireAdmin)
|
||||
group.GET("", h.ListAdmin)
|
||||
group.POST("", h.Create)
|
||||
group.GET("/:id", h.GetAdmin)
|
||||
group.PUT("/:id", h.Update)
|
||||
group.PATCH("/:id/position", h.UpdatePosition)
|
||||
group.DELETE("/:id", h.Delete)
|
||||
|
||||
group.GET("/:id/gallery", h.ListGallery)
|
||||
group.POST("/:id/gallery", h.AddGalleryItem)
|
||||
group.DELETE("/:id/gallery/:mediaId", h.RemoveGalleryItem)
|
||||
}
|
||||
|
||||
// RegisterPublicRoutes exposes the read-only, active-only product catalog
|
||||
// used by the storefront (site vitrine / boutique).
|
||||
func RegisterPublicRoutes(rg *gin.RouterGroup, h *Handler) {
|
||||
rg.GET("/products", h.ListPublic)
|
||||
rg.GET("/products/:slug", h.GetPublicBySlug)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package products
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) List(ctx context.Context, activeOnly bool, categoryID *uuid.UUID) ([]*Product, error) {
|
||||
return s.repo.List(ctx, activeOnly, categoryID)
|
||||
}
|
||||
|
||||
func (s *Service) Get(ctx context.Context, id uuid.UUID) (*Product, error) {
|
||||
return s.repo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *Service) GetBySlug(ctx context.Context, slug string) (*Product, error) {
|
||||
return s.repo.FindBySlug(ctx, slug)
|
||||
}
|
||||
|
||||
type UpsertInput struct {
|
||||
CategoryID *uuid.UUID
|
||||
Name string
|
||||
Slug string
|
||||
ShortDescription string
|
||||
Description string
|
||||
IsActive bool
|
||||
IsFeatured bool
|
||||
PrimaryMediaID *uuid.UUID
|
||||
Position int
|
||||
}
|
||||
|
||||
func (s *Service) Create(ctx context.Context, in UpsertInput) (*Product, error) {
|
||||
p := &Product{
|
||||
ID: uuid.New(),
|
||||
CategoryID: in.CategoryID,
|
||||
Name: in.Name,
|
||||
Slug: in.Slug,
|
||||
ShortDescription: in.ShortDescription,
|
||||
Description: in.Description,
|
||||
IsActive: in.IsActive,
|
||||
IsFeatured: in.IsFeatured,
|
||||
PrimaryMediaID: in.PrimaryMediaID,
|
||||
Position: in.Position,
|
||||
}
|
||||
if err := s.repo.Create(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) Update(ctx context.Context, id uuid.UUID, in UpsertInput) (*Product, error) {
|
||||
p, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.CategoryID = in.CategoryID
|
||||
p.Name = in.Name
|
||||
p.Slug = in.Slug
|
||||
p.ShortDescription = in.ShortDescription
|
||||
p.Description = in.Description
|
||||
p.IsActive = in.IsActive
|
||||
p.IsFeatured = in.IsFeatured
|
||||
p.PrimaryMediaID = in.PrimaryMediaID
|
||||
p.Position = in.Position
|
||||
if err := s.repo.Update(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdatePosition lets the admin reorder the catalog display order without
|
||||
// resending the full product payload.
|
||||
func (s *Service) UpdatePosition(ctx context.Context, id uuid.UUID, position int) (*Product, error) {
|
||||
p, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.Position = position
|
||||
if err := s.repo.Update(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (s *Service) AddGalleryItem(ctx context.Context, productID, mediaID uuid.UUID, position int) error {
|
||||
return s.repo.AddGalleryItem(ctx, &GalleryItem{
|
||||
ID: uuid.New(),
|
||||
ProductID: productID,
|
||||
MediaID: mediaID,
|
||||
Position: position,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) RemoveGalleryItem(ctx context.Context, productID, mediaID uuid.UUID) error {
|
||||
return s.repo.RemoveGalleryItem(ctx, productID, mediaID)
|
||||
}
|
||||
|
||||
func (s *Service) ListGallery(ctx context.Context, productID uuid.UUID) ([]*GalleryItem, error) {
|
||||
return s.repo.ListGallery(ctx, productID)
|
||||
}
|
||||
Reference in New Issue
Block a user