diff --git a/.gitignore b/.gitignore index 2768314..a622dd0 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,5 @@ go.work.sum .env.prod .env.dev -# Ignore avatar images -/public/uploads/avatars/ -/public/uploads/articles/ -/public/uploads/banners/ \ No newline at end of file +# Ignore public uploads +/public/uploads/ \ No newline at end of file diff --git a/dto/store_dto.go b/dto/store_dto.go index a527d50..f5fd606 100644 --- a/dto/store_dto.go +++ b/dto/store_dto.go @@ -31,33 +31,33 @@ 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") + errors["store_name"] = append(errors["store_name"], "Store name is required") } else if len(r.StoreName) < 3 { - errors["storeName"] = append(errors["storeName"], "Store name must be at least 3 characters long") + errors["store_name"] = append(errors["store_name"], "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") + errors["store_name"] = append(errors["store_name"], "Store name can only contain letters, numbers, underscores, and periods") } } if strings.TrimSpace(r.StoreLogo) == "" { - errors["storeLogo"] = append(errors["storeLogo"], "Store logo is required") + errors["store_logo"] = append(errors["store_logo"], "Store logo is required") } if strings.TrimSpace(r.StoreBanner) == "" { - errors["storeBanner"] = append(errors["storeBanner"], "Store banner is required") + errors["store_banner"] = append(errors["store_banner"], "Store banner is required") } if strings.TrimSpace(r.StoreInfo) == "" { - errors["storeInfo"] = append(errors["storeInfo"], "Store info is required") + errors["store_info"] = append(errors["store_info"], "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") + errors["store_address_id"] = append(errors["store_address_id"], "Store address ID is required") } else if matched, _ := regexp.MatchString(uuidRegex, r.StoreAddressID); !matched { - errors["storeAddressId"] = append(errors["storeAddressId"], "Invalid Store Address ID format") + errors["store_address_id"] = append(errors["store_address_id"], "Invalid Store Address ID format") } if len(errors) > 0 { diff --git a/internal/handler/store_handler.go b/internal/handler/store_handler.go index de5dcf1..bf759b8 100644 --- a/internal/handler/store_handler.go +++ b/internal/handler/store_handler.go @@ -19,32 +19,140 @@ func NewStoreHandler(storeService services.StoreService) *StoreHandler { func (h *StoreHandler) CreateStore(c *fiber.Ctx) error { - var requestStoreDTO dto.RequestStoreDTO - if err := c.BodyParser(&requestStoreDTO); err != nil { + storeName := c.FormValue("store_name") + storeInfo := c.FormValue("store_info") + storeAddressID := c.FormValue("store_address_id") - log.Printf("Error parsing body: %v", err) - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) + if storeName == "" || storeInfo == "" || storeAddressID == "" { + log.Println("Missing required fields") + return utils.GenericResponse(c, fiber.StatusBadRequest, "All fields are required") + } + + storeLogo, err := c.FormFile("store_logo") + if err != nil { + log.Printf("Error parsing store logo: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Store logo is required") + } + + storeBanner, err := c.FormFile("store_banner") + if err != nil { + log.Printf("Error parsing store banner: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Store banner is required") + } + + requestStoreDTO := dto.RequestStoreDTO{ + StoreName: storeName, + StoreLogo: storeLogo.Filename, + StoreBanner: storeBanner.Filename, + StoreInfo: storeInfo, + StoreAddressID: storeAddressID, } 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) + store, err := h.StoreService.CreateStore(userID, requestStoreDTO, storeLogo, storeBanner) 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") + return utils.CreateResponse(c, store, "Store created successfully") +} + +func (h *StoreHandler) GetStoreByUserID(c *fiber.Ctx) error { + 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.GetStoreByUserID(userID) + if err != nil { + log.Printf("Error fetching store: %v", err) + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + log.Printf("Store fetched successfully: %v", store) + return utils.SuccessResponse(c, store, "Store fetched successfully") +} + +func (h *StoreHandler) UpdateStore(c *fiber.Ctx) error { + storeID := c.Params("store_id") + if storeID == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Store ID is required") + } + + storeName := c.FormValue("store_name") + storeInfo := c.FormValue("store_info") + storeAddressID := c.FormValue("store_address_id") + + if storeName == "" || storeInfo == "" || storeAddressID == "" { + log.Println("Missing required fields") + return utils.GenericResponse(c, fiber.StatusBadRequest, "All fields are required") + } + + storeLogo, err := c.FormFile("store_logo") + if err != nil && err.Error() != "missing file" { + log.Printf("Error parsing store logo: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing store logo") + } + + storeBanner, err := c.FormFile("store_banner") + if err != nil && err.Error() != "missing file" { + log.Printf("Error parsing store banner: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing store banner") + } + + requestStoreDTO := dto.RequestStoreDTO{ + StoreName: storeName, + StoreLogo: storeLogo.Filename, + StoreBanner: storeBanner.Filename, + StoreInfo: storeInfo, + StoreAddressID: storeAddressID, + } + + 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.UpdateStore(storeID, &requestStoreDTO, storeLogo, storeBanner, userID) + if err != nil { + log.Printf("Error updating store: %v", err) + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + log.Printf("Store updated successfully: %v", store) + return utils.SuccessResponse(c, store, "Store updated successfully") +} + +func (h *StoreHandler) DeleteStore(c *fiber.Ctx) error { + storeID := c.Params("store_id") + if storeID == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Store ID is required") + } + + err := h.StoreService.DeleteStore(storeID) + if err != nil { + log.Printf("Error deleting store: %v", err) + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + log.Printf("Store deleted successfully: %v", storeID) + return utils.GenericResponse(c, fiber.StatusOK, "Store deleted successfully") } diff --git a/internal/repositories/store_repo.go b/internal/repositories/store_repo.go index 94b8b09..989e591 100644 --- a/internal/repositories/store_repo.go +++ b/internal/repositories/store_repo.go @@ -1,14 +1,21 @@ package repositories import ( + "fmt" + "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) type StoreRepository interface { FindStoreByUserID(userID string) (*model.Store, error) + FindStoreByID(storeID string) (*model.Store, error) FindAddressByID(addressID string) (*model.Address, error) + CreateStore(store *model.Store) error + UpdateStore(store *model.Store) error + + DeleteStore(storeID string) error } type storeRepository struct { @@ -30,6 +37,17 @@ func (r *storeRepository) FindStoreByUserID(userID string) (*model.Store, error) return &store, nil } +func (r *storeRepository) FindStoreByID(storeID string) (*model.Store, error) { + var store model.Store + if err := r.DB.Where("id = ?", storeID).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 { @@ -47,3 +65,23 @@ func (r *storeRepository) CreateStore(store *model.Store) error { } return nil } + +func (r *storeRepository) UpdateStore(store *model.Store) error { + if err := r.DB.Save(store).Error; err != nil { + return err + } + return nil +} + +func (r *storeRepository) DeleteStore(storeID string) error { + + if storeID == "" { + return fmt.Errorf("store ID cannot be empty") + } + + if err := r.DB.Where("id = ?", storeID).Delete(&model.Store{}).Error; err != nil { + return fmt.Errorf("failed to delete store: %w", err) + } + + return nil +} diff --git a/internal/services/store_service.go b/internal/services/store_service.go index 1694e42..067a777 100644 --- a/internal/services/store_service.go +++ b/internal/services/store_service.go @@ -2,7 +2,11 @@ package services import ( "fmt" + "mime/multipart" + "os" + "path/filepath" + "github.com/google/uuid" "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/model" @@ -10,7 +14,10 @@ import ( ) type StoreService interface { - CreateStore(userID string, storeDTO *dto.RequestStoreDTO) (*dto.ResponseStoreDTO, error) + CreateStore(userID string, storeDTO dto.RequestStoreDTO, storeLogo *multipart.FileHeader, storeBanner *multipart.FileHeader) (*dto.ResponseStoreDTO, error) + GetStoreByUserID(userID string) (*dto.ResponseStoreDTO, error) + UpdateStore(storeID string, storeDTO *dto.RequestStoreDTO, storeLogo *multipart.FileHeader, storeBanner *multipart.FileHeader, userID string) (*dto.ResponseStoreDTO, error) + DeleteStore(storeID string) error } type storeService struct { @@ -21,7 +28,11 @@ func NewStoreService(storeRepo repositories.StoreRepository) StoreService { return &storeService{storeRepo} } -func (s *storeService) CreateStore(userID string, storeDTO *dto.RequestStoreDTO) (*dto.ResponseStoreDTO, error) { +func (s *storeService) CreateStore(userID string, storeDTO dto.RequestStoreDTO, storeLogo, storeBanner *multipart.FileHeader) (*dto.ResponseStoreDTO, error) { + + if errors, valid := storeDTO.ValidateStoreInput(); !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } existingStore, err := s.storeRepo.FindStoreByUserID(userID) if err != nil { @@ -39,11 +50,21 @@ func (s *storeService) CreateStore(userID string, storeDTO *dto.RequestStoreDTO) return nil, fmt.Errorf("store address ID not found") } + storeLogoPath, err := s.saveStoreImage(storeLogo, "logo") + if err != nil { + return nil, fmt.Errorf("failed to save store logo: %w", err) + } + + storeBannerPath, err := s.saveStoreImage(storeBanner, "banner") + if err != nil { + return nil, fmt.Errorf("failed to save store banner: %w", err) + } + store := model.Store{ UserID: userID, StoreName: storeDTO.StoreName, - StoreLogo: storeDTO.StoreLogo, - StoreBanner: storeDTO.StoreBanner, + StoreLogo: storeLogoPath, + StoreBanner: storeBannerPath, StoreInfo: storeDTO.StoreInfo, StoreAddressID: storeDTO.StoreAddressID, } @@ -71,3 +92,202 @@ func (s *storeService) CreateStore(userID string, storeDTO *dto.RequestStoreDTO) return storeResponseDTO, nil } + +func (s *storeService) GetStoreByUserID(userID string) (*dto.ResponseStoreDTO, error) { + + store, err := s.storeRepo.FindStoreByUserID(userID) + if err != nil { + return nil, fmt.Errorf("error retrieving store by user ID: %w", err) + } + if store == nil { + return nil, fmt.Errorf("store not found for user ID: %s", userID) + } + + createdAt, err := utils.FormatDateToIndonesianFormat(store.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format createdAt: %w", err) + } + + updatedAt, err := utils.FormatDateToIndonesianFormat(store.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format updatedAt: %w", err) + } + + 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 +} + +func (s *storeService) UpdateStore(storeID string, storeDTO *dto.RequestStoreDTO, storeLogo, storeBanner *multipart.FileHeader, userID string) (*dto.ResponseStoreDTO, error) { + store, err := s.storeRepo.FindStoreByID(storeID) + if err != nil { + return nil, fmt.Errorf("error retrieving store by ID: %w", err) + } + if store == nil { + return nil, fmt.Errorf("store not found") + } + + if storeDTO.StoreAddressID == "" { + return nil, fmt.Errorf("store address ID cannot be empty") + } + + 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") + } + + if storeLogo != nil { + if err := s.deleteStoreImage(store.StoreLogo); err != nil { + return nil, fmt.Errorf("failed to delete old store logo: %w", err) + } + storeLogoPath, err := s.saveStoreImage(storeLogo, "logo") + if err != nil { + return nil, fmt.Errorf("failed to save store logo: %w", err) + } + store.StoreLogo = storeLogoPath + } + + if storeBanner != nil { + if err := s.deleteStoreImage(store.StoreBanner); err != nil { + return nil, fmt.Errorf("failed to delete old store banner: %w", err) + } + storeBannerPath, err := s.saveStoreImage(storeBanner, "banner") + if err != nil { + return nil, fmt.Errorf("failed to save store banner: %w", err) + } + store.StoreBanner = storeBannerPath + } + + store.StoreName = storeDTO.StoreName + store.StoreInfo = storeDTO.StoreInfo + store.StoreAddressID = storeDTO.StoreAddressID + + if err := s.storeRepo.UpdateStore(store); err != nil { + return nil, fmt.Errorf("failed to update store: %w", err) + } + + createdAt, err := utils.FormatDateToIndonesianFormat(store.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format createdAt: %w", err) + } + updatedAt, err := utils.FormatDateToIndonesianFormat(store.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format updatedAt: %w", err) + } + + 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 +} + +func (s *storeService) DeleteStore(storeID string) error { + store, err := s.storeRepo.FindStoreByID(storeID) + if err != nil { + return fmt.Errorf("error retrieving store by ID: %w", err) + } + if store == nil { + return fmt.Errorf("store not found") + } + + if store.StoreLogo != "" { + if err := s.deleteStoreImage(store.StoreLogo); err != nil { + return fmt.Errorf("failed to delete store logo: %w", err) + } + } + + if store.StoreBanner != "" { + if err := s.deleteStoreImage(store.StoreBanner); err != nil { + return fmt.Errorf("failed to delete store banner: %w", err) + } + } + + if err := s.storeRepo.DeleteStore(storeID); err != nil { + return fmt.Errorf("failed to delete store: %w", err) + } + + return nil +} + +func (s *storeService) saveStoreImage(file *multipart.FileHeader, imageType string) (string, error) { + + imageDir := fmt.Sprintf("./public/uploads/store/%s", imageType) + if _, err := os.Stat(imageDir); os.IsNotExist(err) { + + if err := os.MkdirAll(imageDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for %s image: %v", imageType, err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(file.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed for %s", imageType) + } + + fileName := fmt.Sprintf("%s_%s%s", imageType, uuid.New().String(), extension) + filePath := filepath.Join(imageDir, fileName) + + fileData, err := file.Open() + if err != nil { + return "", fmt.Errorf("failed to open file: %w", err) + } + defer fileData.Close() + + outFile, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("failed to create %s image file: %v", imageType, err) + } + defer outFile.Close() + + if _, err := outFile.ReadFrom(fileData); err != nil { + return "", fmt.Errorf("failed to save %s image: %v", imageType, err) + } + + return filepath.Join("/uploads/store", imageType, fileName), nil +} + +func (s *storeService) deleteStoreImage(imagePath string) error { + if imagePath == "" { + return nil + } + + filePath := filepath.Join("./public", imagePath) + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + return nil + } + + err := os.Remove(filePath) + if err != nil { + return fmt.Errorf("failed to delete file at %s: %w", filePath, err) + } + + return nil +} diff --git a/presentation/store_route.go b/presentation/store_route.go index e2d41dd..085deab 100644 --- a/presentation/store_route.go +++ b/presentation/store_route.go @@ -17,5 +17,9 @@ func StoreRouter(api fiber.Router) { storeHandler := handler.NewStoreHandler(storeService) storeAPI := api.Group("/storerijig") - storeAPI.Post("/create", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), storeHandler.CreateStore) + + storeAPI.Post("/create", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), storeHandler.CreateStore) + storeAPI.Get("/getbyuser", middleware.AuthMiddleware, storeHandler.GetStoreByUserID) + storeAPI.Put("/update/:store_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), storeHandler.UpdateStore) + storeAPI.Delete("/delete/:store_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), storeHandler.DeleteStore) }