diff --git a/dto/store_dto.go b/dto/store_dto.go new file mode 100644 index 0000000..a527d50 --- /dev/null +++ b/dto/store_dto.go @@ -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 +} diff --git a/internal/handler/store_handler.go b/internal/handler/store_handler.go new file mode 100644 index 0000000..de5dcf1 --- /dev/null +++ b/internal/handler/store_handler.go @@ -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") +} diff --git a/internal/repositories/store_repo.go b/internal/repositories/store_repo.go new file mode 100644 index 0000000..94b8b09 --- /dev/null +++ b/internal/repositories/store_repo.go @@ -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 +} diff --git a/internal/services/store_service.go b/internal/services/store_service.go new file mode 100644 index 0000000..1694e42 --- /dev/null +++ b/internal/services/store_service.go @@ -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 +} diff --git a/model/product_model.go b/model/product_model.go index 48ce531..13fe765 100644 --- a/model/product_model.go +++ b/model/product_model.go @@ -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"` } diff --git a/model/store_model.go b/model/store_model.go index 4b7af9b..b4489eb 100644 --- a/model/store_model.go +++ b/model/store_model.go @@ -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"` } diff --git a/presentation/store_route.go b/presentation/store_route.go new file mode 100644 index 0000000..e2d41dd --- /dev/null +++ b/presentation/store_route.go @@ -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) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 374dbac..84e9e80 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -20,4 +20,5 @@ func SetupRoutes(app *fiber.App) { presentation.BannerRouter(api) presentation.InitialCointRoute(api) presentation.TrashRouter(api) + presentation.StoreRouter(api) }