feat: post store feature

This commit is contained in:
pahmiudahgede 2025-02-22 19:44:03 +07:00
parent 98e1b81213
commit b144ac7750
8 changed files with 289 additions and 26 deletions

68
dto/store_dto.go Normal file
View File

@ -0,0 +1,68 @@
package dto
import (
"regexp"
"strings"
)
type ResponseStoreDTO struct {
ID string `json:"id"`
UserID string `json:"userId"`
StoreName string `json:"storeName"`
StoreLogo string `json:"storeLogo"`
StoreBanner string `json:"storeBanner"`
StoreInfo string `json:"storeInfo"`
StoreAddressID string `json:"storeAddressId"`
TotalProduct int `json:"TotalProduct"`
Followers int `json:"followers"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestStoreDTO struct {
StoreName string `json:"store_name"`
StoreLogo string `json:"store_logo"`
StoreBanner string `json:"store_banner"`
StoreInfo string `json:"store_info"`
StoreAddressID string `json:"store_address_id"`
}
func (r *RequestStoreDTO) ValidateStoreInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.StoreName) == "" {
errors["storeName"] = append(errors["storeName"], "Store name is required")
} else if len(r.StoreName) < 3 {
errors["storeName"] = append(errors["storeName"], "Store name must be at least 3 characters long")
} else {
validNameRegex := `^[a-zA-Z0-9_.\s]+$`
if matched, _ := regexp.MatchString(validNameRegex, r.StoreName); !matched {
errors["storeName"] = append(errors["storeName"], "Store name can only contain letters, numbers, underscores, and periods")
}
}
if strings.TrimSpace(r.StoreLogo) == "" {
errors["storeLogo"] = append(errors["storeLogo"], "Store logo is required")
}
if strings.TrimSpace(r.StoreBanner) == "" {
errors["storeBanner"] = append(errors["storeBanner"], "Store banner is required")
}
if strings.TrimSpace(r.StoreInfo) == "" {
errors["storeInfo"] = append(errors["storeInfo"], "Store info is required")
}
uuidRegex := `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`
if r.StoreAddressID == "" {
errors["storeAddressId"] = append(errors["storeAddressId"], "Store address ID is required")
} else if matched, _ := regexp.MatchString(uuidRegex, r.StoreAddressID); !matched {
errors["storeAddressId"] = append(errors["storeAddressId"], "Invalid Store Address ID format")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,50 @@
package handler
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type StoreHandler struct {
StoreService services.StoreService
}
func NewStoreHandler(storeService services.StoreService) *StoreHandler {
return &StoreHandler{StoreService: storeService}
}
func (h *StoreHandler) CreateStore(c *fiber.Ctx) error {
var requestStoreDTO dto.RequestStoreDTO
if err := c.BodyParser(&requestStoreDTO); err != nil {
log.Printf("Error parsing body: %v", err)
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}})
}
errors, valid := requestStoreDTO.ValidateStoreInput()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userID, ok := c.Locals("userID").(string)
if !ok {
log.Println("User ID not found in Locals")
return utils.GenericResponse(c, fiber.StatusUnauthorized, "User ID not found")
}
store, err := h.StoreService.CreateStore(userID, &requestStoreDTO)
if err != nil {
log.Printf("Error creating store: %v", err)
return utils.GenericResponse(c, fiber.StatusConflict, err.Error())
}
return utils.CreateResponse(c, store, "store created successfully")
}

View File

@ -0,0 +1,49 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type StoreRepository interface {
FindStoreByUserID(userID string) (*model.Store, error)
FindAddressByID(addressID string) (*model.Address, error)
CreateStore(store *model.Store) error
}
type storeRepository struct {
DB *gorm.DB
}
func NewStoreRepository(DB *gorm.DB) StoreRepository {
return &storeRepository{DB}
}
func (r *storeRepository) FindStoreByUserID(userID string) (*model.Store, error) {
var store model.Store
if err := r.DB.Where("user_id = ?", userID).First(&store).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &store, nil
}
func (r *storeRepository) FindAddressByID(addressID string) (*model.Address, error) {
var address model.Address
if err := r.DB.Where("id = ?", addressID).First(&address).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &address, nil
}
func (r *storeRepository) CreateStore(store *model.Store) error {
if err := r.DB.Create(store).Error; err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,73 @@
package services
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/model"
"github.com/pahmiudahgede/senggoldong/utils"
)
type StoreService interface {
CreateStore(userID string, storeDTO *dto.RequestStoreDTO) (*dto.ResponseStoreDTO, error)
}
type storeService struct {
storeRepo repositories.StoreRepository
}
func NewStoreService(storeRepo repositories.StoreRepository) StoreService {
return &storeService{storeRepo}
}
func (s *storeService) CreateStore(userID string, storeDTO *dto.RequestStoreDTO) (*dto.ResponseStoreDTO, error) {
existingStore, err := s.storeRepo.FindStoreByUserID(userID)
if err != nil {
return nil, fmt.Errorf("error checking if user already has a store: %w", err)
}
if existingStore != nil {
return nil, fmt.Errorf("user already has a store")
}
address, err := s.storeRepo.FindAddressByID(storeDTO.StoreAddressID)
if err != nil {
return nil, fmt.Errorf("error validating store address ID: %w", err)
}
if address == nil {
return nil, fmt.Errorf("store address ID not found")
}
store := model.Store{
UserID: userID,
StoreName: storeDTO.StoreName,
StoreLogo: storeDTO.StoreLogo,
StoreBanner: storeDTO.StoreBanner,
StoreInfo: storeDTO.StoreInfo,
StoreAddressID: storeDTO.StoreAddressID,
}
if err := s.storeRepo.CreateStore(&store); err != nil {
return nil, fmt.Errorf("failed to create store: %w", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(store.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(store.UpdatedAt)
storeResponseDTO := &dto.ResponseStoreDTO{
ID: store.ID,
UserID: store.UserID,
StoreName: store.StoreName,
StoreLogo: store.StoreLogo,
StoreBanner: store.StoreBanner,
StoreInfo: store.StoreInfo,
StoreAddressID: store.StoreAddressID,
TotalProduct: store.TotalProduct,
Followers: store.Followers,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
return storeResponseDTO, nil
}

View File

@ -3,20 +3,20 @@ package model
import "time"
type Product struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
StoreID string `gorm:"type:uuid;not null" json:"storeId"`
Store Store `gorm:"foreignKey:StoreID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"store"`
ProductName string `gorm:"not null" json:"productName"`
ProductImages []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE;" json:"productImages"`
Quantity int `gorm:"not null" json:"quantity"`
Saled int `gorm:"default:0" json:"saled"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
StoreID string `gorm:"type:uuid;not null;column:store_id" json:"storeId"`
Store Store `gorm:"foreignKey:StoreID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
ProductName string `gorm:"not null;column:product_name;index" json:"productName"`
ProductImages []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE;" json:"productImages,omitempty"`
Quantity int `gorm:"not null;column:quantity" json:"quantity"`
Saled int `gorm:"default:0;column:saled" json:"saled"`
CreatedAt time.Time `gorm:"default:current_timestamp;column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp;column:updated_at" json:"updatedAt"`
}
type ProductImage struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
ProductID string `gorm:"type:uuid;not null" json:"productId"`
Product Product `gorm:"foreignKey:ProductID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"product"`
ImageURL string `gorm:"not null" json:"imageURL"`
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
ProductID string `gorm:"type:uuid;not null;column:product_id" json:"productId"`
Product Product `gorm:"foreignKey:ProductID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
ImageURL string `gorm:"not null;column:image_url" json:"imageURL"`
}

View File

@ -3,17 +3,18 @@ package model
import "time"
type Store struct {
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
UserID string `gorm:"type:uuid;not null" json:"userId"`
User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"`
StoreName string `gorm:"not null" json:"storeName"`
StoreLogo string `gorm:"not null" json:"storeLogo"`
StoreBanner string `gorm:"not null" json:"storeBanner"`
StoreInfo string `gorm:"not null" json:"storeInfo"`
StoreAddressID string `gorm:"type:uuid;not null" json:"storeAddressId"`
StoreAddress Address `gorm:"foreignKey:StoreAddressID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"storeAddress"`
Followers int `gorm:"default:0" json:"followers"`
Products []Product `gorm:"foreignKey:StoreID;constraint:OnDelete:CASCADE;" json:"products"`
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
UserID string `gorm:"type:uuid;not null;column:user_id" json:"userId"`
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
StoreName string `gorm:"not null;column:store_name;index" json:"storeName"`
StoreLogo string `gorm:"not null;column:store_logo" json:"storeLogo"`
StoreBanner string `gorm:"not null;column:store_banner" json:"storeBanner"`
StoreInfo string `gorm:"not null;column:store_info" json:"storeInfo"`
StoreAddressID string `gorm:"type:uuid;not null;column:store_address_id" json:"storeAddressId"`
StoreAddress Address `gorm:"foreignKey:StoreAddressID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
Followers int `gorm:"default:0;column:followers" json:"followers,omitempty"`
Products []Product `gorm:"foreignKey:StoreID;constraint:OnDelete:CASCADE;" json:"products,omitempty"`
TotalProduct int `gorm:"default:0;column:total_product" json:"totalProduct,omitempty"`
CreatedAt time.Time `gorm:"default:current_timestamp;column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"default:current_timestamp;column:updated_at" json:"updatedAt"`
}

View File

@ -0,0 +1,21 @@
package presentation
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/config"
"github.com/pahmiudahgede/senggoldong/internal/handler"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/middleware"
"github.com/pahmiudahgede/senggoldong/utils"
)
func StoreRouter(api fiber.Router) {
storeRepo := repositories.NewStoreRepository(config.DB)
storeService := services.NewStoreService(storeRepo)
storeHandler := handler.NewStoreHandler(storeService)
storeAPI := api.Group("/storerijig")
storeAPI.Post("/create", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), storeHandler.CreateStore)
}

View File

@ -20,4 +20,5 @@ func SetupRoutes(app *fiber.App) {
presentation.BannerRouter(api)
presentation.InitialCointRoute(api)
presentation.TrashRouter(api)
presentation.StoreRouter(api)
}