feat: post store feature
This commit is contained in:
parent
98e1b81213
commit
b144ac7750
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -3,20 +3,20 @@ package model
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Product struct {
|
type Product struct {
|
||||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
|
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
|
||||||
StoreID string `gorm:"type:uuid;not null" json:"storeId"`
|
StoreID string `gorm:"type:uuid;not null;column:store_id" json:"storeId"`
|
||||||
Store Store `gorm:"foreignKey:StoreID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"store"`
|
Store Store `gorm:"foreignKey:StoreID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
||||||
ProductName string `gorm:"not null" json:"productName"`
|
ProductName string `gorm:"not null;column:product_name;index" json:"productName"`
|
||||||
ProductImages []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE;" json:"productImages"`
|
ProductImages []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE;" json:"productImages,omitempty"`
|
||||||
Quantity int `gorm:"not null" json:"quantity"`
|
Quantity int `gorm:"not null;column:quantity" json:"quantity"`
|
||||||
Saled int `gorm:"default:0" json:"saled"`
|
Saled int `gorm:"default:0;column:saled" json:"saled"`
|
||||||
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
CreatedAt time.Time `gorm:"default:current_timestamp;column:created_at" json:"createdAt"`
|
||||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
UpdatedAt time.Time `gorm:"default:current_timestamp;column:updated_at" json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductImage struct {
|
type ProductImage struct {
|
||||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
|
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
|
||||||
ProductID string `gorm:"type:uuid;not null" json:"productId"`
|
ProductID string `gorm:"type:uuid;not null;column:product_id" json:"productId"`
|
||||||
Product Product `gorm:"foreignKey:ProductID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"product"`
|
Product Product `gorm:"foreignKey:ProductID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
||||||
ImageURL string `gorm:"not null" json:"imageURL"`
|
ImageURL string `gorm:"not null;column:image_url" json:"imageURL"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,18 @@ package model
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"`
|
ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null;column:id" json:"id"`
|
||||||
UserID string `gorm:"type:uuid;not null" json:"userId"`
|
UserID string `gorm:"type:uuid;not null;column:user_id" json:"userId"`
|
||||||
User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"`
|
User User `gorm:"foreignKey:UserID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
||||||
StoreName string `gorm:"not null" json:"storeName"`
|
StoreName string `gorm:"not null;column:store_name;index" json:"storeName"`
|
||||||
StoreLogo string `gorm:"not null" json:"storeLogo"`
|
StoreLogo string `gorm:"not null;column:store_logo" json:"storeLogo"`
|
||||||
StoreBanner string `gorm:"not null" json:"storeBanner"`
|
StoreBanner string `gorm:"not null;column:store_banner" json:"storeBanner"`
|
||||||
StoreInfo string `gorm:"not null" json:"storeInfo"`
|
StoreInfo string `gorm:"not null;column:store_info" json:"storeInfo"`
|
||||||
StoreAddressID string `gorm:"type:uuid;not null" json:"storeAddressId"`
|
StoreAddressID string `gorm:"type:uuid;not null;column:store_address_id" json:"storeAddressId"`
|
||||||
StoreAddress Address `gorm:"foreignKey:StoreAddressID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"storeAddress"`
|
StoreAddress Address `gorm:"foreignKey:StoreAddressID;references:ID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"`
|
||||||
Followers int `gorm:"default:0" json:"followers"`
|
Followers int `gorm:"default:0;column:followers" json:"followers,omitempty"`
|
||||||
Products []Product `gorm:"foreignKey:StoreID;constraint:OnDelete:CASCADE;" json:"products"`
|
Products []Product `gorm:"foreignKey:StoreID;constraint:OnDelete:CASCADE;" json:"products,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
TotalProduct int `gorm:"default:0;column:total_product" json:"totalProduct,omitempty"`
|
||||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
CreatedAt time.Time `gorm:"default:current_timestamp;column:created_at" json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `gorm:"default:current_timestamp;column:updated_at" json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -20,4 +20,5 @@ func SetupRoutes(app *fiber.App) {
|
||||||
presentation.BannerRouter(api)
|
presentation.BannerRouter(api)
|
||||||
presentation.InitialCointRoute(api)
|
presentation.InitialCointRoute(api)
|
||||||
presentation.TrashRouter(api)
|
presentation.TrashRouter(api)
|
||||||
|
presentation.StoreRouter(api)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue