diff --git a/domain/product.go b/domain/product.go index ee862de..a921304 100644 --- a/domain/product.go +++ b/domain/product.go @@ -6,7 +6,7 @@ type Product struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` UserID string `gorm:"type:uuid;not null" json:"user_id"` ProductTitle string `gorm:"not null" json:"product_title"` - ProductImages []ProductImage `gorm:"foreignKey:ProductID" json:"product_images"` + ProductImages []ProductImage `gorm:"foreignKey:ProductID;constraint:OnDelete:CASCADE;" json:"product_images"` TrashDetailID string `gorm:"type:uuid;not null" json:"trash_detail_id"` TrashDetail TrashDetail `gorm:"foreignKey:TrashDetailID" json:"trash_detail"` SalePrice int64 `gorm:"not null" json:"sale_price"` diff --git a/dto/product.go b/dto/product.go index 784afac..982cd56 100644 --- a/dto/product.go +++ b/dto/product.go @@ -1,18 +1,19 @@ package dto -type ProductResponseDTO struct { - ID string `json:"id"` - UserID string `json:"user_id"` - ProductTitle string `json:"product_title"` - ProductImages []ProductImageDTO `json:"product_images"` - TrashDetail TrashDetailResponseDTO `json:"trash_detail"` +import "errors" - SalePrice int64 `json:"sale_price"` - Quantity int `json:"quantity"` - ProductDescribe string `json:"product_describe"` - Sold int `json:"sold"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` +type ProductResponseDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + ProductTitle string `json:"product_title"` + ProductImages []ProductImageDTO `json:"product_images"` + TrashDetail TrashDetailResponseDTO `json:"trash_detail"` + SalePrice int64 `json:"sale_price"` + Quantity int `json:"quantity"` + ProductDescribe string `json:"product_describe"` + Sold int `json:"sold"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } type ProductImageDTO struct { @@ -24,3 +25,30 @@ type TrashDetailResponseDTO struct { Description string `json:"description"` Price int `json:"price"` } + +type CreateProductRequestDTO struct { + ProductTitle string `json:"product_title" validate:"required,min=3,max=255"` + ProductImages []string `json:"product_images" validate:"required,min=1,dive,url"` + TrashDetailID string `json:"trash_detail_id" validate:"required,uuid"` + SalePrice int64 `json:"sale_price" validate:"required,gt=0"` + Quantity int `json:"quantity" validate:"required,gt=0"` + ProductDescribe string `json:"product_describe,omitempty"` +} + +type CreateProductResponseDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + ProductTitle string `json:"product_title"` + ProductImages []string `json:"product_images"` + TrashDetailID string `json:"trash_detail_id"` + SalePrice int64 `json:"sale_price"` + Quantity int `json:"quantity"` + ProductDescribe string `json:"product_describe,omitempty"` +} + +func ValidateSalePrice(marketPrice, salePrice int64) error { + if salePrice > marketPrice { + return errors.New("sale price cannot be greater than market price") + } + return nil +} diff --git a/dto/validator.go b/dto/validator.go index fb5440e..47d9a65 100644 --- a/dto/validator.go +++ b/dto/validator.go @@ -2,4 +2,8 @@ package dto import "github.com/go-playground/validator/v10" -var validate = validator.New() \ No newline at end of file +var validate = validator.New() + +func GetValidator() *validator.Validate { + return validate +} diff --git a/internal/api/routes.go b/internal/api/routes.go index b90b3fe..0885147 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -102,11 +102,12 @@ func AppRouter(app *fiber.App) { api.Get("/wilayah-indonesia/villages/:id", controllers.GetVillageByID) // # request pickup by user (masyarakat) # - api.Get("/requestpickup", middleware.RoleRequired(utils.RoleMasyarakat), controllers.GetRequestPickupsByUser) + api.Get("/requestpickup", middleware.RoleRequired(utils.RoleMasyarakat, utils.RolePengepul), controllers.GetRequestPickupsByUser) api.Post("/addrequestpickup", middleware.RoleRequired(utils.RoleMasyarakat), controllers.CreateRequestPickup) api.Delete("/deleterequestpickup/:id", middleware.RoleRequired(utils.RoleMasyarakat), controllers.DeleteRequestPickup) // # product post by pengepul api.Get("/post/products", middleware.RoleRequired(utils.RolePengepul), controllers.GetAllProducts) api.Get("/post/product/:productid", middleware.RoleRequired(utils.RolePengepul), controllers.GetProductByID) + api.Post("/post/addproduct", middleware.RoleRequired(utils.RolePengepul), controllers.CreateProduct) } diff --git a/internal/controllers/product.go b/internal/controllers/product.go index fdba795..c0c4548 100644 --- a/internal/controllers/product.go +++ b/internal/controllers/product.go @@ -8,6 +8,15 @@ import ( ) func GetAllProducts(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Unauthorized: user ID is missing", + nil, + )) + } + limit := c.QueryInt("limit", 0) page := c.QueryInt("page", 1) @@ -19,15 +28,7 @@ func GetAllProducts(c *fiber.Ctx) error { )) } - var products []dto.ProductResponseDTO - var err error - - if limit == 0 { - products, err = services.GetProducts(0, 0) - } else { - products, err = services.GetProducts(limit, page) - } - + products, err := services.GetProductsByUserID(userID, limit, page) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse( fiber.StatusInternalServerError, @@ -44,6 +45,15 @@ func GetAllProducts(c *fiber.Ctx) error { } func GetProductByID(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Unauthorized: user ID is missing", + nil, + )) + } + productID := c.Params("productid") if productID == "" { return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( @@ -53,7 +63,7 @@ func GetProductByID(c *fiber.Ctx) error { )) } - product, err := services.GetProductByID(productID) + product, err := services.GetProductByIDAndUserID(productID, userID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse( fiber.StatusNotFound, @@ -68,3 +78,38 @@ func GetProductByID(c *fiber.Ctx) error { product, )) } + +func CreateProduct(c *fiber.Ctx) error { + var input dto.CreateProductRequestDTO + if err := c.BodyParser(&input); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse( + fiber.StatusBadRequest, + "lengkapi data dengan benar", + nil, + )) + } + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Unauthorized: user ID is missing", + nil, + )) + } + + product, err := services.CreateProduct(input, userID) + if err != nil { + return c.Status(fiber.StatusUnprocessableEntity).JSON(utils.FormatResponse( + fiber.StatusUnprocessableEntity, + err.Error(), + nil, + )) + } + + return c.Status(fiber.StatusCreated).JSON(utils.FormatResponse( + fiber.StatusCreated, + "Product created successfully", + product, + )) +} \ No newline at end of file diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 13b44b8..6c957f3 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -12,7 +12,6 @@ import ( func RoleRequired(roles ...string) fiber.Handler { return func(c *fiber.Ctx) error { - tokenString := c.Get("Authorization") if tokenString == "" { return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( @@ -47,7 +46,26 @@ func RoleRequired(roles ...string) fiber.Handler { )) } - role := claims["role"].(string) + userID, ok := claims["sub"].(string) + if !ok || userID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Invalid or missing user ID in token", + nil, + )) + } + + role, ok := claims["role"].(string) + if !ok || role == "" { + return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse( + fiber.StatusUnauthorized, + "Invalid or missing role in token", + nil, + )) + } + + c.Locals("userID", userID) + c.Locals("role", role) for _, r := range roles { if r == role { diff --git a/internal/repositories/product.go b/internal/repositories/product.go index b1c8a58..5edc4e6 100644 --- a/internal/repositories/product.go +++ b/internal/repositories/product.go @@ -3,28 +3,46 @@ package repositories import ( "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/domain" + "gorm.io/gorm" ) -func GetAllProducts(limit, offset int) ([]domain.Product, error) { +func GetProductsByUserID(userID string, limit, offset int) ([]domain.Product, error) { var products []domain.Product + query := config.DB.Preload("ProductImages").Preload("TrashDetail").Where("user_id = ?", userID) - query := config.DB.Preload("ProductImages").Preload("TrashDetail") if limit > 0 { query = query.Limit(limit).Offset(offset) } err := query.Find(&products).Error - if err != nil { - return nil, err - } - return products, nil + return products, err } -func GetProductByID(productID string) (domain.Product, error) { +func GetProductByIDAndUserID(productID, userID string) (domain.Product, error) { var product domain.Product - err := config.DB.Preload("ProductImages").Preload("TrashDetail").Where("id = ?", productID).First(&product).Error - if err != nil { - return domain.Product{}, err - } - return product, nil + err := config.DB.Preload("ProductImages").Preload("TrashDetail"). + Where("id = ? AND user_id = ?", productID, userID). + First(&product).Error + + return product, err +} + +func CreateProduct(product *domain.Product, images []domain.ProductImage) error { + err := config.DB.Transaction(func(tx *gorm.DB) error { + + if err := tx.Create(product).Error; err != nil { + return err + } + + if len(images) > 0 { + for i := range images { + images[i].ProductID = product.ID + } + if err := tx.Create(&images).Error; err != nil { + return err + } + } + return nil + }) + return err } diff --git a/internal/services/product.go b/internal/services/product.go index 810a71d..4192e04 100644 --- a/internal/services/product.go +++ b/internal/services/product.go @@ -1,15 +1,15 @@ package services import ( + "github.com/pahmiudahgede/senggoldong/domain" "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/utils" ) -func GetProducts(limit, page int) ([]dto.ProductResponseDTO, error) { +func GetProductsByUserID(userID string, limit, page int) ([]dto.ProductResponseDTO, error) { offset := (page - 1) * limit - - products, err := repositories.GetAllProducts(limit, offset) + products, err := repositories.GetProductsByUserID(userID, limit, offset) if err != nil { return nil, err } @@ -21,18 +21,16 @@ func GetProducts(limit, page int) ([]dto.ProductResponseDTO, error) { images = append(images, dto.ProductImageDTO{ImageURL: img.ImageURL}) } - trashDetail := dto.TrashDetailResponseDTO{ - ID: product.TrashDetail.ID, - Description: product.TrashDetail.Description, - Price: product.TrashDetail.Price, - } - productResponses = append(productResponses, dto.ProductResponseDTO{ - ID: product.ID, - UserID: product.UserID, - ProductTitle: product.ProductTitle, - ProductImages: images, - TrashDetail: trashDetail, + ID: product.ID, + UserID: product.UserID, + ProductTitle: product.ProductTitle, + ProductImages: images, + TrashDetail: dto.TrashDetailResponseDTO{ + ID: product.TrashDetail.ID, + Description: product.TrashDetail.Description, + Price: product.TrashDetail.Price, + }, SalePrice: product.SalePrice, Quantity: product.Quantity, ProductDescribe: product.ProductDescribe, @@ -45,8 +43,8 @@ func GetProducts(limit, page int) ([]dto.ProductResponseDTO, error) { return productResponses, nil } -func GetProductByID(productID string) (dto.ProductResponseDTO, error) { - product, err := repositories.GetProductByID(productID) +func GetProductByIDAndUserID(productID, userID string) (dto.ProductResponseDTO, error) { + product, err := repositories.GetProductByIDAndUserID(productID, userID) if err != nil { return dto.ProductResponseDTO{}, err } @@ -56,25 +54,56 @@ func GetProductByID(productID string) (dto.ProductResponseDTO, error) { images = append(images, dto.ProductImageDTO{ImageURL: img.ImageURL}) } - trashDetail := dto.TrashDetailResponseDTO{ - ID: product.TrashDetail.ID, - Description: product.TrashDetail.Description, - Price: product.TrashDetail.Price, - } - - productResponse := dto.ProductResponseDTO{ - ID: product.ID, - UserID: product.UserID, - ProductTitle: product.ProductTitle, - ProductImages: images, - TrashDetail: trashDetail, + return dto.ProductResponseDTO{ + ID: product.ID, + UserID: product.UserID, + ProductTitle: product.ProductTitle, + ProductImages: images, + TrashDetail: dto.TrashDetailResponseDTO{ + ID: product.TrashDetail.ID, + Description: product.TrashDetail.Description, + Price: product.TrashDetail.Price, + }, SalePrice: product.SalePrice, Quantity: product.Quantity, ProductDescribe: product.ProductDescribe, Sold: product.Sold, CreatedAt: utils.FormatDateToIndonesianFormat(product.CreatedAt), UpdatedAt: utils.FormatDateToIndonesianFormat(product.UpdatedAt), + }, nil +} + +func CreateProduct(input dto.CreateProductRequestDTO, userID string) (dto.CreateProductResponseDTO, error) { + if err := dto.GetValidator().Struct(input); err != nil { + return dto.CreateProductResponseDTO{}, err } - return productResponse, nil + product := &domain.Product{ + UserID: userID, + ProductTitle: input.ProductTitle, + TrashDetailID: input.TrashDetailID, + SalePrice: input.SalePrice, + Quantity: input.Quantity, + ProductDescribe: input.ProductDescribe, + } + + var images []domain.ProductImage + for _, imageURL := range input.ProductImages { + images = append(images, domain.ProductImage{ImageURL: imageURL}) + } + + if err := repositories.CreateProduct(product, images); err != nil { + return dto.CreateProductResponseDTO{}, err + } + + return dto.CreateProductResponseDTO{ + ID: product.ID, + UserID: product.UserID, + ProductTitle: product.ProductTitle, + ProductImages: input.ProductImages, + TrashDetailID: product.TrashDetailID, + SalePrice: product.SalePrice, + Quantity: product.Quantity, + ProductDescribe: product.ProductDescribe, + }, nil }