From 98e1b81213899539dacffa5ba6cefa9b186be99b Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 22 Feb 2025 14:59:26 +0700 Subject: [PATCH 01/48] feat: add model store and product, migrate to database --- config/database.go | 18 ++++++++++++++++-- model/product_model.go | 22 ++++++++++++++++++++++ model/store_model.go | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 model/product_model.go create mode 100644 model/store_model.go diff --git a/config/database.go b/config/database.go index 5322c65..9e5a624 100644 --- a/config/database.go +++ b/config/database.go @@ -38,17 +38,31 @@ func ConnectDatabase() { &model.Village{}, // ==wilayah indonesia== - // ==main feature== + // ==============main feature============== + // =>user preparation<= &model.User{}, &model.Role{}, &model.UserPin{}, &model.Address{}, + // =>user preparation<= + + // =>store preparation<= + &model.Store{}, + &model.Product{}, + &model.ProductImage{}, + // =>store preparation<= + // ==============main feature============== + + // ==============additional content======== &model.Article{}, &model.Banner{}, &model.InitialCoint{}, + + // =>Trash Model<= &model.TrashCategory{}, &model.TrashDetail{}, - // ==main feature== + // =>Trash Model<= + // ==============additional content======== ) if err != nil { log.Fatalf("Error performing auto-migration: %v", err) diff --git a/model/product_model.go b/model/product_model.go new file mode 100644 index 0000000..48ce531 --- /dev/null +++ b/model/product_model.go @@ -0,0 +1,22 @@ +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"` +} + +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"` +} diff --git a/model/store_model.go b/model/store_model.go new file mode 100644 index 0000000..4b7af9b --- /dev/null +++ b/model/store_model.go @@ -0,0 +1,19 @@ +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"` +} From b144ac7750ce897ac08ca28b1f63281bbcce2d84 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 22 Feb 2025 19:44:03 +0700 Subject: [PATCH 02/48] feat: post store feature --- dto/store_dto.go | 68 +++++++++++++++++++++++++++ internal/handler/store_handler.go | 50 ++++++++++++++++++++ internal/repositories/store_repo.go | 49 +++++++++++++++++++ internal/services/store_service.go | 73 +++++++++++++++++++++++++++++ model/product_model.go | 26 +++++----- model/store_model.go | 27 ++++++----- presentation/store_route.go | 21 +++++++++ router/setup_routes.go.go | 1 + 8 files changed, 289 insertions(+), 26 deletions(-) create mode 100644 dto/store_dto.go create mode 100644 internal/handler/store_handler.go create mode 100644 internal/repositories/store_repo.go create mode 100644 internal/services/store_service.go create mode 100644 presentation/store_route.go 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) } From f22351ffbe0a5b938b353d84c65d5d29667e13ff Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Tue, 25 Feb 2025 10:49:16 +0700 Subject: [PATCH 03/48] refact: upload file statement fixing and refact --- .gitignore | 6 +- dto/store_dto.go | 16 +- internal/handler/store_handler.go | 126 +++++++++++++-- internal/repositories/store_repo.go | 38 +++++ internal/services/store_service.go | 228 +++++++++++++++++++++++++++- presentation/store_route.go | 6 +- 6 files changed, 394 insertions(+), 26 deletions(-) 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) } From 4f586076e7f2d29e65624ede843bbd3b86f20d5c Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 26 Feb 2025 11:34:00 +0700 Subject: [PATCH 04/48] fix: fixing some code and base url refactoring --- .env.example | 3 + .gitignore | 2 +- cmd/main.go | 7 +- dto/product_dto.go | 55 +++++++ internal/handler/product_handler.go | 120 ++++++++++++++ internal/repositories/product_repo.go | 101 ++++++++++++ internal/services/article_service.go | 4 +- internal/services/banner_service.go | 2 +- internal/services/product_service.go | 228 ++++++++++++++++++++++++++ internal/services/store_service.go | 2 +- internal/services/user_service.go | 31 ++-- presentation/product_route.go | 24 +++ router/setup_routes.go.go | 7 +- 13 files changed, 567 insertions(+), 19 deletions(-) create mode 100644 dto/product_dto.go create mode 100644 internal/handler/product_handler.go create mode 100644 internal/repositories/product_repo.go create mode 100644 internal/services/product_service.go create mode 100644 presentation/product_route.go diff --git a/.env.example b/.env.example index 4b313a9..2f50fe6 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +#BASE URL +BASE_URL= + # SERVER SETTINGS SERVER_HOST= SERVER_PORT= diff --git a/.gitignore b/.gitignore index a622dd0..e3d41dc 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ go.work.sum .env.dev # Ignore public uploads -/public/uploads/ \ No newline at end of file +/public/apirijig/v2/uploads/ \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 1c865ce..371c38f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -8,10 +8,11 @@ import ( func main() { config.SetupConfig() - + app := fiber.New() - + // app.Static(utils.BaseUrl+"/uploads", "./public"+utils.BaseUrl+"/uploads") + router.SetupRoutes(app) - config.StartServer(app) + } diff --git a/dto/product_dto.go b/dto/product_dto.go new file mode 100644 index 0000000..66a5a58 --- /dev/null +++ b/dto/product_dto.go @@ -0,0 +1,55 @@ +package dto + +import ( + "mime/multipart" + "regexp" + "strings" +) + +type ResponseProductImageDTO struct { + ID string `json:"id"` + ProductID string `json:"productId"` + ImageURL string `json:"imageURL"` +} + +type ResponseProductDTO struct { + ID string `json:"id"` + StoreID string `json:"storeId"` + ProductName string `json:"productName"` + Quantity int `json:"quantity"` + Saled int `json:"saled"` + ProductImages []ResponseProductImageDTO `json:"productImages,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestProductDTO struct { + ProductName string `json:"product_name"` + Quantity int `json:"quantity"` + ProductImages []*multipart.FileHeader `json:"product_images,omitempty"` +} + +func (r *RequestProductDTO) ValidateProductInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.ProductName) == "" { + errors["product_name"] = append(errors["product_name"], "Product name is required") + } else if len(r.ProductName) < 3 { + errors["product_name"] = append(errors["product_name"], "Product name must be at least 3 characters long") + } else { + validNameRegex := `^[a-zA-Z0-9\s_.-]+$` + if matched, _ := regexp.MatchString(validNameRegex, r.ProductName); !matched { + errors["product_name"] = append(errors["product_name"], "Product name can only contain letters, numbers, spaces, underscores, and dashes") + } + } + + if r.Quantity < 1 { + errors["quantity"] = append(errors["quantity"], "Quantity must be at least 1") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go new file mode 100644 index 0000000..f41b931 --- /dev/null +++ b/internal/handler/product_handler.go @@ -0,0 +1,120 @@ +package handler + +import ( + "fmt" + "log" + "strconv" + + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type ProductHandler struct { + ProductService services.ProductService +} + +func NewProductHandler(productService services.ProductService) *ProductHandler { + return &ProductHandler{ProductService: productService} +} + +func ConvertStringToInt(value string) (int, error) { + convertedValue, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf("invalid integer format: %s", value) + } + return convertedValue, nil +} + +func GetPaginationParams(c *fiber.Ctx) (int, int, error) { + pageStr := c.Query("page", "1") + limitStr := c.Query("limit", "50") + + page, err := strconv.Atoi(pageStr) + if err != nil || page <= 0 { + return 0, 0, fmt.Errorf("invalid page value") + } + + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + return 0, 0, fmt.Errorf("invalid limit value") + } + + return page, limit, nil +} + +func (h *ProductHandler) CreateProduct(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") + } + + productName := c.FormValue("product_name") + quantityStr := c.FormValue("quantity") + productImages, err := c.MultipartForm() + if err != nil { + log.Printf("Error parsing form data: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing form data") + } + + quantity, err := ConvertStringToInt(quantityStr) + if err != nil { + log.Printf("Invalid quantity: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid quantity") + } + + productDTO := dto.RequestProductDTO{ + ProductName: productName, + Quantity: quantity, + ProductImages: productImages.File["product_image"], + } + + product, err := h.ProductService.CreateProduct(userID, &productDTO) + if err != nil { + log.Printf("Error creating product: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) + } + + return utils.CreateResponse(c, product, "Product created successfully") +} + +func (h *ProductHandler) GetAllProductsByStoreID(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") + } + + page, limit, err := GetPaginationParams(c) + if err != nil { + log.Printf("Invalid pagination params: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid pagination parameters") + } + + products, total, err := h.ProductService.GetAllProductsByStoreID(userID, page, limit) + if err != nil { + log.Printf("Error fetching products: %v", err) + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.PaginatedResponse(c, products, page, limit, int(total), "Products fetched successfully") +} + +func (h *ProductHandler) GetProductByID(c *fiber.Ctx) error { + + productID := c.Params("product_id") + if productID == "" { + log.Println("Product ID is required") + return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") + } + + product, err := h.ProductService.GetProductByID(productID) + if err != nil { + log.Printf("Error fetching product: %v", err) + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.SuccessResponse(c, product, "Product fetched successfully") +} diff --git a/internal/repositories/product_repo.go b/internal/repositories/product_repo.go new file mode 100644 index 0000000..66a60e7 --- /dev/null +++ b/internal/repositories/product_repo.go @@ -0,0 +1,101 @@ +package repositories + +import ( + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type ProductRepository interface { + CountProductsByStoreID(storeID string) (int64, error) + CreateProduct(product *model.Product) error + GetProductByID(productID string) (*model.Product, error) + GetProductsByStoreID(storeID string) ([]model.Product, error) + FindProductsByStoreID(storeID string, page, limit int) ([]model.Product, error) + FindProductImagesByProductID(productID string) ([]model.ProductImage, error) + UpdateProduct(product *model.Product) error + DeleteProduct(productID string) error + + AddProductImages(images []model.ProductImage) error + DeleteProductImagesByProductID(productID string) error +} + +type productRepository struct { + DB *gorm.DB +} + +func NewProductRepository(DB *gorm.DB) ProductRepository { + return &productRepository{DB} +} + +func (r *productRepository) CreateProduct(product *model.Product) error { + return r.DB.Create(product).Error +} + +func (r *productRepository) CountProductsByStoreID(storeID string) (int64, error) { + var count int64 + if err := r.DB.Model(&model.Product{}).Where("store_id = ?", storeID).Count(&count).Error; err != nil { + return 0, err + } + return count, nil +} + +func (r *productRepository) GetProductByID(productID string) (*model.Product, error) { + var product model.Product + if err := r.DB.Preload("ProductImages").Where("id = ?", productID).First(&product).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &product, nil +} + +func (r *productRepository) GetProductsByStoreID(storeID string) ([]model.Product, error) { + var products []model.Product + if err := r.DB.Where("store_id = ?", storeID).Preload("ProductImages").Find(&products).Error; err != nil { + return nil, err + } + return products, nil +} + +func (r *productRepository) FindProductsByStoreID(storeID string, page, limit int) ([]model.Product, error) { + var products []model.Product + offset := (page - 1) * limit + + if err := r.DB. + Where("store_id = ?", storeID). + Limit(limit). + Offset(offset). + Find(&products).Error; err != nil { + return nil, err + } + + return products, nil +} + +func (r *productRepository) FindProductImagesByProductID(productID string) ([]model.ProductImage, error) { + var productImages []model.ProductImage + if err := r.DB.Where("product_id = ?", productID).Find(&productImages).Error; err != nil { + return nil, err + } + return productImages, nil +} + +func (r *productRepository) UpdateProduct(product *model.Product) error { + return r.DB.Save(product).Error +} + +func (r *productRepository) DeleteProduct(productID string) error { + return r.DB.Delete(&model.Product{}, "id = ?", productID).Error +} + +func (r *productRepository) AddProductImages(images []model.ProductImage) error { + if len(images) == 0 { + return nil + } + return r.DB.Create(&images).Error +} + +func (r *productRepository) DeleteProductImagesByProductID(productID string) error { + return r.DB.Where("product_id = ?", productID).Delete(&model.ProductImage{}).Error +} diff --git a/internal/services/article_service.go b/internal/services/article_service.go index b8467ab..7d674b8 100644 --- a/internal/services/article_service.go +++ b/internal/services/article_service.go @@ -33,7 +33,7 @@ func NewArticleService(articleRepo repositories.ArticleRepository) ArticleServic func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { - coverImageDir := "./public/uploads/articles" + coverImageDir := "./public" + os.Getenv("BASE_URL") + "/uploads/articles" if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { return nil, fmt.Errorf("failed to create directory for cover image: %v", err) } @@ -338,7 +338,7 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, } func (s *articleService) saveCoverImage(coverImage *multipart.FileHeader, oldImagePath string) (string, error) { - coverImageDir := "./public/uploads/articles" + coverImageDir := "/uploads/articles" if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { return "", fmt.Errorf("failed to create directory for cover image: %v", err) diff --git a/internal/services/banner_service.go b/internal/services/banner_service.go index 9d5b6c1..67a5d7c 100644 --- a/internal/services/banner_service.go +++ b/internal/services/banner_service.go @@ -31,7 +31,7 @@ func NewBannerService(bannerRepo repositories.BannerRepository) BannerService { } func (s *bannerService) saveBannerImage(bannerImage *multipart.FileHeader) (string, error) { - bannerImageDir := "./public/uploads/banners" + bannerImageDir := "./public" + os.Getenv("BASE_URL") + "/uploads/banners" if _, err := os.Stat(bannerImageDir); os.IsNotExist(err) { if err := os.MkdirAll(bannerImageDir, os.ModePerm); err != nil { return "", fmt.Errorf("failed to create directory for banner image: %v", err) diff --git a/internal/services/product_service.go b/internal/services/product_service.go new file mode 100644 index 0000000..c24fa16 --- /dev/null +++ b/internal/services/product_service.go @@ -0,0 +1,228 @@ +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" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type ProductService interface { + SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) + CreateProduct(userID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) + + GetAllProductsByStoreID(userID string, page, limit int) ([]dto.ResponseProductDTO, int64, error) + GetProductByID(productID string) (*dto.ResponseProductDTO, error) +} + +type productService struct { + productRepo repositories.ProductRepository + storeRepo repositories.StoreRepository +} + +func NewProductService(productRepo repositories.ProductRepository, storeRepo repositories.StoreRepository) ProductService { + return &productService{productRepo, storeRepo} +} + +func (s *productService) CreateProduct(userID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, 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 %s", userID) + } + + var imagePaths []string + var productImages []model.ProductImage + for _, file := range productDTO.ProductImages { + imagePath, err := s.SaveProductImage(file, "product") + if err != nil { + return nil, fmt.Errorf("failed to save product image: %w", err) + } + imagePaths = append(imagePaths, imagePath) + + productImages = append(productImages, model.ProductImage{ + ImageURL: imagePath, + }) + } + + if len(imagePaths) == 0 { + return nil, fmt.Errorf("at least one image is required for the product") + } + + product := model.Product{ + StoreID: store.ID, + ProductName: productDTO.ProductName, + Quantity: productDTO.Quantity, + } + + product.ProductImages = productImages + + if err := s.productRepo.CreateProduct(&product); err != nil { + return nil, fmt.Errorf("failed to create product: %w", err) + } + + createdAt, err := utils.FormatDateToIndonesianFormat(product.CreatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format createdAt: %w", err) + } + updatedAt, err := utils.FormatDateToIndonesianFormat(product.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to format updatedAt: %w", err) + } + + var productImagesDTO []dto.ResponseProductImageDTO + for _, img := range product.ProductImages { + productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ + ID: img.ID, + ProductID: img.ProductID, + ImageURL: img.ImageURL, + }) + } + + productDTOResponse := &dto.ResponseProductDTO{ + ID: product.ID, + StoreID: product.StoreID, + ProductName: product.ProductName, + Quantity: product.Quantity, + ProductImages: productImagesDTO, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return productDTOResponse, nil +} + +func (s *productService) GetAllProductsByStoreID(userID string, page, limit int) ([]dto.ResponseProductDTO, int64, error) { + + store, err := s.storeRepo.FindStoreByUserID(userID) + if err != nil { + return nil, 0, fmt.Errorf("error retrieving store by user ID: %w", err) + } + if store == nil { + return nil, 0, fmt.Errorf("store not found for user %s", userID) + } + + total, err := s.productRepo.CountProductsByStoreID(store.ID) + if err != nil { + return nil, 0, fmt.Errorf("error counting products: %w", err) + } + + products, err := s.productRepo.FindProductsByStoreID(store.ID, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("error fetching products: %w", err) + } + + var productDTOs []dto.ResponseProductDTO + for _, product := range products { + productImages, err := s.productRepo.FindProductImagesByProductID(product.ID) + if err != nil { + return nil, 0, fmt.Errorf("error fetching product images: %w", err) + } + + var productImagesDTO []dto.ResponseProductImageDTO + for _, img := range productImages { + productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ + ID: img.ID, + ProductID: img.ProductID, + ImageURL: img.ImageURL, + }) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) + + productDTOs = append(productDTOs, dto.ResponseProductDTO{ + ID: product.ID, + StoreID: product.StoreID, + ProductName: product.ProductName, + Quantity: product.Quantity, + Saled: product.Saled, + ProductImages: productImagesDTO, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + return productDTOs, total, nil +} + +func (s *productService) GetProductByID(productID string) (*dto.ResponseProductDTO, error) { + + product, err := s.productRepo.GetProductByID(productID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve product: %w", err) + } + if product == nil { + return nil, fmt.Errorf("product not found") + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) + + productDTO := &dto.ResponseProductDTO{ + ID: product.ID, + StoreID: product.StoreID, + ProductName: product.ProductName, + Quantity: product.Quantity, + Saled: product.Saled, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + var productImagesDTO []dto.ResponseProductImageDTO + for _, image := range product.ProductImages { + productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ + ID: image.ID, + ProductID: image.ProductID, + ImageURL: image.ImageURL, + }) + } + + productDTO.ProductImages = productImagesDTO + + return productDTO, nil +} + +func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) { + imageDir := fmt.Sprintf("./public%s/uploads/store/%s", os.Getenv("BASE_URL"), 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 +} diff --git a/internal/services/store_service.go b/internal/services/store_service.go index 067a777..43467dd 100644 --- a/internal/services/store_service.go +++ b/internal/services/store_service.go @@ -237,7 +237,7 @@ func (s *storeService) DeleteStore(storeID string) error { func (s *storeService) saveStoreImage(file *multipart.FileHeader, imageType string) (string, error) { - imageDir := fmt.Sprintf("./public/uploads/store/%s", imageType) + imageDir := fmt.Sprintf("./public%s/uploads/store/%s",os.Getenv("BASE_URL"), imageType) if _, err := os.Stat(imageDir); os.IsNotExist(err) { if err := os.MkdirAll(imageDir, os.ModePerm); err != nil { diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 827a099..de79026 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "mime/multipart" "os" "path/filepath" @@ -16,8 +17,6 @@ import ( "golang.org/x/crypto/bcrypt" ) -const avatarDir = "./public/uploads/avatars" - var allowedExtensions = []string{".jpg", ".jpeg", ".png"} type UserProfileService interface { @@ -183,8 +182,13 @@ func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto. } func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) { + baseURL := os.Getenv("BASE_URL") + if baseURL == "" { + return "", fmt.Errorf("BASE_URL is not set in environment variables") + } - if err := ensureAvatarDirectoryExists(); err != nil { + avatarDir := filepath.Join("./public", baseURL, "/uploads/avatars") + if err := ensureAvatarDirectoryExists(avatarDir); err != nil { return "", err } @@ -198,13 +202,19 @@ func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.Fil } if updatedUser.Avatar != nil && *updatedUser.Avatar != "" { - oldAvatarPath := "./public" + *updatedUser.Avatar - if err := os.Remove(oldAvatarPath); err != nil { - return "", fmt.Errorf("failed to remove old avatar: %v", err) + oldAvatarPath := filepath.Join("./public", *updatedUser.Avatar) + if _, err := os.Stat(oldAvatarPath); err == nil { + + if err := os.Remove(oldAvatarPath); err != nil { + return "", fmt.Errorf("failed to remove old avatar: %v", err) + } + } else { + + log.Printf("Old avatar file not found: %s", oldAvatarPath) } } - avatarURL, err := saveAvatarFile(file, userID) + avatarURL, err := saveAvatarFile(file, userID, avatarDir) if err != nil { return "", err } @@ -217,7 +227,7 @@ func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.Fil return "Foto profil berhasil diupdate", nil } -func ensureAvatarDirectoryExists() error { +func ensureAvatarDirectoryExists(avatarDir string) error { if _, err := os.Stat(avatarDir); os.IsNotExist(err) { if err := os.MkdirAll(avatarDir, os.ModePerm); err != nil { return fmt.Errorf("failed to create avatar directory: %v", err) @@ -236,7 +246,7 @@ func validateAvatarFile(file *multipart.FileHeader) error { return fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } -func saveAvatarFile(file *multipart.FileHeader, userID string) (string, error) { +func saveAvatarFile(file *multipart.FileHeader, userID, avatarDir string) (string, error) { extension := filepath.Ext(file.Filename) avatarFileName := fmt.Sprintf("%s_avatar%s", userID, extension) avatarPath := filepath.Join(avatarDir, avatarFileName) @@ -258,5 +268,6 @@ func saveAvatarFile(file *multipart.FileHeader, userID string) (string, error) { return "", fmt.Errorf("failed to save avatar file: %v", err) } - return fmt.Sprintf("/uploads/avatars/%s", avatarFileName), nil + relativePath := filepath.Join("/uploads/avatars", avatarFileName) + return relativePath, nil } diff --git a/presentation/product_route.go b/presentation/product_route.go new file mode 100644 index 0000000..0fa5f8a --- /dev/null +++ b/presentation/product_route.go @@ -0,0 +1,24 @@ +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 ProductRouter(api fiber.Router) { + productRepo := repositories.NewProductRepository(config.DB) + storeRepo := repositories.NewStoreRepository(config.DB) + productService := services.NewProductService(productRepo, storeRepo) + productHandler := handler.NewProductHandler(productService) + + productAPI := api.Group("/productinstore") + + productAPI.Post("/add-product", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.CreateProduct) + productAPI.Get("/getproductbyuser", middleware.AuthMiddleware, productHandler.GetAllProductsByStoreID) + productAPI.Get("getproduct/:product_id", middleware.AuthMiddleware, productHandler.GetProductByID) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 84e9e80..3885718 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -1,13 +1,17 @@ package router import ( + "os" + "github.com/gofiber/fiber/v2" "github.com/pahmiudahgede/senggoldong/middleware" "github.com/pahmiudahgede/senggoldong/presentation" ) func SetupRoutes(app *fiber.App) { - api := app.Group("/apirijikid/v2") + app.Static(os.Getenv("BASE_URL")+"/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") + + api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) presentation.AuthRouter(api) @@ -21,4 +25,5 @@ func SetupRoutes(app *fiber.App) { presentation.InitialCointRoute(api) presentation.TrashRouter(api) presentation.StoreRouter(api) + presentation.ProductRouter(api) } From 3de0d9c101a825565c039c6bcf212fffe840ed59 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 26 Feb 2025 15:03:02 +0700 Subject: [PATCH 05/48] fix: code and statemnt and add update delete products(yet) --- internal/handler/product_handler.go | 106 ++++++++++++++++ internal/repositories/product_repo.go | 39 +++++- internal/services/product_service.go | 176 ++++++++++++++++++++++++++ presentation/product_route.go | 11 ++ 4 files changed, 331 insertions(+), 1 deletion(-) diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go index f41b931..9a84911 100644 --- a/internal/handler/product_handler.go +++ b/internal/handler/product_handler.go @@ -118,3 +118,109 @@ func (h *ProductHandler) GetProductByID(c *fiber.Ctx) error { return utils.SuccessResponse(c, product, "Product fetched successfully") } + +func (h *ProductHandler) UpdateProduct(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") + } + + productID := c.Params("product_id") + if productID == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") + } + + var productDTO dto.RequestProductDTO + if err := c.BodyParser(&productDTO); err != nil { + log.Printf("Error parsing body: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") + } + + productImages, err := c.MultipartForm() + if err != nil { + log.Printf("Error parsing form data: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing form data") + } + + productDTO.ProductImages = productImages.File["product_images"] + + product, err := h.ProductService.UpdateProduct(userID, productID, &productDTO) + if err != nil { + log.Printf("Error updating product: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) + } + + return utils.CreateResponse(c, product, "Product updated successfully") +} + +func (h *ProductHandler) DeleteProduct(c *fiber.Ctx) error { + productID := c.Params("product_id") + if productID == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") + } + + err := h.ProductService.DeleteProduct(productID) + if err != nil { + log.Printf("Error deleting product: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Product deleted successfully") +} + +func (h *ProductHandler) DeleteProducts(c *fiber.Ctx) error { + var productIDs []string + if err := c.BodyParser(&productIDs); err != nil { + log.Printf("Error parsing product IDs: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid input for product IDs") + } + + if len(productIDs) == 0 { + return utils.GenericResponse(c, fiber.StatusBadRequest, "No product IDs provided") + } + + err := h.ProductService.DeleteProducts(productIDs) + if err != nil { + log.Printf("Error deleting products: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete products: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Products deleted successfully") +} + +func (h *ProductHandler) DeleteProductImage(c *fiber.Ctx) error { + imageID := c.Params("image_id") + if imageID == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Image ID is required") + } + + err := h.ProductService.DeleteProductImage(imageID) + if err != nil { + log.Printf("Error deleting product image: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product image: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Product image deleted successfully") +} + +func (h *ProductHandler) DeleteProductImages(c *fiber.Ctx) error { + var imageIDs []string + if err := c.BodyParser(&imageIDs); err != nil { + log.Printf("Error parsing image IDs: %v", err) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid input for image IDs") + } + + if len(imageIDs) == 0 { + return utils.GenericResponse(c, fiber.StatusBadRequest, "No image IDs provided") + } + + err := h.ProductService.DeleteProductImages(imageIDs) + if err != nil { + log.Printf("Error deleting product images: %v", err) + return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product images: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Product images deleted successfully") +} diff --git a/internal/repositories/product_repo.go b/internal/repositories/product_repo.go index 66a60e7..5ab4f56 100644 --- a/internal/repositories/product_repo.go +++ b/internal/repositories/product_repo.go @@ -1,6 +1,8 @@ package repositories import ( + "fmt" + "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) @@ -12,11 +14,14 @@ type ProductRepository interface { GetProductsByStoreID(storeID string) ([]model.Product, error) FindProductsByStoreID(storeID string, page, limit int) ([]model.Product, error) FindProductImagesByProductID(productID string) ([]model.ProductImage, error) + GetProductImageByID(imageID string) (*model.ProductImage, error) UpdateProduct(product *model.Product) error DeleteProduct(productID string) error - + DeleteProductsByID(productIDs []string) error AddProductImages(images []model.ProductImage) error DeleteProductImagesByProductID(productID string) error + DeleteProductImagesByID(imageIDs []string) error + DeleteProductImageByID(imageID string) error } type productRepository struct { @@ -81,6 +86,17 @@ func (r *productRepository) FindProductImagesByProductID(productID string) ([]mo return productImages, nil } +func (r *productRepository) GetProductImageByID(imageID string) (*model.ProductImage, error) { + var productImage model.ProductImage + if err := r.DB.Where("id = ?", imageID).First(&productImage).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &productImage, nil +} + func (r *productRepository) UpdateProduct(product *model.Product) error { return r.DB.Save(product).Error } @@ -89,6 +105,13 @@ func (r *productRepository) DeleteProduct(productID string) error { return r.DB.Delete(&model.Product{}, "id = ?", productID).Error } +func (r *productRepository) DeleteProductsByID(productIDs []string) error { + if err := r.DB.Where("id IN ?", productIDs).Delete(&model.Product{}).Error; err != nil { + return fmt.Errorf("failed to delete products: %v", err) + } + return nil +} + func (r *productRepository) AddProductImages(images []model.ProductImage) error { if len(images) == 0 { return nil @@ -99,3 +122,17 @@ func (r *productRepository) AddProductImages(images []model.ProductImage) error func (r *productRepository) DeleteProductImagesByProductID(productID string) error { return r.DB.Where("product_id = ?", productID).Delete(&model.ProductImage{}).Error } + +func (r *productRepository) DeleteProductImagesByID(imageIDs []string) error { + if err := r.DB.Where("id IN ?", imageIDs).Delete(&model.ProductImage{}).Error; err != nil { + return fmt.Errorf("failed to delete product images: %v", err) + } + return nil +} + +func (r *productRepository) DeleteProductImageByID(imageID string) error { + if err := r.DB.Where("id = ?", imageID).Delete(&model.ProductImage{}).Error; err != nil { + return fmt.Errorf("failed to delete product image: %v", err) + } + return nil +} diff --git a/internal/services/product_service.go b/internal/services/product_service.go index c24fa16..448c6c6 100644 --- a/internal/services/product_service.go +++ b/internal/services/product_service.go @@ -19,6 +19,13 @@ type ProductService interface { GetAllProductsByStoreID(userID string, page, limit int) ([]dto.ResponseProductDTO, int64, error) GetProductByID(productID string) (*dto.ResponseProductDTO, error) + + UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) + + DeleteProduct(productID string) error + DeleteProducts(productIDs []string) error + DeleteProductImages(imageIDs []string) error + DeleteProductImage(imageID string) error } type productService struct { @@ -191,8 +198,135 @@ func (s *productService) GetProductByID(productID string) (*dto.ResponseProductD return productDTO, nil } +func (s *productService) UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, 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 %s", userID) + } + + product, err := s.productRepo.GetProductByID(productID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve product: %v", err) + } + if product == nil { + return nil, fmt.Errorf("product not found") + } + + if product.StoreID != store.ID { + return nil, fmt.Errorf("user does not own the store for this product") + } + + if err := s.deleteProductImages(productID); err != nil { + return nil, fmt.Errorf("failed to delete old product images: %v", err) + } + + var productImages []model.ProductImage + for _, file := range productDTO.ProductImages { + imagePath, err := s.SaveProductImage(file, "product") + if err != nil { + return nil, fmt.Errorf("failed to save product image: %w", err) + } + + productImages = append(productImages, model.ProductImage{ + ImageURL: imagePath, + }) + } + + product.ProductName = productDTO.ProductName + product.Quantity = productDTO.Quantity + product.ProductImages = productImages + + if err := s.productRepo.UpdateProduct(product); err != nil { + return nil, fmt.Errorf("failed to update product: %w", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) + + var productImagesDTO []dto.ResponseProductImageDTO + for _, img := range product.ProductImages { + productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ + ID: img.ID, + ProductID: img.ProductID, + ImageURL: img.ImageURL, + }) + } + + productDTOResponse := &dto.ResponseProductDTO{ + ID: product.ID, + StoreID: product.StoreID, + ProductName: product.ProductName, + Quantity: product.Quantity, + ProductImages: productImagesDTO, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return productDTOResponse, nil +} + +func (s *productService) DeleteProduct(productID string) error { + + if err := s.deleteProductImages(productID); err != nil { + return fmt.Errorf("failed to delete associated product images: %w", err) + } + + if err := s.productRepo.DeleteProduct(productID); err != nil { + return fmt.Errorf("failed to delete product: %w", err) + } + return nil +} + +func (s *productService) DeleteProducts(productIDs []string) error { + + for _, productID := range productIDs { + if err := s.deleteProductImages(productID); err != nil { + return fmt.Errorf("failed to delete associated images for product %s: %w", productID, err) + } + } + + if err := s.productRepo.DeleteProductsByID(productIDs); err != nil { + return fmt.Errorf("failed to delete products: %w", err) + } + + return nil +} + +func (s *productService) DeleteProductImages(imageIDs []string) error { + + for _, imageID := range imageIDs { + if err := s.deleteImageFile(imageID); err != nil { + return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) + } + } + + if err := s.productRepo.DeleteProductImagesByID(imageIDs); err != nil { + return fmt.Errorf("failed to delete product images from database: %w", err) + } + + return nil +} + +func (s *productService) DeleteProductImage(imageID string) error { + + if err := s.deleteImageFile(imageID); err != nil { + return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) + } + + if err := s.productRepo.DeleteProductImageByID(imageID); err != nil { + return fmt.Errorf("failed to delete product image from database: %w", err) + } + + return nil +} + func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) { + imageDir := fmt.Sprintf("./public%s/uploads/store/%s", os.Getenv("BASE_URL"), 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) @@ -226,3 +360,45 @@ func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType return filepath.Join("/uploads/store/", imageType, fileName), nil } + +func (s *productService) deleteProductImages(productID string) error { + + productImages, err := s.productRepo.FindProductImagesByProductID(productID) + if err != nil { + return fmt.Errorf("failed to fetch product images: %w", err) + } + + for _, img := range productImages { + if err := s.deleteImageFile(img.ID); err != nil { + return fmt.Errorf("failed to delete image file: %w", err) + } + } + + if err := s.productRepo.DeleteProductImagesByProductID(productID); err != nil { + return fmt.Errorf("failed to delete product images in database: %w", err) + } + + return nil +} + +func (s *productService) deleteImageFile(imageID string) error { + + productImage, err := s.productRepo.GetProductImageByID(imageID) + if err != nil { + return fmt.Errorf("failed to fetch product image: %w", err) + } + + if productImage == nil { + return fmt.Errorf("product image with ID %s not found", imageID) + } + + extension := filepath.Ext(productImage.ImageURL) + + imagePath := fmt.Sprintf("./public/uploads/store/product/%s%s", imageID, extension) + + if err := os.Remove(imagePath); err != nil { + return fmt.Errorf("failed to delete image file: %w", err) + } + + return nil +} diff --git a/presentation/product_route.go b/presentation/product_route.go index 0fa5f8a..7afab8d 100644 --- a/presentation/product_route.go +++ b/presentation/product_route.go @@ -19,6 +19,17 @@ func ProductRouter(api fiber.Router) { productAPI := api.Group("/productinstore") productAPI.Post("/add-product", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.CreateProduct) + productAPI.Get("/getproductbyuser", middleware.AuthMiddleware, productHandler.GetAllProductsByStoreID) productAPI.Get("getproduct/:product_id", middleware.AuthMiddleware, productHandler.GetProductByID) + + productAPI.Put("updateproduct/:product_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.UpdateProduct) + + productAPI.Delete("/delete/:product_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProduct) + + productAPI.Delete("/delete-products", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProducts) + + productAPI.Delete("/delete-image/:image_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProductImage) + + productAPI.Delete("/delete-images", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProductImages) } From 103b5b0418fc570b473811249161328a22815684 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 27 Feb 2025 13:57:23 +0700 Subject: [PATCH 06/48] fix: fixing delete method in product --- internal/services/product_service.go | 123 +++++++++++++-------------- 1 file changed, 61 insertions(+), 62 deletions(-) diff --git a/internal/services/product_service.go b/internal/services/product_service.go index 448c6c6..bc6eb23 100644 --- a/internal/services/product_service.go +++ b/internal/services/product_service.go @@ -21,11 +21,11 @@ type ProductService interface { GetProductByID(productID string) (*dto.ResponseProductDTO, error) UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) - DeleteProduct(productID string) error DeleteProducts(productIDs []string) error - DeleteProductImages(imageIDs []string) error DeleteProductImage(imageID string) error + DeleteProductImages(imageIDs []string) error + deleteImageFile(imageID string) error } type productService struct { @@ -199,6 +199,7 @@ func (s *productService) GetProductByID(productID string) (*dto.ResponseProductD } func (s *productService) UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) { + store, err := s.storeRepo.FindStoreByUserID(userID) if err != nil { return nil, fmt.Errorf("error retrieving store by user ID: %w", err) @@ -268,61 +269,6 @@ func (s *productService) UpdateProduct(userID, productID string, productDTO *dto return productDTOResponse, nil } -func (s *productService) DeleteProduct(productID string) error { - - if err := s.deleteProductImages(productID); err != nil { - return fmt.Errorf("failed to delete associated product images: %w", err) - } - - if err := s.productRepo.DeleteProduct(productID); err != nil { - return fmt.Errorf("failed to delete product: %w", err) - } - return nil -} - -func (s *productService) DeleteProducts(productIDs []string) error { - - for _, productID := range productIDs { - if err := s.deleteProductImages(productID); err != nil { - return fmt.Errorf("failed to delete associated images for product %s: %w", productID, err) - } - } - - if err := s.productRepo.DeleteProductsByID(productIDs); err != nil { - return fmt.Errorf("failed to delete products: %w", err) - } - - return nil -} - -func (s *productService) DeleteProductImages(imageIDs []string) error { - - for _, imageID := range imageIDs { - if err := s.deleteImageFile(imageID); err != nil { - return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) - } - } - - if err := s.productRepo.DeleteProductImagesByID(imageIDs); err != nil { - return fmt.Errorf("failed to delete product images from database: %w", err) - } - - return nil -} - -func (s *productService) DeleteProductImage(imageID string) error { - - if err := s.deleteImageFile(imageID); err != nil { - return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) - } - - if err := s.productRepo.DeleteProductImageByID(imageID); err != nil { - return fmt.Errorf("failed to delete product image from database: %w", err) - } - - return nil -} - func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) { imageDir := fmt.Sprintf("./public%s/uploads/store/%s", os.Getenv("BASE_URL"), imageType) @@ -361,8 +307,62 @@ func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType return filepath.Join("/uploads/store/", imageType, fileName), nil } -func (s *productService) deleteProductImages(productID string) error { +func (s *productService) DeleteProduct(productID string) error { + if err := s.deleteProductImages(productID); err != nil { + return fmt.Errorf("failed to delete associated product images: %w", err) + } + + if err := s.productRepo.DeleteProduct(productID); err != nil { + return fmt.Errorf("failed to delete product: %w", err) + } + return nil +} + +func (s *productService) DeleteProducts(productIDs []string) error { + + for _, productID := range productIDs { + if err := s.deleteProductImages(productID); err != nil { + return fmt.Errorf("failed to delete associated images for product %s: %w", productID, err) + } + } + + if err := s.productRepo.DeleteProductsByID(productIDs); err != nil { + return fmt.Errorf("failed to delete products: %w", err) + } + + return nil +} + +func (s *productService) DeleteProductImage(imageID string) error { + + if err := s.deleteImageFile(imageID); err != nil { + return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) + } + + if err := s.productRepo.DeleteProductImageByID(imageID); err != nil { + return fmt.Errorf("failed to delete product image from database: %w", err) + } + + return nil +} + +func (s *productService) DeleteProductImages(imageIDs []string) error { + + for _, imageID := range imageIDs { + if err := s.deleteImageFile(imageID); err != nil { + return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) + } + } + + if err := s.productRepo.DeleteProductImagesByID(imageIDs); err != nil { + return fmt.Errorf("failed to delete product images from database: %w", err) + } + + return nil +} + +func (s *productService) deleteProductImages(productID string) error { productImages, err := s.productRepo.FindProductImagesByProductID(productID) if err != nil { return fmt.Errorf("failed to fetch product images: %w", err) @@ -375,14 +375,13 @@ func (s *productService) deleteProductImages(productID string) error { } if err := s.productRepo.DeleteProductImagesByProductID(productID); err != nil { - return fmt.Errorf("failed to delete product images in database: %w", err) + return fmt.Errorf("failed to delete product images from database: %w", err) } return nil } func (s *productService) deleteImageFile(imageID string) error { - productImage, err := s.productRepo.GetProductImageByID(imageID) if err != nil { return fmt.Errorf("failed to fetch product image: %w", err) @@ -392,9 +391,9 @@ func (s *productService) deleteImageFile(imageID string) error { return fmt.Errorf("product image with ID %s not found", imageID) } - extension := filepath.Ext(productImage.ImageURL) + baseURL := os.Getenv("BASE_URL") - imagePath := fmt.Sprintf("./public/uploads/store/product/%s%s", imageID, extension) + imagePath := fmt.Sprintf("./public%s%s", baseURL, productImage.ImageURL) if err := os.Remove(imagePath); err != nil { return fmt.Errorf("failed to delete image file: %w", err) From 2072d047d49e3222cee407a556f09ef5c63ac596 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 7 Mar 2025 23:17:31 +0700 Subject: [PATCH 07/48] cors management and role showing in public access --- cmd/main.go | 32 +++++++++++++++++++++++++++----- presentation/role_route.go | 6 ++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 371c38f..1792938 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,17 +2,39 @@ package main import ( "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/router" ) func main() { config.SetupConfig() - app := fiber.New() - // app.Static(utils.BaseUrl+"/uploads", "./public"+utils.BaseUrl+"/uploads") - - router.SetupRoutes(app) - config.StartServer(app) + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:3000", + AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", + AllowCredentials: true, + })) + + app.Use(func(c *fiber.Ctx) error { + c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + c.Set("Access-Control-Allow-Credentials", "true") + return c.Next() + }) + + app.Options("*", func(c *fiber.Ctx) error { + c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + c.Set("Access-Control-Allow-Credentials", "true") + return c.SendStatus(fiber.StatusNoContent) + }) + + router.SetupRoutes(app) + + config.StartServer(app) } diff --git a/presentation/role_route.go b/presentation/role_route.go index d4652a1..1895e7f 100644 --- a/presentation/role_route.go +++ b/presentation/role_route.go @@ -6,8 +6,6 @@ import ( "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 RoleRouter(api fiber.Router) { @@ -15,6 +13,6 @@ func RoleRouter(api fiber.Router) { roleService := services.NewRoleService(roleRepo) roleHandler := handler.NewRoleHandler(roleService) - api.Get("/roles", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), roleHandler.GetRoles) - api.Get("/role/:role_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), roleHandler.GetRoleByID) + api.Get("/roles", roleHandler.GetRoles) + api.Get("/role/:role_id", roleHandler.GetRoleByID) } From 7d2a02c65f8d40b1240c72b34f9f172d07fb02b3 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 7 Mar 2025 23:20:38 +0700 Subject: [PATCH 08/48] refact: public access get roles --- internal/handler/role_handler.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go index 623b979..2141ed5 100644 --- a/internal/handler/role_handler.go +++ b/internal/handler/role_handler.go @@ -16,10 +16,10 @@ func NewRoleHandler(roleService services.RoleService) *RoleHandler { func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { - roleID, ok := c.Locals("roleID").(string) - if !ok || roleID != utils.RoleAdministrator { - return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") - } + // roleID, ok := c.Locals("roleID").(string) + // if !ok || roleID != utils.RoleAdministrator { + // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") + // } roles, err := h.RoleService.GetRoles() if err != nil { @@ -32,10 +32,10 @@ func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { roleID := c.Params("role_id") - roleIDFromSession, ok := c.Locals("roleID").(string) - if !ok || roleIDFromSession != utils.RoleAdministrator { - return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") - } + // roleIDFromSession, ok := c.Locals("roleID").(string) + // if !ok || roleIDFromSession != utils.RoleAdministrator { + // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") + // } role, err := h.RoleService.GetRoleByID(roleID) if err != nil { From cc347fed7dbee3d6428bdf97183324d783f2cda6 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 14 Mar 2025 02:55:34 +0700 Subject: [PATCH 09/48] feat: user admin akses job and add dep midtrans --- go.mod | 1 + go.sum | 2 ++ internal/handler/user_handler.go | 43 ++++++++++++++++++++++++ internal/repositories/user_repo.go | 21 ++++++++++++ internal/services/user_service.go | 53 ++++++++++++++++++++++++++++++ presentation/user_route.go | 15 ++++++--- 6 files changed, 131 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 634b41e..235205c 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/midtrans/midtrans-go v1.3.8 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/go.sum b/go.sum index cf521f9..3073dd4 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/midtrans/midtrans-go v1.3.8 h1:r6eq51LJwbMQ05dBF3Twg99u45G3pLxP5INYoqOoNzU= +github.com/midtrans/midtrans-go v1.3.8/go.mod h1:5hN2oiZDP3/SwSBxHPTg8eC/RVoRE9DXQOY1Ah9au10= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 158fab8..c4f60d5 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -30,6 +30,49 @@ func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") } + +func (h *UserProfileHandler) GetUserProfileById(c *fiber.Ctx) error { + userID := c.Params("userid") + if userID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{"userid": {"user ID is required"}}) + } + + // userID, ok := c.Locals("userID").(string) + // if !ok || userID == "" { + // return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + // } + + userProfile, err := h.UserProfileService.GetUserProfile(userID) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") +} + +func (h *UserProfileHandler) GetAllUsers(c *fiber.Ctx) error { + users, err := h.UserProfileService.GetAllUsers() + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, users, "All users retrieved successfully") +} + +func (h *UserProfileHandler) GetUsersByRoleID(c *fiber.Ctx) error { + roleID := c.Params("roleid") + if roleID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{"roleId": {"Role ID is required"}}) + } + + users, err := h.UserProfileService.GetUsersByRoleID(roleID) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, users, "Users retrieved successfully") +} + func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { var updateData dto.UpdateUserDTO if err := c.BodyParser(&updateData); err != nil { diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go index 604d2db..3279377 100644 --- a/internal/repositories/user_repo.go +++ b/internal/repositories/user_repo.go @@ -11,6 +11,9 @@ type UserProfileRepository interface { FindByID(userID string) (*model.User, error) Update(user *model.User) error UpdateAvatar(userID, avatarURL string) error + + FindAll() ([]model.User, error) + FindByRoleID(roleID string) ([]model.User, error) } type userProfileRepository struct { @@ -54,3 +57,21 @@ func (r *userProfileRepository) UpdateAvatar(userID, avatarURL string) error { } return nil } + +func (r *userProfileRepository) FindAll() ([]model.User, error) { + var users []model.User + err := r.DB.Preload("Role").Find(&users).Error + if err != nil { + return nil, err + } + return users, nil +} + +func (r *userProfileRepository) FindByRoleID(roleID string) ([]model.User, error) { + var users []model.User + err := r.DB.Preload("Role").Where("role_id = ?", roleID).Find(&users).Error + if err != nil { + return nil, err + } + return users, nil +} diff --git a/internal/services/user_service.go b/internal/services/user_service.go index de79026..3b8b092 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -24,6 +24,9 @@ type UserProfileService interface { UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) + + GetAllUsers() ([]dto.UserResponseDTO, error) + GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) } type userProfileService struct { @@ -87,6 +90,56 @@ func (s *userProfileService) GetUserProfile(userID string) (*dto.UserResponseDTO return userResponse, nil } +func (s *userProfileService) GetAllUsers() ([]dto.UserResponseDTO, error) { + users, err := s.UserProfileRepo.FindAll() + if err != nil { + return nil, err + } + + var response []dto.UserResponseDTO + for _, user := range users { + response = append(response, dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: user.Role.RoleName, + CreatedAt: user.CreatedAt.Format(time.RFC3339), + UpdatedAt: user.UpdatedAt.Format(time.RFC3339), + }) + } + + return response, nil +} + +func (s *userProfileService) GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) { + users, err := s.UserProfileRepo.FindByRoleID(roleID) + if err != nil { + return nil, err + } + + var response []dto.UserResponseDTO + for _, user := range users { + response = append(response, dto.UserResponseDTO{ + ID: user.ID, + Username: user.Username, + Avatar: user.Avatar, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + EmailVerified: user.EmailVerified, + RoleName: user.Role.RoleName, + CreatedAt: user.CreatedAt.Format(time.RFC3339), + UpdatedAt: user.UpdatedAt.Format(time.RFC3339), + }) + } + + return response, nil +} + func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) { user, err := s.UserProfileRepo.FindByID(userID) if err != nil { diff --git a/presentation/user_route.go b/presentation/user_route.go index d847432..dcdb00c 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -14,8 +14,15 @@ func UserProfileRouter(api fiber.Router) { userProfileService := services.NewUserProfileService(userProfileRepo) userProfileHandler := handler.NewUserProfileHandler(userProfileService) - api.Get("/user", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) - api.Put("/user/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) - api.Patch("/user/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) - api.Patch("/user/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) + userProfilRoute := api.Group("/user") + + userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) + + userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsers) + userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) + userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) + + userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) + userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) + userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) } From ffd71d4fc6d6a816236516dc7fc4c93e4dedd827 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 15 Mar 2025 03:20:37 +0700 Subject: [PATCH 10/48] mod tidy --- go.mod | 27 ++++++++++++--------------- go.sum | 37 +++++++++++++++++++------------------ 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 235205c..ff6e89a 100644 --- a/go.mod +++ b/go.mod @@ -2,41 +2,38 @@ module github.com/pahmiudahgede/senggoldong go 1.23.3 +require ( + github.com/go-redis/redis/v8 v8.11.5 + github.com/gofiber/fiber/v2 v2.52.5 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/google/uuid v1.5.0 + github.com/joho/godotenv v1.5.1 + golang.org/x/crypto v0.19.0 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 +) + require ( github.com/andybalholm/brotli v1.0.5 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.23.0 // indirect - github.com/go-redis/redis/v8 v8.11.5 // indirect - github.com/gofiber/fiber/v2 v2.52.5 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/google/uuid v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/klauspost/compress v1.17.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/lucsky/cuid v1.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/midtrans/midtrans-go v1.3.8 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.9.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.20.0 // indirect - gorm.io/driver/postgres v1.5.11 // indirect - gorm.io/gorm v1.25.12 // indirect ) diff --git a/go.sum b/go.sum index 3073dd4..e2d87b7 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,12 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= @@ -37,10 +33,6 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lucsky/cuid v1.2.1 h1:MtJrL2OFhvYufUIn48d35QGXyeTC8tn0upumW9WwTHg= -github.com/lucsky/cuid v1.2.1/go.mod h1:QaaJqckboimOmhRSJXSx/+IT+VTfxfPGSo/6mfgUfmE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -48,22 +40,27 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/midtrans/midtrans-go v1.3.8 h1:r6eq51LJwbMQ05dBF3Twg99u45G3pLxP5INYoqOoNzU= -github.com/midtrans/midtrans-go v1.3.8/go.mod h1:5hN2oiZDP3/SwSBxHPTg8eC/RVoRE9DXQOY1Ah9au10= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= @@ -72,14 +69,18 @@ golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= From 21d4f28cef9023d7e032d8f7cc994611a0bb8e36 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 19 Mar 2025 23:25:19 +0700 Subject: [PATCH 11/48] refact: register now can handle otp from phone --- config/server.go | 4 + config/setup_config.go | 1 + config/whatsapp.go | 111 ++++++++++++++ dto/auth_dto.go | 234 ++++++++++++++++++----------- dto/user_dto.go | 10 +- go.mod | 27 +++- go.sum | 60 ++++++-- internal/handler/auth_handler.go | 96 +++++++----- internal/handler/user_handler.go | 38 ++--- internal/repositories/auth_repo.go | 54 ++----- internal/services/auth_service.go | 225 ++++++++++++--------------- internal/services/user_service.go | 76 +++++----- presentation/auth_route.go | 39 +++-- presentation/user_route.go | 2 +- utils/redis_caching.go | 41 +++++ 15 files changed, 617 insertions(+), 401 deletions(-) create mode 100644 config/whatsapp.go diff --git a/config/server.go b/config/server.go index 8fc1592..10caf70 100644 --- a/config/server.go +++ b/config/server.go @@ -8,6 +8,10 @@ import ( "github.com/gofiber/fiber/v2" ) +func GetSecretKey() string { + return os.Getenv("SECRET_KEY") +} + func StartServer(app *fiber.App) { host := os.Getenv("SERVER_HOST") port := os.Getenv("SERVER_PORT") diff --git a/config/setup_config.go b/config/setup_config.go index e6679a2..b302514 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -14,4 +14,5 @@ func SetupConfig() { ConnectDatabase() ConnectRedis() + InitWhatsApp() } diff --git a/config/whatsapp.go b/config/whatsapp.go new file mode 100644 index 0000000..18d073c --- /dev/null +++ b/config/whatsapp.go @@ -0,0 +1,111 @@ +package config + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + _ "github.com/lib/pq" + "github.com/mdp/qrterminal/v3" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/store/sqlstore" + "go.mau.fi/whatsmeow/types" + waLog "go.mau.fi/whatsmeow/util/log" + "google.golang.org/protobuf/proto" +) + +var WhatsAppClient *whatsmeow.Client +var container *sqlstore.Container + +func InitWhatsApp() { + dbLog := waLog.Stdout("Database", "DEBUG", true) + + dsn := fmt.Sprintf( + "postgres://%s:%s@%s:%s/%s?sslmode=disable", + os.Getenv("DB_USER"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"), + os.Getenv("DB_NAME"), + ) + + var err error + container, err = sqlstore.New("postgres", dsn, dbLog) + if err != nil { + log.Fatalf("Failed to connect to WhatsApp database: %v", err) + } + + deviceStore, err := container.GetFirstDevice() + if err != nil { + log.Fatalf("Failed to get WhatsApp device: %v", err) + } + + clientLog := waLog.Stdout("Client", "DEBUG", true) + WhatsAppClient = whatsmeow.NewClient(deviceStore, clientLog) + + if WhatsAppClient.Store.ID == nil { + fmt.Println("WhatsApp Client is not logged in, generating QR Code...") + + qrChan, _ := WhatsAppClient.GetQRChannel(context.Background()) + err = WhatsAppClient.Connect() + if err != nil { + log.Fatalf("Failed to connect WhatsApp client: %v", err) + } + + for evt := range qrChan { + if evt.Event == "code" { + fmt.Println("QR Code untuk login:") + generateQRCode(evt.Code) + } else { + fmt.Println("Login event:", evt.Event) + } + } + } else { + fmt.Println("WhatsApp Client sudah login, langsung terhubung...") + err = WhatsAppClient.Connect() + if err != nil { + log.Fatalf("Failed to connect WhatsApp client: %v", err) + } + } + + log.Println("WhatsApp client connected successfully!") + go handleShutdown() +} + +func generateQRCode(qrString string) { + qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) +} + + +func handleShutdown() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + + log.Println("Shutting down WhatsApp client...") + WhatsAppClient.Disconnect() + os.Exit(0) +} + +func SendWhatsAppMessage(phone, message string) error { + if WhatsAppClient == nil { + return fmt.Errorf("WhatsApp client is not initialized") + } + + targetJID, _ := types.ParseJID(phone + "@s.whatsapp.net") + msg := waE2E.Message{ + Conversation: proto.String(message), + } + + _, err := WhatsAppClient.SendMessage(context.Background(), targetJID, &msg) + if err != nil { + return fmt.Errorf("failed to send WhatsApp message: %v", err) + } + + log.Printf("WhatsApp message sent successfully to: %s", phone) + return nil +} diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 5a7e2ea..6232f8a 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -5,39 +5,46 @@ import ( "strings" ) -type LoginDTO struct { - RoleID string `json:"roleid"` - Identifier string `json:"identifier"` - Password string `json:"password"` +type RegisterRequest struct { + RoleID string `json:"role_id"` + Phone string `json:"phone"` } -type UserResponseWithToken struct { +type VerifyOTPRequest struct { + Phone string `json:"phone"` + OTP string `json:"otp"` +} + +type MetaResponse struct { + Status int `json:"status"` + Message string `json:"message"` +} + +// UserDataResponse untuk bagian data +type UserDataResponse struct { UserID string `json:"user_id"` - RoleName string `json:"role_name"` + UserRole string `json:"user_role"` Token string `json:"token"` } -type RegisterDTO struct { - Username string `json:"username"` - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` - Password string `json:"password"` - ConfirmPassword string `json:"confirm_password"` - RoleID string `json:"roleId,omitempty"` +// Response struct utama +type Response struct { + Meta MetaResponse `json:"meta"` + Data *UserDataResponse `json:"data,omitempty"` // Gunakan pointer agar bisa bernilai nil jika tidak diperlukan } -func (l *LoginDTO) Validate() (map[string][]string, bool) { +func (l *RegisterRequest) Validate() (map[string][]string, bool) { errors := make(map[string][]string) + // Validasi RoleID dan Phone if strings.TrimSpace(l.RoleID) == "" { errors["roleid"] = append(errors["roleid"], "Role ID is required") } - if strings.TrimSpace(l.Identifier) == "" { - errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required") - } - if strings.TrimSpace(l.Password) == "" { - errors["password"] = append(errors["password"], "Password is required") + + if strings.TrimSpace(l.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone is required") + } else if !IsValidPhoneNumber(l.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") } if len(errors) > 0 { @@ -46,76 +53,131 @@ func (l *LoginDTO) Validate() (map[string][]string, bool) { return nil, true } -func (r *RegisterDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - r.validateRequiredFields(errors) - - if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") - } - - if r.Email != "" && !IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } - - if r.Password != "" && !IsValidPassword(r.Password) { - errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") - } - - if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { - errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { - - if strings.TrimSpace(r.Username) == "" { - errors["username"] = append(errors["username"], "Username is required") - } - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "Name is required") - } - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } - if strings.TrimSpace(r.Password) == "" { - errors["password"] = append(errors["password"], "Password is required") - } - if strings.TrimSpace(r.ConfirmPassword) == "" { - errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") - } - if strings.TrimSpace(r.RoleID) == "" { - errors["roleId"] = append(errors["roleId"], "RoleID is required") - } -} - +// IsValidPhoneNumber untuk validasi format nomor telepon func IsValidPhoneNumber(phone string) bool { - - re := regexp.MustCompile(`^\+62\d{9,13}$`) + // Validasi format nomor telepon harus dimulai dengan 62 dan 9-13 digit setelahnya + re := regexp.MustCompile(`^62\d{9,13}$`) return re.MatchString(phone) } -func IsValidEmail(email string) bool { +// package dto - re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return re.MatchString(email) -} +// import ( +// "regexp" +// "strings" +// ) -func IsValidPassword(password string) bool { - if len(password) < 8 { - return false - } +// type LoginDTO struct { +// RoleID string `json:"roleid"` +// Identifier string `json:"identifier"` +// Password string `json:"password"` +// } - re := regexp.MustCompile(`\d`) - return re.MatchString(password) -} +// type UserResponseWithToken struct { +// UserID string `json:"user_id"` +// RoleName string `json:"role_name"` +// Token string `json:"token"` +// } + +// type RegisterDTO struct { +// Username string `json:"username"` +// Name string `json:"name"` +// Phone string `json:"phone"` +// Email string `json:"email"` +// Password string `json:"password"` +// ConfirmPassword string `json:"confirm_password"` +// RoleID string `json:"roleId,omitempty"` +// } + +// func (l *LoginDTO) Validate() (map[string][]string, bool) { +// errors := make(map[string][]string) + +// if strings.TrimSpace(l.RoleID) == "" { +// errors["roleid"] = append(errors["roleid"], "Role ID is required") +// } +// if strings.TrimSpace(l.Identifier) == "" { +// errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required") +// } +// if strings.TrimSpace(l.Password) == "" { +// errors["password"] = append(errors["password"], "Password is required") +// } + +// if len(errors) > 0 { +// return errors, false +// } +// return nil, true +// } + +// func (r *RegisterDTO) Validate() (map[string][]string, bool) { +// errors := make(map[string][]string) + +// r.validateRequiredFields(errors) + +// if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { +// errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") +// } + +// if r.Email != "" && !IsValidEmail(r.Email) { +// errors["email"] = append(errors["email"], "Invalid email format") +// } + +// if r.Password != "" && !IsValidPassword(r.Password) { +// errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") +// } + +// if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { +// errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") +// } + +// if len(errors) > 0 { +// return errors, false +// } + +// return nil, true +// } + +// func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { + +// if strings.TrimSpace(r.Username) == "" { +// errors["username"] = append(errors["username"], "Username is required") +// } +// if strings.TrimSpace(r.Name) == "" { +// errors["name"] = append(errors["name"], "Name is required") +// } +// if strings.TrimSpace(r.Phone) == "" { +// errors["phone"] = append(errors["phone"], "Phone number is required") +// } +// if strings.TrimSpace(r.Email) == "" { +// errors["email"] = append(errors["email"], "Email is required") +// } +// if strings.TrimSpace(r.Password) == "" { +// errors["password"] = append(errors["password"], "Password is required") +// } +// if strings.TrimSpace(r.ConfirmPassword) == "" { +// errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") +// } +// if strings.TrimSpace(r.RoleID) == "" { +// errors["roleId"] = append(errors["roleId"], "RoleID is required") +// } +// } + +// func IsValidPhoneNumber(phone string) bool { + +// re := regexp.MustCompile(`^\+62\d{9,13}$`) +// return re.MatchString(phone) +// } + +// func IsValidEmail(email string) bool { + +// re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) +// return re.MatchString(email) +// } + +// func IsValidPassword(password string) bool { +// if len(password) < 8 { +// return false +// } + +// re := regexp.MustCompile(`\d`) +// return re.MatchString(password) +// } diff --git a/dto/user_dto.go b/dto/user_dto.go index cb2dc38..b527605 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -37,11 +37,11 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } else if !IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } + // if strings.TrimSpace(r.Email) == "" { + // errors["email"] = append(errors["email"], "Email is required") + // } else if !IsValidEmail(r.Email) { + // errors["email"] = append(errors["email"], "Invalid email format") + // } if len(errors) > 0 { return errors, false diff --git a/go.mod b/go.mod index ff6e89a..99bc202 100644 --- a/go.mod +++ b/go.mod @@ -6,17 +6,24 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/fiber/v2 v2.52.5 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - golang.org/x/crypto v0.19.0 + golang.org/x/crypto v0.36.0 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 ) require ( + golang.org/x/term v0.30.0 // indirect + rsc.io/qr v0.2.0 // indirect +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect @@ -24,16 +31,22 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/klauspost/compress v1.17.0 // indirect + github.com/lib/pq v1.10.9 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.20.0 // indirect + go.mau.fi/libsignal v0.1.2 // indirect + go.mau.fi/util v0.8.6 // indirect + go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175 + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.5 ) diff --git a/go.sum b/go.sum index e2d87b7..83b4326 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,10 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -11,12 +14,17 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -33,46 +41,68 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0= +go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE= +go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54= +go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE= +go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175 h1:BDShdc10qJzi3B0xPGA6HVQl+929wIFst8/W+8EnvbI= +go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -85,3 +115,5 @@ gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 3f48822..50edbe0 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,72 +1,88 @@ 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 UserHandler struct { - UserService services.UserService +type AuthHandler struct { + AuthService services.AuthService } -func NewUserHandler(userService services.UserService) *UserHandler { - return &UserHandler{UserService: userService} +func NewAuthHandler(authService services.AuthService) *AuthHandler { + return &AuthHandler{AuthService: authService} } -func (h *UserHandler) Login(c *fiber.Ctx) error { - var loginDTO dto.LoginDTO - if err := c.BodyParser(&loginDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +func (h *AuthHandler) Register(c *fiber.Ctx) error { + var request dto.RegisterRequest + + if err := c.BodyParser(&request); err != nil { + return c.Status(400).SendString("Invalid input") } - validationErrors, valid := loginDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, validationErrors) + if errors, valid := request.Validate(); !valid { + return c.Status(400).JSON(errors) } - user, err := h.UserService.Login(loginDTO) + _, err := h.AuthService.RegisterUser(request) if err != nil { - return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) + return c.Status(500).SendString(err.Error()) } - return utils.SuccessResponse(c, user, "Login successful") + return c.Status(201).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": 201, + "message": "The input register from the user has been successfully recorded. Please check the otp code sent to your number.", + }, + }) } -func (h *UserHandler) Register(c *fiber.Ctx) error { - - var registerDTO dto.RegisterDTO - if err := c.BodyParser(®isterDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) +func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { + var request struct { + Phone string `json:"phone"` + OTP string `json:"otp"` } - errors, valid := registerDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) + if err := c.BodyParser(&request); err != nil { + return c.Status(400).SendString("Invalid input") } - userResponse, err := h.UserService.Register(registerDTO) + err := h.AuthService.VerifyOTP(request.Phone, request.OTP) if err != nil { - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) + return c.Status(400).JSON(dto.Response{ + Meta: dto.MetaResponse{ + Status: 400, + Message: "Invalid OTP", + }, + Data: nil, + }) } - return utils.CreateResponse(c, userResponse, "Registration successful") -} - -func (h *UserHandler) Logout(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - log.Println("Unauthorized access: User ID not found in session") - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - err := utils.DeleteSessionData(userID) + user, err := h.AuthService.GetUserByPhone(request.Phone) if err != nil { - return utils.InternalServerErrorResponse(c, "Error logging out") + return c.Status(500).SendString("Error retrieving user") + } + if user == nil { + return c.Status(404).SendString("User not found") } - return utils.SuccessResponse(c, nil, "Logout successful") + token, err := h.AuthService.GenerateJWT(user) + if err != nil { + return c.Status(500).SendString("Error generating token") + } + + response := dto.Response{ + Meta: dto.MetaResponse{ + Status: 200, + Message: "OTP yang dimasukkan valid", + }, + Data: &dto.UserDataResponse{ + UserID: user.ID, + UserRole: user.Role.RoleName, + Token: token, + }, + } + + return c.Status(200).JSON(response) } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index c4f60d5..f8e0d73 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -97,29 +97,29 @@ func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { return utils.SuccessResponse(c, userResponse, "User profile updated successfully") } -func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { - var passwordData dto.UpdatePasswordDTO - if err := c.BodyParser(&passwordData); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } +// func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { +// var passwordData dto.UpdatePasswordDTO +// if err := c.BodyParser(&passwordData); err != nil { +// return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +// } - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } +// userID, ok := c.Locals("userID").(string) +// if !ok || userID == "" { +// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") +// } - errors, valid := passwordData.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } +// errors, valid := passwordData.Validate() +// if !valid { +// return utils.ValidationErrorResponse(c, errors) +// } - message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) - } +// message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) +// if err != nil { +// return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) +// } - return utils.GenericResponse(c, fiber.StatusOK, message) -} +// return utils.GenericResponse(c, fiber.StatusOK, message) +// } func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(string) diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 2b0622f..5ffe64d 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,20 +1,14 @@ package repositories import ( - "fmt" - "github.com/pahmiudahgede/senggoldong/model" "gorm.io/gorm" ) type UserRepository interface { - FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) - FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) - FindByUsername(username string) (*model.User, error) + FindByPhone(phone string) (*model.User, error) FindByPhoneAndRole(phone, roleID string) (*model.User, error) - FindByEmailAndRole(email, roleID string) (*model.User, error) - - Create(user *model.User) error + CreateUser(user *model.User) error } type userRepository struct { @@ -25,22 +19,14 @@ func NewUserRepository(db *gorm.DB) UserRepository { return &userRepository{DB: db} } -func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) { +func (r *userRepository) FindByPhone(phone string) (*model.User, error) { var user model.User - err := r.DB.Preload("Role").Where("(email = ? OR username = ? OR phone = ?) AND role_id = ?", identifier, identifier, identifier, roleID).First(&user).Error - if err != nil { - return nil, err - } - if user.Role == nil { - return nil, fmt.Errorf("role not found for this user") - } - return &user, nil -} -func (r *userRepository) FindByUsername(username string) (*model.User, error) { - var user model.User - err := r.DB.Where("username = ?", username).First(&user).Error + err := r.DB.Preload("Role").Where("phone = ?", phone).First(&user).Error if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } return nil, err } return &user, nil @@ -55,28 +41,6 @@ func (r *userRepository) FindByPhoneAndRole(phone, roleID string) (*model.User, return &user, nil } -func (r *userRepository) FindByEmailAndRole(email, roleID string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? AND role_id = ?", email, roleID).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? OR username = ? OR phone = ?", identifier, identifier, identifier).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) Create(user *model.User) error { - err := r.DB.Create(user).Error - if err != nil { - return err - } - return nil +func (r *userRepository) CreateUser(user *model.User) error { + return r.DB.Create(user).Error } diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 6be1bba..93bc0c2 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,171 +1,134 @@ package services import ( - "errors" "fmt" "time" + "github.com/go-redis/redis/v8" "github.com/golang-jwt/jwt/v5" + "github.com/pahmiudahgede/senggoldong/config" "github.com/pahmiudahgede/senggoldong/dto" "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" - "golang.org/x/crypto/bcrypt" ) -const ( - ErrUsernameTaken = "username is already taken" - ErrPhoneTaken = "phone number is already used for this role" - ErrEmailTaken = "email is already used for this role" - ErrInvalidRoleID = "invalid roleId" - ErrPasswordMismatch = "password and confirm password do not match" - ErrRoleIDRequired = "roleId is required" - ErrFailedToHashPassword = "failed to hash password" - ErrFailedToCreateUser = "failed to create user" - ErrIncorrectPassword = "incorrect password" - ErrAccountNotFound = "account not found" -) - -type UserService interface { - Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) - Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) +type AuthService interface { + RegisterUser(request dto.RegisterRequest) (*model.User, error) + VerifyOTP(phone, otp string) error + GetUserByPhone(phone string) (*model.User, error) + GenerateJWT(user *model.User) (string, error) } -type userService struct { - UserRepo repositories.UserRepository - RoleRepo repositories.RoleRepository - SecretKey string +type authService struct { + UserRepo repositories.UserRepository } -func NewUserService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, secretKey string) UserService { - return &userService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey} +func NewAuthService(userRepo repositories.UserRepository) AuthService { + return &authService{UserRepo: userRepo} } -func (s *userService) Login(credentials dto.LoginDTO) (*dto.UserResponseWithToken, error) { - if credentials.RoleID == "" { - return nil, errors.New(ErrRoleIDRequired) +func (s *authService) RegisterUser(request dto.RegisterRequest) (*model.User, error) { + + user, err := s.UserRepo.FindByPhone(request.Phone) + if err == nil && user != nil { + return nil, fmt.Errorf("user with phone %s already exists", request.Phone) } - user, err := s.UserRepo.FindByIdentifierAndRole(credentials.Identifier, credentials.RoleID) + user = &model.User{ + Phone: request.Phone, + RoleID: request.RoleID, + EmailVerified: false, + } + + err = s.UserRepo.CreateUser(user) if err != nil { - return nil, errors.New(ErrAccountNotFound) + return nil, fmt.Errorf("failed to create user: %v", err) } - if !CheckPasswordHash(credentials.Password, user.Password) { - return nil, errors.New(ErrIncorrectPassword) - } - - token, err := s.generateJWT(user) + _, err = s.SendOTP(request.Phone) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to send OTP: %v", err) } - sessionKey := fmt.Sprintf("session:%s", user.ID) - sessionData := map[string]interface{}{ - "userID": user.ID, - "roleID": user.RoleID, - "roleName": user.Role.RoleName, - } - - err = utils.SetJSONData(sessionKey, sessionData, time.Hour*24) - if err != nil { - return nil, err - } - - return &dto.UserResponseWithToken{ - RoleName: user.Role.RoleName, - UserID: user.ID, - Token: token, - }, nil + return user, nil } -func (s *userService) generateJWT(user *model.User) (string, error) { +func (s *authService) GetUserByPhone(phone string) (*model.User, error) { + user, err := s.UserRepo.FindByPhone(phone) + if err != nil { + return nil, fmt.Errorf("error retrieving user by phone: %v", err) + } + if user == nil { + return nil, fmt.Errorf("user not found") + } + return user, nil +} + +func (s *authService) SendOTP(phone string) (string, error) { + otpCode := generateOTP() + + message := fmt.Sprintf("Your OTP code is: %s", otpCode) + err := config.SendWhatsAppMessage(phone, message) + if err != nil { + return "", fmt.Errorf("failed to send OTP via WhatsApp: %v", err) + } + + expirationTime := 5 * time.Minute + err = config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() + if err != nil { + return "", fmt.Errorf("failed to store OTP in Redis: %v", err) + } + + return otpCode, nil +} + +func (s *authService) VerifyOTP(phone, otp string) error { + + otpRecord, err := config.RedisClient.Get(config.Ctx, phone).Result() + if err == redis.Nil { + + return fmt.Errorf("OTP not found or expired") + } else if err != nil { + + return fmt.Errorf("failed to retrieve OTP from Redis: %v", err) + } + + if otp != otpRecord { + return fmt.Errorf("invalid OTP") + } + + err = config.RedisClient.Del(config.Ctx, phone).Err() + if err != nil { + return fmt.Errorf("failed to delete OTP from Redis: %v", err) + } + + return nil +} + +func (s *authService) GenerateJWT(user *model.User) (string, error) { + if user == nil || user.Role == nil { + return "", fmt.Errorf("user or user role is nil, cannot generate token") + } + claims := jwt.MapClaims{ - "sub": user.ID, - "iat": time.Now().Unix(), - "exp": time.Now().Add(time.Hour * 24).Unix(), + "sub": user.ID, + "role": user.Role.RoleName, + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour * 24).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(s.SecretKey)) + secretKey := config.GetSecretKey() + + tokenString, err := token.SignedString([]byte(secretKey)) if err != nil { - return "", err + return "", fmt.Errorf("failed to generate JWT token: %v", err) } return tokenString, nil } -func CheckPasswordHash(password, hashedPassword string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) - return err == nil -} - -func (s *userService) Register(user dto.RegisterDTO) (*dto.UserResponseDTO, error) { - - if user.Password != user.ConfirmPassword { - return nil, fmt.Errorf("%s", ErrPasswordMismatch) - } - - if user.RoleID == "" { - return nil, fmt.Errorf("%s", ErrRoleIDRequired) - } - - role, err := s.RoleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrInvalidRoleID, err) - } - - if existingUser, _ := s.UserRepo.FindByUsername(user.Username); existingUser != nil { - return nil, fmt.Errorf("%s", ErrUsernameTaken) - } - - if existingPhone, _ := s.UserRepo.FindByPhoneAndRole(user.Phone, user.RoleID); existingPhone != nil { - return nil, fmt.Errorf("%s", ErrPhoneTaken) - } - - if existingEmail, _ := s.UserRepo.FindByEmailAndRole(user.Email, user.RoleID); existingEmail != nil { - return nil, fmt.Errorf("%s", ErrEmailTaken) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrFailedToHashPassword, err) - } - - newUser := model.User{ - Username: user.Username, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - Password: string(hashedPassword), - RoleID: user.RoleID, - } - - err = s.UserRepo.Create(&newUser) - if err != nil { - return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) - } - - userResponse := s.prepareUserResponse(newUser, role) - - return userResponse, nil -} - -func (s *userService) prepareUserResponse(user model.User, role *model.Role) *dto.UserResponseDTO { - - createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - - return &dto.UserResponseDTO{ - ID: user.ID, - Username: user.Username, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, - RoleName: role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } +func generateOTP() string { + return fmt.Sprintf("%06d", time.Now().UnixNano()%1000000) } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 3b8b092..1f5b54e 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -14,7 +14,7 @@ import ( "github.com/pahmiudahgede/senggoldong/internal/repositories" "github.com/pahmiudahgede/senggoldong/model" "github.com/pahmiudahgede/senggoldong/utils" - "golang.org/x/crypto/bcrypt" + // "golang.org/x/crypto/bcrypt" ) var allowedExtensions = []string{".jpg", ".jpeg", ".png"} @@ -22,7 +22,7 @@ var allowedExtensions = []string{".jpg", ".jpeg", ".png"} type UserProfileService interface { GetUserProfile(userID string) (*dto.UserResponseDTO, error) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) - UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) + // UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) GetAllUsers() ([]dto.UserResponseDTO, error) @@ -162,12 +162,12 @@ func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.Upd user.Phone = updateData.Phone } - if updateData.Email != "" && updateData.Email != user.Email { - if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { - return nil, err - } - user.Email = updateData.Email - } + // if updateData.Email != "" && updateData.Email != user.Email { + // if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { + // return nil, err + // } + // user.Email = updateData.Email + // } err = s.UserProfileRepo.Update(user) if err != nil { @@ -196,43 +196,43 @@ func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone stri return nil } -func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { - existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) - if existingEmail != nil { - return fmt.Errorf("email is already used for this role") - } - return nil -} +// func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { +// existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) +// if existingEmail != nil { +// return fmt.Errorf("email is already used for this role") +// } +// return nil +// } -func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { +// func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { - validationErrors, valid := passwordData.Validate() - if !valid { - return "", fmt.Errorf("validation failed: %v", validationErrors) - } +// validationErrors, valid := passwordData.Validate() +// if !valid { +// return "", fmt.Errorf("validation failed: %v", validationErrors) +// } - user, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return "", errors.New("user not found") - } +// user, err := s.UserProfileRepo.FindByID(userID) +// if err != nil { +// return "", errors.New("user not found") +// } - if !CheckPasswordHash(passwordData.OldPassword, user.Password) { - return "", errors.New("old password is incorrect") - } +// if !CheckPasswordHash(passwordData.OldPassword, user.Password) { +// return "", errors.New("old password is incorrect") +// } - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) - if err != nil { - return "", fmt.Errorf("failed to hash new password: %v", err) - } +// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) +// if err != nil { +// return "", fmt.Errorf("failed to hash new password: %v", err) +// } - user.Password = string(hashedPassword) - err = s.UserProfileRepo.Update(user) - if err != nil { - return "", fmt.Errorf("failed to update password: %v", err) - } +// user.Password = string(hashedPassword) +// err = s.UserProfileRepo.Update(user) +// if err != nil { +// return "", fmt.Errorf("failed to update password: %v", err) +// } - return "Password berhasil diupdate", nil -} +// return "Password berhasil diupdate", nil +// } func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) { baseURL := os.Getenv("BASE_URL") diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 138992e..16ccc7f 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,31 +1,40 @@ package presentation import ( - "log" - "os" - "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" + // "gorm.io/gorm" + // "github.com/pahmiudahgede/senggoldong/middleware" ) func AuthRouter(api fiber.Router) { - secretKey := os.Getenv("SECRET_KEY") - if secretKey == "" { - log.Fatal("SECRET_KEY is not set in the environment variables") - os.Exit(1) - } + // userRepo := repositories.NewUserRepository(config.DB) + // roleRepo := repositories.NewRoleRepository(config.DB) + // userService := services.NewUserService(userRepo, roleRepo, secretKey) + // userHandler := handler.NewUserHandler(userService) + // api.Post("/login", userHandler.Login) + // api.Post("/register", userHandler.Register) + // api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) + // userRepo := repositories.NewUserRepository(config.DB) + // authService := services.NewAuthService(userRepo, secretKey) + + // // Inisialisasi handler + // authHandler := handler.NewAuthHandler(authService) + + // // Endpoint OTP + // authRoutes := api.Group("/auth") + // authRoutes.Post("/send-otp", authHandler.SendOTP) + // authRoutes.Post("/verify-otp", authHandler.VerifyOTP) userRepo := repositories.NewUserRepository(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - userService := services.NewUserService(userRepo, roleRepo, secretKey) - userHandler := handler.NewUserHandler(userService) + authService := services.NewAuthService(userRepo) - api.Post("/login", userHandler.Login) - api.Post("/register", userHandler.Register) - api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) + authHandler := handler.NewAuthHandler(authService) + // Routes + api.Post("/register", authHandler.Register) + api.Post("/verify-otp", authHandler.VerifyOTP) } diff --git a/presentation/user_route.go b/presentation/user_route.go index dcdb00c..2e54a2e 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -23,6 +23,6 @@ func UserProfileRouter(api fiber.Router) { userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) - userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) + // userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 24e748e..0cbf2ff 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -97,3 +97,44 @@ func logAndReturnError(message string, err error) error { log.Printf("%s: %v", message, err) return err } + +func SetStringData(key, value string, expiration time.Duration) error { + if expiration == 0 { + expiration = defaultExpiration + } + + err := config.RedisClient.Set(ctx, key, value, expiration).Err() + if err != nil { + return logAndReturnError(fmt.Sprintf("Error setting string data in Redis with key: %s", key), err) + } + + log.Printf("String data stored in Redis with key: %s", key) + return nil +} + +func GetStringData(key string) (string, error) { + val, err := config.RedisClient.Get(ctx, key).Result() + if err == redis.Nil { + return "", nil + } else if err != nil { + return "", logAndReturnError(fmt.Sprintf("Error retrieving string data from Redis with key: %s", key), err) + } + + return val, nil +} + +func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { + err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() + if err != nil { + return fmt.Errorf("failed to store OTP in Redis: %v", err) + } + return nil +} + +func GetOTPFromRedis(phone string) (string, error) { + otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() + if err != nil { + return "", fmt.Errorf("failed to get OTP from Redis: %v", err) + } + return otpCode, nil +} \ No newline at end of file From 14d9c9c56344663dbd1365af699ce26e5781bfbd Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 21 Mar 2025 19:23:36 +0700 Subject: [PATCH 12/48] code implemed and not finishing yet --- cmd/main.go | 13 +- config/database.go | 3 +- dto/auth_dto.go | 147 +------------- go.mod | 2 +- internal/handler/address_handler.go | 7 +- internal/handler/article_handler.go | 7 +- internal/handler/auth_handler.go | 71 ++----- internal/handler/banner_handler.go | 7 +- internal/handler/initialcoint_handler.go | 7 +- internal/handler/product_handler.go | 7 +- internal/handler/role_handler.go | 5 +- internal/handler/store_handler.go | 7 +- internal/handler/trash_handler.go | 7 +- internal/handler/user_handler.go | 12 +- internal/handler/userpin_handler.go | 7 +- internal/handler/wilayah_indonesia_handler.go | 5 +- internal/repositories/address_repo.go | 6 +- internal/repositories/article_repo.go | 5 +- internal/repositories/auth_repo.go | 92 +++++++-- internal/repositories/banner_repo.go | 3 +- internal/repositories/initialcoint_repo.go | 3 +- internal/repositories/product_repo.go | 3 +- internal/repositories/role_repo.go | 3 +- internal/repositories/store_repo.go | 3 +- internal/repositories/trash_repo.go | 3 +- internal/repositories/user_repo.go | 3 +- internal/repositories/userpin_repo.go | 3 +- .../repositories/wilayah_indonesia_repo.go | 5 +- internal/services/address_service.go | 8 +- internal/services/article_service.go | 9 +- internal/services/auth_service.go | 190 +++++++++--------- internal/services/banner_service.go | 9 +- internal/services/initialcoint_service.go | 8 +- internal/services/product_service.go | 9 +- internal/services/role_service.go | 6 +- internal/services/store_service.go | 11 +- internal/services/trash_service.go | 8 +- internal/services/user_service.go | 34 ++-- internal/services/userpin_service.go | 7 +- .../services/wilayah_indonesia_service.go | 8 +- middleware/api_key.go | 3 +- middleware/auth_middleware.go | 3 +- middleware/role_middleware.go | 3 +- presentation/address_route.go | 11 +- presentation/article_route.go | 13 +- presentation/auth_route.go | 32 ++- presentation/banner_route.go | 13 +- presentation/initialcoint_route.go | 13 +- presentation/product_route.go | 13 +- presentation/role_route.go | 9 +- presentation/store_route.go | 13 +- presentation/trash_route.go | 13 +- presentation/user_route.go | 11 +- presentation/userpin_route.go | 11 +- presentation/wilayahindonesia_route.go | 31 +-- router/setup_routes.go.go | 5 +- utils/redis_caching.go | 5 +- 57 files changed, 452 insertions(+), 503 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 1792938..d4ee0e9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,11 @@ package main import ( + "rijig/config" + "rijig/router" + "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/pahmiudahgede/senggoldong/config" - "github.com/pahmiudahgede/senggoldong/router" ) func main() { @@ -12,7 +13,7 @@ func main() { app := fiber.New() app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:3000", + AllowOrigins: "http://localhost:3000", AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", AllowCredentials: true, @@ -21,7 +22,7 @@ func main() { app.Use(func(c *fiber.Ctx) error { c.Set("Access-Control-Allow-Origin", "http://localhost:3000") c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") c.Set("Access-Control-Allow-Credentials", "true") return c.Next() }) @@ -29,9 +30,9 @@ func main() { app.Options("*", func(c *fiber.Ctx) error { c.Set("Access-Control-Allow-Origin", "http://localhost:3000") c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") c.Set("Access-Control-Allow-Credentials", "true") - return c.SendStatus(fiber.StatusNoContent) + return c.SendStatus(fiber.StatusNoContent) }) router.SetupRoutes(app) diff --git a/config/database.go b/config/database.go index 9e5a624..52ac433 100644 --- a/config/database.go +++ b/config/database.go @@ -5,7 +5,8 @@ import ( "log" "os" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/driver/postgres" "gorm.io/gorm" ) diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 6232f8a..de3674d 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -15,35 +15,22 @@ type VerifyOTPRequest struct { OTP string `json:"otp"` } -type MetaResponse struct { - Status int `json:"status"` - Message string `json:"message"` -} - -// UserDataResponse untuk bagian data type UserDataResponse struct { UserID string `json:"user_id"` UserRole string `json:"user_role"` Token string `json:"token"` } -// Response struct utama -type Response struct { - Meta MetaResponse `json:"meta"` - Data *UserDataResponse `json:"data,omitempty"` // Gunakan pointer agar bisa bernilai nil jika tidak diperlukan -} - -func (l *RegisterRequest) Validate() (map[string][]string, bool) { +func (r *RegisterRequest) Validate() (map[string][]string, bool) { errors := make(map[string][]string) - // Validasi RoleID dan Phone - if strings.TrimSpace(l.RoleID) == "" { - errors["roleid"] = append(errors["roleid"], "Role ID is required") + if strings.TrimSpace(r.RoleID) == "" { + errors["role_id"] = append(errors["role_id"], "Role ID is required") } - if strings.TrimSpace(l.Phone) == "" { + if strings.TrimSpace(r.Phone) == "" { errors["phone"] = append(errors["phone"], "Phone is required") - } else if !IsValidPhoneNumber(l.Phone) { + } else if !IsValidPhoneNumber(r.Phone) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") } @@ -53,131 +40,7 @@ func (l *RegisterRequest) Validate() (map[string][]string, bool) { return nil, true } -// IsValidPhoneNumber untuk validasi format nomor telepon func IsValidPhoneNumber(phone string) bool { - // Validasi format nomor telepon harus dimulai dengan 62 dan 9-13 digit setelahnya re := regexp.MustCompile(`^62\d{9,13}$`) return re.MatchString(phone) } - -// package dto - -// import ( -// "regexp" -// "strings" -// ) - -// type LoginDTO struct { -// RoleID string `json:"roleid"` -// Identifier string `json:"identifier"` -// Password string `json:"password"` -// } - -// type UserResponseWithToken struct { -// UserID string `json:"user_id"` -// RoleName string `json:"role_name"` -// Token string `json:"token"` -// } - -// type RegisterDTO struct { -// Username string `json:"username"` -// Name string `json:"name"` -// Phone string `json:"phone"` -// Email string `json:"email"` -// Password string `json:"password"` -// ConfirmPassword string `json:"confirm_password"` -// RoleID string `json:"roleId,omitempty"` -// } - -// func (l *LoginDTO) Validate() (map[string][]string, bool) { -// errors := make(map[string][]string) - -// if strings.TrimSpace(l.RoleID) == "" { -// errors["roleid"] = append(errors["roleid"], "Role ID is required") -// } -// if strings.TrimSpace(l.Identifier) == "" { -// errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required") -// } -// if strings.TrimSpace(l.Password) == "" { -// errors["password"] = append(errors["password"], "Password is required") -// } - -// if len(errors) > 0 { -// return errors, false -// } -// return nil, true -// } - -// func (r *RegisterDTO) Validate() (map[string][]string, bool) { -// errors := make(map[string][]string) - -// r.validateRequiredFields(errors) - -// if r.Phone != "" && !IsValidPhoneNumber(r.Phone) { -// errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") -// } - -// if r.Email != "" && !IsValidEmail(r.Email) { -// errors["email"] = append(errors["email"], "Invalid email format") -// } - -// if r.Password != "" && !IsValidPassword(r.Password) { -// errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number") -// } - -// if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword { -// errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match") -// } - -// if len(errors) > 0 { -// return errors, false -// } - -// return nil, true -// } - -// func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) { - -// if strings.TrimSpace(r.Username) == "" { -// errors["username"] = append(errors["username"], "Username is required") -// } -// if strings.TrimSpace(r.Name) == "" { -// errors["name"] = append(errors["name"], "Name is required") -// } -// if strings.TrimSpace(r.Phone) == "" { -// errors["phone"] = append(errors["phone"], "Phone number is required") -// } -// if strings.TrimSpace(r.Email) == "" { -// errors["email"] = append(errors["email"], "Email is required") -// } -// if strings.TrimSpace(r.Password) == "" { -// errors["password"] = append(errors["password"], "Password is required") -// } -// if strings.TrimSpace(r.ConfirmPassword) == "" { -// errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required") -// } -// if strings.TrimSpace(r.RoleID) == "" { -// errors["roleId"] = append(errors["roleId"], "RoleID is required") -// } -// } - -// func IsValidPhoneNumber(phone string) bool { - -// re := regexp.MustCompile(`^\+62\d{9,13}$`) -// return re.MatchString(phone) -// } - -// func IsValidEmail(email string) bool { - -// re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) -// return re.MatchString(email) -// } - -// func IsValidPassword(password string) bool { -// if len(password) < 8 { -// return false -// } - -// re := regexp.MustCompile(`\d`) -// return re.MatchString(password) -// } diff --git a/go.mod b/go.mod index 99bc202..687dde0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/pahmiudahgede/senggoldong +module rijig go 1.23.3 diff --git a/internal/handler/address_handler.go b/internal/handler/address_handler.go index 44dfe9d..46a550d 100644 --- a/internal/handler/address_handler.go +++ b/internal/handler/address_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type AddressHandler struct { diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go index 6462870..844e554 100644 --- a/internal/handler/article_handler.go +++ b/internal/handler/article_handler.go @@ -5,10 +5,11 @@ import ( "mime/multipart" "strconv" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type ArticleHandler struct { diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 50edbe0..4653495 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,88 +1,53 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" ) type AuthHandler struct { - AuthService services.AuthService + authService services.AuthService } func NewAuthHandler(authService services.AuthService) *AuthHandler { - return &AuthHandler{AuthService: authService} + return &AuthHandler{ + authService: authService, + } } func (h *AuthHandler) Register(c *fiber.Ctx) error { var request dto.RegisterRequest if err := c.BodyParser(&request); err != nil { - return c.Status(400).SendString("Invalid input") + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) } if errors, valid := request.Validate(); !valid { - return c.Status(400).JSON(errors) + return utils.ValidationErrorResponse(c, errors) } - _, err := h.AuthService.RegisterUser(request) + err := h.authService.RegisterUser(&request) if err != nil { - return c.Status(500).SendString(err.Error()) + return utils.ErrorResponse(c, err.Error()) } - return c.Status(201).JSON(fiber.Map{ - "meta": fiber.Map{ - "status": 201, - "message": "The input register from the user has been successfully recorded. Please check the otp code sent to your number.", - }, - }) + return utils.SuccessResponse(c, nil, "OTP has been sent to your phone") } func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { - var request struct { - Phone string `json:"phone"` - OTP string `json:"otp"` - } + var request dto.VerifyOTPRequest if err := c.BodyParser(&request); err != nil { - return c.Status(400).SendString("Invalid input") + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) } - err := h.AuthService.VerifyOTP(request.Phone, request.OTP) + err := h.authService.VerifyOTP(&request) if err != nil { - return c.Status(400).JSON(dto.Response{ - Meta: dto.MetaResponse{ - Status: 400, - Message: "Invalid OTP", - }, - Data: nil, - }) + return utils.ErrorResponse(c, err.Error()) } - user, err := h.AuthService.GetUserByPhone(request.Phone) - if err != nil { - return c.Status(500).SendString("Error retrieving user") - } - if user == nil { - return c.Status(404).SendString("User not found") - } - - token, err := h.AuthService.GenerateJWT(user) - if err != nil { - return c.Status(500).SendString("Error generating token") - } - - response := dto.Response{ - Meta: dto.MetaResponse{ - Status: 200, - Message: "OTP yang dimasukkan valid", - }, - Data: &dto.UserDataResponse{ - UserID: user.ID, - UserRole: user.Role.RoleName, - Token: token, - }, - } - - return c.Status(200).JSON(response) + return utils.SuccessResponse(c, nil, "User successfully registered") } diff --git a/internal/handler/banner_handler.go b/internal/handler/banner_handler.go index ac5bd9e..731fd1f 100644 --- a/internal/handler/banner_handler.go +++ b/internal/handler/banner_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type BannerHandler struct { diff --git a/internal/handler/initialcoint_handler.go b/internal/handler/initialcoint_handler.go index 2cf275a..c80b466 100644 --- a/internal/handler/initialcoint_handler.go +++ b/internal/handler/initialcoint_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type InitialCointHandler struct { diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go index 9a84911..5c0eccf 100644 --- a/internal/handler/product_handler.go +++ b/internal/handler/product_handler.go @@ -5,10 +5,11 @@ import ( "log" "strconv" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type ProductHandler struct { diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go index 2141ed5..f0bb07f 100644 --- a/internal/handler/role_handler.go +++ b/internal/handler/role_handler.go @@ -1,9 +1,10 @@ package handler import ( + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type RoleHandler struct { diff --git a/internal/handler/store_handler.go b/internal/handler/store_handler.go index bf759b8..dde89f1 100644 --- a/internal/handler/store_handler.go +++ b/internal/handler/store_handler.go @@ -3,10 +3,11 @@ package handler import ( "log" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type StoreHandler struct { diff --git a/internal/handler/trash_handler.go b/internal/handler/trash_handler.go index d07c8d4..afc85b5 100644 --- a/internal/handler/trash_handler.go +++ b/internal/handler/trash_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type TrashHandler struct { diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index f8e0d73..e479047 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type UserProfileHandler struct { @@ -30,7 +31,6 @@ func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") } - func (h *UserProfileHandler) GetUserProfileById(c *fiber.Ctx) error { userID := c.Params("userid") if userID == "" { @@ -118,8 +118,8 @@ func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { // return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) // } -// return utils.GenericResponse(c, fiber.StatusOK, message) -// } +// return utils.GenericResponse(c, fiber.StatusOK, message) +// } func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(string) diff --git a/internal/handler/userpin_handler.go b/internal/handler/userpin_handler.go index f0e2908..bb65b4b 100644 --- a/internal/handler/userpin_handler.go +++ b/internal/handler/userpin_handler.go @@ -1,10 +1,11 @@ package handler import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type UserPinHandler struct { diff --git a/internal/handler/wilayah_indonesia_handler.go b/internal/handler/wilayah_indonesia_handler.go index 02ae1bb..bde943a 100644 --- a/internal/handler/wilayah_indonesia_handler.go +++ b/internal/handler/wilayah_indonesia_handler.go @@ -3,9 +3,10 @@ package handler import ( "strconv" + "rijig/internal/services" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/internal/services" - "github.com/pahmiudahgede/senggoldong/utils" ) type WilayahIndonesiaHandler struct { diff --git a/internal/repositories/address_repo.go b/internal/repositories/address_repo.go index 1661b6b..d8c2cf5 100644 --- a/internal/repositories/address_repo.go +++ b/internal/repositories/address_repo.go @@ -1,7 +1,8 @@ package repositories import ( - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) @@ -25,7 +26,6 @@ func (r *addressRepository) CreateAddress(address *model.Address) error { return r.DB.Create(address).Error } - func (r *addressRepository) FindAddressByUserID(userID string) ([]model.Address, error) { var addresses []model.Address err := r.DB.Where("user_id = ?", userID).Find(&addresses).Error @@ -58,4 +58,4 @@ func (r *addressRepository) DeleteAddress(id string) error { return err } return nil -} \ No newline at end of file +} diff --git a/internal/repositories/article_repo.go b/internal/repositories/article_repo.go index 79f2fcb..d9a1222 100644 --- a/internal/repositories/article_repo.go +++ b/internal/repositories/article_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) @@ -71,4 +72,4 @@ func (r *articleRepository) UpdateArticle(id string, article *model.Article) err func (r *articleRepository) DeleteArticle(id string) error { result := r.DB.Delete(&model.Article{}, "id = ?", id) return result.Error -} \ No newline at end of file +} diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 5ffe64d..0869d2b 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,29 +1,40 @@ package repositories import ( - "github.com/pahmiudahgede/senggoldong/model" + "context" + "encoding/json" + "fmt" + "time" + + "rijig/model" + + "github.com/go-redis/redis/v8" "gorm.io/gorm" ) type UserRepository interface { + SaveUser(user *model.User) (*model.User, error) FindByPhone(phone string) (*model.User, error) - FindByPhoneAndRole(phone, roleID string) (*model.User, error) - CreateUser(user *model.User) error } type userRepository struct { - DB *gorm.DB + db *gorm.DB } func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{DB: db} + return &userRepository{db: db} +} + +func (r *userRepository) SaveUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil } func (r *userRepository) FindByPhone(phone string) (*model.User, error) { var user model.User - - err := r.DB.Preload("Role").Where("phone = ?", phone).First(&user).Error - if err != nil { + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil } @@ -32,15 +43,62 @@ func (r *userRepository) FindByPhone(phone string) (*model.User, error) { return &user, nil } -func (r *userRepository) FindByPhoneAndRole(phone, roleID string) (*model.User, error) { - var user model.User - err := r.DB.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil +type RedisRepository interface { + StoreData(key string, data interface{}, expiration time.Duration) error + GetData(key string) (interface{}, error) // Mengembalikan interface{} + DeleteData(key string) error } -func (r *userRepository) CreateUser(user *model.User) error { - return r.DB.Create(user).Error +type redisRepository struct { + client *redis.Client + ctx context.Context +} + +// NewRedisRepository membuat instance baru dari redisRepository +func NewRedisRepository(client *redis.Client) RedisRepository { + return &redisRepository{ + client: client, + ctx: context.Background(), + } +} + +// StoreData menyimpan data ke dalam Redis (dalam format JSON) +func (r *redisRepository) StoreData(key string, data interface{}, expiration time.Duration) error { + // Marshaling data ke JSON + jsonData, err := json.Marshal(data) + if err != nil { + return fmt.Errorf("failed to marshal data: %v", err) + } + + // Simpan JSON ke Redis + err = r.client.Set(r.ctx, key, jsonData, expiration).Err() + if err != nil { + return fmt.Errorf("failed to store data in Redis: %v", err) + } + return nil +} + +// GetData mengambil data dari Redis berdasarkan key +func (r *redisRepository) GetData(key string) (interface{}, error) { + val, err := r.client.Get(r.ctx, key).Result() + if err != nil { + return nil, fmt.Errorf("failed to get data from Redis: %v", err) + } + + // Unmarshal data JSON kembali ke objek + var data interface{} + err = json.Unmarshal([]byte(val), &data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal data: %v", err) + } + return data, nil +} + +// DeleteData menghapus data di Redis berdasarkan key +func (r *redisRepository) DeleteData(key string) error { + err := r.client.Del(r.ctx, key).Err() + if err != nil { + return fmt.Errorf("failed to delete data from Redis: %v", err) + } + return nil } diff --git a/internal/repositories/banner_repo.go b/internal/repositories/banner_repo.go index 8f553f9..41803de 100644 --- a/internal/repositories/banner_repo.go +++ b/internal/repositories/banner_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/initialcoint_repo.go b/internal/repositories/initialcoint_repo.go index 0a64709..ec03479 100644 --- a/internal/repositories/initialcoint_repo.go +++ b/internal/repositories/initialcoint_repo.go @@ -2,7 +2,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/product_repo.go b/internal/repositories/product_repo.go index 5ab4f56..08f1b1f 100644 --- a/internal/repositories/product_repo.go +++ b/internal/repositories/product_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go index 7abea10..df4299b 100644 --- a/internal/repositories/role_repo.go +++ b/internal/repositories/role_repo.go @@ -1,7 +1,8 @@ package repositories import ( - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/store_repo.go b/internal/repositories/store_repo.go index 989e591..9b02bb7 100644 --- a/internal/repositories/store_repo.go +++ b/internal/repositories/store_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index a41e84d..1fd4861 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go index 3279377..4243219 100644 --- a/internal/repositories/user_repo.go +++ b/internal/repositories/user_repo.go @@ -3,7 +3,8 @@ package repositories import ( "fmt" - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/userpin_repo.go b/internal/repositories/userpin_repo.go index 057964e..47d7118 100644 --- a/internal/repositories/userpin_repo.go +++ b/internal/repositories/userpin_repo.go @@ -1,7 +1,8 @@ package repositories import ( - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) diff --git a/internal/repositories/wilayah_indonesia_repo.go b/internal/repositories/wilayah_indonesia_repo.go index ea4bd89..60a53bf 100644 --- a/internal/repositories/wilayah_indonesia_repo.go +++ b/internal/repositories/wilayah_indonesia_repo.go @@ -1,7 +1,8 @@ package repositories import ( - "github.com/pahmiudahgede/senggoldong/model" + "rijig/model" + "gorm.io/gorm" ) @@ -240,4 +241,4 @@ func (r *wilayahIndonesiaRepository) FindVillageByID(id string) (*model.Village, return nil, err } return &village, nil -} \ No newline at end of file +} diff --git a/internal/services/address_service.go b/internal/services/address_service.go index 7e3f22f..0f73781 100644 --- a/internal/services/address_service.go +++ b/internal/services/address_service.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" ) type AddressService interface { diff --git a/internal/services/article_service.go b/internal/services/article_service.go index 7d674b8..b733a8d 100644 --- a/internal/services/article_service.go +++ b/internal/services/article_service.go @@ -8,11 +8,12 @@ import ( "path/filepath" "time" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + "github.com/google/uuid" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" ) type ArticleService interface { diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 93bc0c2..0c05b6e 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,103 +1,127 @@ package services import ( + "encoding/json" "fmt" + "log" "time" - "github.com/go-redis/redis/v8" - "github.com/golang-jwt/jwt/v5" - "github.com/pahmiudahgede/senggoldong/config" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" + "math/rand" + + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" ) type AuthService interface { - RegisterUser(request dto.RegisterRequest) (*model.User, error) - VerifyOTP(phone, otp string) error - GetUserByPhone(phone string) (*model.User, error) - GenerateJWT(user *model.User) (string, error) + RegisterUser(request *dto.RegisterRequest) error + VerifyOTP(request *dto.VerifyOTPRequest) error } type authService struct { - UserRepo repositories.UserRepository + userRepo repositories.UserRepository + roleRepo repositories.RoleRepository + redisRepo repositories.RedisRepository } -func NewAuthService(userRepo repositories.UserRepository) AuthService { - return &authService{UserRepo: userRepo} +func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, redisRepo repositories.RedisRepository) AuthService { + return &authService{ + userRepo: userRepo, + roleRepo: roleRepo, + redisRepo: redisRepo, + } } -func (s *authService) RegisterUser(request dto.RegisterRequest) (*model.User, error) { +func (s *authService) RegisterUser(request *dto.RegisterRequest) error { - user, err := s.UserRepo.FindByPhone(request.Phone) - if err == nil && user != nil { - return nil, fmt.Errorf("user with phone %s already exists", request.Phone) + if request.RoleID == "" { + return fmt.Errorf("role_id cannot be empty") } - user = &model.User{ - Phone: request.Phone, - RoleID: request.RoleID, - EmailVerified: false, - } - - err = s.UserRepo.CreateUser(user) + role, err := s.roleRepo.FindByID(request.RoleID) if err != nil { - return nil, fmt.Errorf("failed to create user: %v", err) + return fmt.Errorf("role not found: %v", err) + } + if role == nil { + return fmt.Errorf("role with ID %s not found", request.RoleID) } - _, err = s.SendOTP(request.Phone) + existingUser, err := s.userRepo.FindByPhone(request.Phone) if err != nil { - return nil, fmt.Errorf("failed to send OTP: %v", err) + return fmt.Errorf("failed to check existing user: %v", err) + } + if existingUser != nil { + return fmt.Errorf("phone number already registered") } - return user, nil + temporaryData := &model.User{ + Phone: request.Phone, + RoleID: request.RoleID, + } + + err = s.redisRepo.StoreData(request.Phone, temporaryData, 1*time.Hour) + if err != nil { + return fmt.Errorf("failed to store registration data in Redis: %v", err) + } + + otp := generateOTP() + err = s.redisRepo.StoreData("otp:"+request.Phone, otp, 10*time.Minute) + if err != nil { + return fmt.Errorf("failed to store OTP in Redis: %v", err) + } + + err = config.SendWhatsAppMessage(request.Phone, fmt.Sprintf("Your OTP is: %s", otp)) + if err != nil { + return fmt.Errorf("failed to send OTP via WhatsApp: %v", err) + } + + log.Printf("OTP sent to phone number: %s", request.Phone) + return nil } -func (s *authService) GetUserByPhone(phone string) (*model.User, error) { - user, err := s.UserRepo.FindByPhone(phone) +func (s *authService) VerifyOTP(request *dto.VerifyOTPRequest) error { + + storedOTP, err := s.redisRepo.GetData("otp:" + request.Phone) if err != nil { - return nil, fmt.Errorf("error retrieving user by phone: %v", err) - } - if user == nil { - return nil, fmt.Errorf("user not found") - } - return user, nil -} - -func (s *authService) SendOTP(phone string) (string, error) { - otpCode := generateOTP() - - message := fmt.Sprintf("Your OTP code is: %s", otpCode) - err := config.SendWhatsAppMessage(phone, message) - if err != nil { - return "", fmt.Errorf("failed to send OTP via WhatsApp: %v", err) - } - - expirationTime := 5 * time.Minute - err = config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() - if err != nil { - return "", fmt.Errorf("failed to store OTP in Redis: %v", err) - } - - return otpCode, nil -} - -func (s *authService) VerifyOTP(phone, otp string) error { - - otpRecord, err := config.RedisClient.Get(config.Ctx, phone).Result() - if err == redis.Nil { - - return fmt.Errorf("OTP not found or expired") - } else if err != nil { - return fmt.Errorf("failed to retrieve OTP from Redis: %v", err) } - - if otp != otpRecord { + if storedOTP != request.OTP { return fmt.Errorf("invalid OTP") } - err = config.RedisClient.Del(config.Ctx, phone).Err() + temporaryData, err := s.redisRepo.GetData(request.Phone) + if err != nil { + return fmt.Errorf("failed to get registration data from Redis: %v", err) + } + if temporaryData == "" { + return fmt.Errorf("no registration data found for phone: %s", request.Phone) + } + + temporaryDataStr, ok := temporaryData.(string) + if !ok { + return fmt.Errorf("failed to assert data to string") + } + + temporaryDataBytes := []byte(temporaryDataStr) + + var user model.User + err = json.Unmarshal(temporaryDataBytes, &user) + if err != nil { + return fmt.Errorf("failed to unmarshal registration data: %v", err) + } + + _, err = s.userRepo.SaveUser(&user) + if err != nil { + return fmt.Errorf("failed to save user to database: %v", err) + } + + err = s.redisRepo.DeleteData(request.Phone) + if err != nil { + return fmt.Errorf("failed to delete registration data from Redis: %v", err) + } + + err = s.redisRepo.DeleteData("otp:" + request.Phone) if err != nil { return fmt.Errorf("failed to delete OTP from Redis: %v", err) } @@ -105,30 +129,12 @@ func (s *authService) VerifyOTP(phone, otp string) error { return nil } -func (s *authService) GenerateJWT(user *model.User) (string, error) { - if user == nil || user.Role == nil { - return "", fmt.Errorf("user or user role is nil, cannot generate token") - } - - claims := jwt.MapClaims{ - "sub": user.ID, - "role": user.Role.RoleName, - "iat": time.Now().Unix(), - "exp": time.Now().Add(time.Hour * 24).Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secretKey := config.GetSecretKey() - - tokenString, err := token.SignedString([]byte(secretKey)) - if err != nil { - return "", fmt.Errorf("failed to generate JWT token: %v", err) - } - - return tokenString, nil -} - func generateOTP() string { - return fmt.Sprintf("%06d", time.Now().UnixNano()%1000000) + + return fmt.Sprintf("%06d", RandomInt(100000, 999999)) +} + +func RandomInt(min, max int) int { + rand.Seed(time.Now().UnixNano()) + return rand.Intn(max-min+1) + min } diff --git a/internal/services/banner_service.go b/internal/services/banner_service.go index 67a5d7c..1b1b734 100644 --- a/internal/services/banner_service.go +++ b/internal/services/banner_service.go @@ -7,11 +7,12 @@ import ( "path/filepath" "time" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + "github.com/google/uuid" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" ) type BannerService interface { diff --git a/internal/services/initialcoint_service.go b/internal/services/initialcoint_service.go index 73bb599..2791335 100644 --- a/internal/services/initialcoint_service.go +++ b/internal/services/initialcoint_service.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" ) type InitialCointService interface { diff --git a/internal/services/product_service.go b/internal/services/product_service.go index bc6eb23..f448396 100644 --- a/internal/services/product_service.go +++ b/internal/services/product_service.go @@ -6,11 +6,12 @@ import ( "os" "path/filepath" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + "github.com/google/uuid" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" ) type ProductService interface { diff --git a/internal/services/role_service.go b/internal/services/role_service.go index c8a8741..d40d490 100644 --- a/internal/services/role_service.go +++ b/internal/services/role_service.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/utils" ) type RoleService interface { diff --git a/internal/services/store_service.go b/internal/services/store_service.go index 43467dd..ef855c7 100644 --- a/internal/services/store_service.go +++ b/internal/services/store_service.go @@ -6,11 +6,12 @@ import ( "os" "path/filepath" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + "github.com/google/uuid" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" ) type StoreService interface { @@ -237,7 +238,7 @@ func (s *storeService) DeleteStore(storeID string) error { func (s *storeService) saveStoreImage(file *multipart.FileHeader, imageType string) (string, error) { - imageDir := fmt.Sprintf("./public%s/uploads/store/%s",os.Getenv("BASE_URL"), imageType) + imageDir := fmt.Sprintf("./public%s/uploads/store/%s", os.Getenv("BASE_URL"), imageType) if _, err := os.Stat(imageDir); os.IsNotExist(err) { if err := os.MkdirAll(imageDir, os.ModePerm); err != nil { diff --git a/internal/services/trash_service.go b/internal/services/trash_service.go index 58fa05d..dcc05ca 100644 --- a/internal/services/trash_service.go +++ b/internal/services/trash_service.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" ) type TrashService interface { diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 1f5b54e..f1ab25a 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -10,10 +10,10 @@ import ( "path/filepath" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" // "golang.org/x/crypto/bcrypt" ) @@ -155,12 +155,12 @@ func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.Upd user.Name = updateData.Name } - if updateData.Phone != "" && updateData.Phone != user.Phone { - if err := s.updatePhoneIfNeeded(user, updateData.Phone); err != nil { - return nil, err - } - user.Phone = updateData.Phone - } + // if updateData.Phone != "" && updateData.Phone != user.Phone { + // if err := s.updatePhoneIfNeeded(user, updateData.Phone); err != nil { + // return nil, err + // } + // user.Phone = updateData.Phone + // } // if updateData.Email != "" && updateData.Email != user.Email { // if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { @@ -188,13 +188,13 @@ func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.Upd return userResponse, nil } -func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone string) error { - existingPhone, _ := s.UserRepo.FindByPhoneAndRole(newPhone, user.RoleID) - if existingPhone != nil { - return fmt.Errorf("phone number is already used for this role") - } - return nil -} +// func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone string) error { +// existingPhone, _ := s.UserRepo.FindByPhoneAndRole(newPhone, user.RoleID) +// if existingPhone != nil { +// return fmt.Errorf("phone number is already used for this role") +// } +// return nil +// } // func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { // existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) diff --git a/internal/services/userpin_service.go b/internal/services/userpin_service.go index 4324c17..9a2ba45 100644 --- a/internal/services/userpin_service.go +++ b/internal/services/userpin_service.go @@ -4,9 +4,10 @@ import ( "fmt" "time" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + "golang.org/x/crypto/bcrypt" ) diff --git a/internal/services/wilayah_indonesia_service.go b/internal/services/wilayah_indonesia_service.go index 0f78490..519522a 100644 --- a/internal/services/wilayah_indonesia_service.go +++ b/internal/services/wilayah_indonesia_service.go @@ -6,10 +6,10 @@ import ( "strconv" "time" - "github.com/pahmiudahgede/senggoldong/dto" - "github.com/pahmiudahgede/senggoldong/internal/repositories" - "github.com/pahmiudahgede/senggoldong/model" - "github.com/pahmiudahgede/senggoldong/utils" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" ) type WilayahIndonesiaService interface { diff --git a/middleware/api_key.go b/middleware/api_key.go index 0693eb4..3f2c7d6 100644 --- a/middleware/api_key.go +++ b/middleware/api_key.go @@ -3,8 +3,9 @@ package middleware import ( "os" + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/utils" ) func APIKeyMiddleware(c *fiber.Ctx) error { diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index 427c5f6..c1b868d 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -3,9 +3,10 @@ package middleware import ( "os" + "rijig/utils" + "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" - "github.com/pahmiudahgede/senggoldong/utils" ) func AuthMiddleware(c *fiber.Ctx) error { diff --git a/middleware/role_middleware.go b/middleware/role_middleware.go index dcb6687..cfd864c 100644 --- a/middleware/role_middleware.go +++ b/middleware/role_middleware.go @@ -1,8 +1,9 @@ package middleware import ( + "rijig/utils" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/utils" ) func RoleMiddleware(allowedRoles ...string) fiber.Handler { diff --git a/presentation/address_route.go b/presentation/address_route.go index 84e549d..f9038be 100644 --- a/presentation/address_route.go +++ b/presentation/address_route.go @@ -1,12 +1,13 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "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" ) func AddressRouter(api fiber.Router) { diff --git a/presentation/article_route.go b/presentation/article_route.go index 3d51dd0..1606b01 100644 --- a/presentation/article_route.go +++ b/presentation/article_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 ArticleRouter(api fiber.Router) { diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 16ccc7f..d0f8f33 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "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" // "gorm.io/gorm" - // "github.com/pahmiudahgede/senggoldong/middleware" + // "rijig/middleware" ) func AuthRouter(api fiber.Router) { @@ -29,12 +30,25 @@ func AuthRouter(api fiber.Router) { // authRoutes := api.Group("/auth") // authRoutes.Post("/send-otp", authHandler.SendOTP) // authRoutes.Post("/verify-otp", authHandler.VerifyOTP) - userRepo := repositories.NewUserRepository(config.DB) - authService := services.NewAuthService(userRepo) + // userRepo := repositories.NewUserRepository(config.DB) + // authService := services.NewAuthService(userRepo) + // authHandler := handler.NewAuthHandler(authService) + + // // Routes + // api.Post("/register", authHandler.Register) + // api.Post("/verify-otp", authHandler.VerifyOTP) + userRepo := repositories.NewUserRepository(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + redisRepo := repositories.NewRedisRepository(config.RedisClient) + + // Setup Service + authService := services.NewAuthService(userRepo, roleRepo, redisRepo) + + // Setup Handler authHandler := handler.NewAuthHandler(authService) - // Routes - api.Post("/register", authHandler.Register) + // Define Routes + api.Post("/register", authHandler.Register) // Route untuk registrasi api.Post("/verify-otp", authHandler.VerifyOTP) } diff --git a/presentation/banner_route.go b/presentation/banner_route.go index 7139c4f..411b0f0 100644 --- a/presentation/banner_route.go +++ b/presentation/banner_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 BannerRouter(api fiber.Router) { diff --git a/presentation/initialcoint_route.go b/presentation/initialcoint_route.go index 9ef0dd5..fba9237 100644 --- a/presentation/initialcoint_route.go +++ b/presentation/initialcoint_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 InitialCointRoute(api fiber.Router) { diff --git a/presentation/product_route.go b/presentation/product_route.go index 7afab8d..c1a74e6 100644 --- a/presentation/product_route.go +++ b/presentation/product_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 ProductRouter(api fiber.Router) { diff --git a/presentation/role_route.go b/presentation/role_route.go index 1895e7f..b221e8f 100644 --- a/presentation/role_route.go +++ b/presentation/role_route.go @@ -1,11 +1,12 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "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" ) func RoleRouter(api fiber.Router) { diff --git a/presentation/store_route.go b/presentation/store_route.go index 085deab..fd6c6e9 100644 --- a/presentation/store_route.go +++ b/presentation/store_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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) { diff --git a/presentation/trash_route.go b/presentation/trash_route.go index 5e5eb59..064bf98 100644 --- a/presentation/trash_route.go +++ b/presentation/trash_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 TrashRouter(api fiber.Router) { diff --git a/presentation/user_route.go b/presentation/user_route.go index 2e54a2e..86277eb 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -1,12 +1,13 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "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" ) func UserProfileRouter(api fiber.Router) { diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go index 3f1a8ee..15dea4d 100644 --- a/presentation/userpin_route.go +++ b/presentation/userpin_route.go @@ -1,12 +1,13 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "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" ) func UserPinRouter(api fiber.Router) { diff --git a/presentation/wilayahindonesia_route.go b/presentation/wilayahindonesia_route.go index 12aa1d4..9eb1ffa 100644 --- a/presentation/wilayahindonesia_route.go +++ b/presentation/wilayahindonesia_route.go @@ -1,13 +1,14 @@ package presentation import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + "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 WilayahRouter(api fiber.Router) { @@ -16,20 +17,20 @@ func WilayahRouter(api fiber.Router) { wilayahService := services.NewWilayahIndonesiaService(wilayahRepo) wilayahHandler := handler.NewWilayahImportHandler(wilayahService) - api.Post("/import/data-wilayah-indonesia", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), wilayahHandler.ImportWilayahData) + api.Post("/import/data-wilayah-indonesia", middleware.RoleMiddleware(utils.RoleAdministrator), wilayahHandler.ImportWilayahData) wilayahAPI := api.Group("/wilayah-indonesia") - wilayahAPI.Get("/provinces", middleware.AuthMiddleware, wilayahHandler.GetProvinces) - wilayahAPI.Get("/provinces/:provinceid", middleware.AuthMiddleware, wilayahHandler.GetProvinceByID) + wilayahAPI.Get("/provinces", wilayahHandler.GetProvinces) + wilayahAPI.Get("/provinces/:provinceid", wilayahHandler.GetProvinceByID) - wilayahAPI.Get("/regencies", middleware.AuthMiddleware, wilayahHandler.GetAllRegencies) - wilayahAPI.Get("/regencies/:regencyid", middleware.AuthMiddleware, wilayahHandler.GetRegencyByID) + wilayahAPI.Get("/regencies", wilayahHandler.GetAllRegencies) + wilayahAPI.Get("/regencies/:regencyid", wilayahHandler.GetRegencyByID) - wilayahAPI.Get("/districts", middleware.AuthMiddleware, wilayahHandler.GetAllDistricts) - wilayahAPI.Get("/districts/:districtid", middleware.AuthMiddleware, wilayahHandler.GetDistrictByID) + wilayahAPI.Get("/districts", wilayahHandler.GetAllDistricts) + wilayahAPI.Get("/districts/:districtid", wilayahHandler.GetDistrictByID) - wilayahAPI.Get("/villages", middleware.AuthMiddleware, wilayahHandler.GetAllVillages) - wilayahAPI.Get("/villages/:villageid", middleware.AuthMiddleware, wilayahHandler.GetVillageByID) + wilayahAPI.Get("/villages", wilayahHandler.GetAllVillages) + wilayahAPI.Get("/villages/:villageid", wilayahHandler.GetVillageByID) } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 3885718..ef85491 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -3,9 +3,10 @@ package router import ( "os" + "rijig/middleware" + "rijig/presentation" + "github.com/gofiber/fiber/v2" - "github.com/pahmiudahgede/senggoldong/middleware" - "github.com/pahmiudahgede/senggoldong/presentation" ) func SetupRoutes(app *fiber.App) { diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 0cbf2ff..99d4e8c 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -7,8 +7,9 @@ import ( "log" "time" + "rijig/config" + "github.com/go-redis/redis/v8" - "github.com/pahmiudahgede/senggoldong/config" ) var ctx = context.Background() @@ -137,4 +138,4 @@ func GetOTPFromRedis(phone string) (string, error) { return "", fmt.Errorf("failed to get OTP from Redis: %v", err) } return otpCode, nil -} \ No newline at end of file +} From 298b0de7254bd21203f2392f8737ceaac5c52f12 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 21 Mar 2025 23:34:53 +0700 Subject: [PATCH 13/48] update: completely register with otp request --- internal/handler/auth_handler.go | 30 +++-- internal/repositories/auth_repo.go | 81 +------------- internal/services/auth_service.go | 172 +++++++++++++++-------------- presentation/auth_route.go | 37 +------ utils/redis_caching.go | 53 ++++++--- 5 files changed, 147 insertions(+), 226 deletions(-) diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 4653495..896df83 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -13,41 +13,37 @@ type AuthHandler struct { } func NewAuthHandler(authService services.AuthService) *AuthHandler { - return &AuthHandler{ - authService: authService, - } + return &AuthHandler{authService} } -func (h *AuthHandler) Register(c *fiber.Ctx) error { - var request dto.RegisterRequest - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) +func (h *AuthHandler) RegisterUser(c *fiber.Ctx) error { + var req dto.RegisterRequest + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") } - if errors, valid := request.Validate(); !valid { + if errors, valid := req.Validate(); !valid { return utils.ValidationErrorResponse(c, errors) } - err := h.authService.RegisterUser(&request) + err := h.authService.RegisterUser(&req) if err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, nil, "OTP has been sent to your phone") + return utils.SuccessResponse(c, nil, "Kode OTP telah dikirimkan ke nomor WhatsApp anda") } func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { - var request dto.VerifyOTPRequest - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"invalid request body"}}) + var req dto.VerifyOTPRequest + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") } - err := h.authService.VerifyOTP(&request) + response, err := h.authService.VerifyOTP(&req) if err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, nil, "User successfully registered") + return utils.SuccessResponse(c, response, "Registration successful") } diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 0869d2b..24ed934 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -1,20 +1,14 @@ package repositories import ( - "context" - "encoding/json" - "fmt" - "time" - "rijig/model" - "github.com/go-redis/redis/v8" "gorm.io/gorm" ) type UserRepository interface { - SaveUser(user *model.User) (*model.User, error) - FindByPhone(phone string) (*model.User, error) + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) } type userRepository struct { @@ -22,83 +16,20 @@ type userRepository struct { } func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{db: db} + return &userRepository{db} } -func (r *userRepository) SaveUser(user *model.User) (*model.User, error) { +func (r *userRepository) CreateUser(user *model.User) (*model.User, error) { if err := r.db.Create(user).Error; err != nil { return nil, err } return user, nil } -func (r *userRepository) FindByPhone(phone string) (*model.User, error) { +func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) { var user model.User if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } return nil, err } return &user, nil -} - -type RedisRepository interface { - StoreData(key string, data interface{}, expiration time.Duration) error - GetData(key string) (interface{}, error) // Mengembalikan interface{} - DeleteData(key string) error -} - -type redisRepository struct { - client *redis.Client - ctx context.Context -} - -// NewRedisRepository membuat instance baru dari redisRepository -func NewRedisRepository(client *redis.Client) RedisRepository { - return &redisRepository{ - client: client, - ctx: context.Background(), - } -} - -// StoreData menyimpan data ke dalam Redis (dalam format JSON) -func (r *redisRepository) StoreData(key string, data interface{}, expiration time.Duration) error { - // Marshaling data ke JSON - jsonData, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("failed to marshal data: %v", err) - } - - // Simpan JSON ke Redis - err = r.client.Set(r.ctx, key, jsonData, expiration).Err() - if err != nil { - return fmt.Errorf("failed to store data in Redis: %v", err) - } - return nil -} - -// GetData mengambil data dari Redis berdasarkan key -func (r *redisRepository) GetData(key string) (interface{}, error) { - val, err := r.client.Get(r.ctx, key).Result() - if err != nil { - return nil, fmt.Errorf("failed to get data from Redis: %v", err) - } - - // Unmarshal data JSON kembali ke objek - var data interface{} - err = json.Unmarshal([]byte(val), &data) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal data: %v", err) - } - return data, nil -} - -// DeleteData menghapus data di Redis berdasarkan key -func (r *redisRepository) DeleteData(key string) error { - err := r.client.Del(r.ctx, key).Err() - if err != nil { - return fmt.Errorf("failed to delete data from Redis: %v", err) - } - return nil -} +} \ No newline at end of file diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 0c05b6e..80d4c7f 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,140 +1,142 @@ package services import ( - "encoding/json" + "errors" "fmt" - "log" - "time" - "math/rand" - "rijig/config" "rijig/dto" "rijig/internal/repositories" "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" ) type AuthService interface { - RegisterUser(request *dto.RegisterRequest) error - VerifyOTP(request *dto.VerifyOTPRequest) error + RegisterUser(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) } type authService struct { - userRepo repositories.UserRepository - roleRepo repositories.RoleRepository - redisRepo repositories.RedisRepository + userRepo repositories.UserRepository + roleRepo repositories.RoleRepository } -func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository, redisRepo repositories.RedisRepository) AuthService { - return &authService{ - userRepo: userRepo, - roleRepo: roleRepo, - redisRepo: redisRepo, - } +func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { + return &authService{userRepo, roleRepo} } -func (s *authService) RegisterUser(request *dto.RegisterRequest) error { +func (s *authService) RegisterUser(req *dto.RegisterRequest) error { - if request.RoleID == "" { - return fmt.Errorf("role_id cannot be empty") + userID := uuid.New().String() + + user := &model.User{ + Phone: req.Phone, + RoleID: req.RoleID, } - role, err := s.roleRepo.FindByID(request.RoleID) + err := utils.SetJSONData("user:"+userID, user, 10*time.Minute) if err != nil { - return fmt.Errorf("role not found: %v", err) - } - if role == nil { - return fmt.Errorf("role with ID %s not found", request.RoleID) + return err } - existingUser, err := s.userRepo.FindByPhone(request.Phone) + err = utils.SetStringData("user_phone:"+req.Phone, userID, 10*time.Minute) if err != nil { - return fmt.Errorf("failed to check existing user: %v", err) - } - if existingUser != nil { - return fmt.Errorf("phone number already registered") - } - - temporaryData := &model.User{ - Phone: request.Phone, - RoleID: request.RoleID, - } - - err = s.redisRepo.StoreData(request.Phone, temporaryData, 1*time.Hour) - if err != nil { - return fmt.Errorf("failed to store registration data in Redis: %v", err) + return err } otp := generateOTP() - err = s.redisRepo.StoreData("otp:"+request.Phone, otp, 10*time.Minute) + + err = config.SendWhatsAppMessage(req.Phone, fmt.Sprintf("Your OTP is: %s", otp)) if err != nil { - return fmt.Errorf("failed to store OTP in Redis: %v", err) + return err } - err = config.SendWhatsAppMessage(request.Phone, fmt.Sprintf("Your OTP is: %s", otp)) + err = utils.SetStringData("otp:"+req.Phone, otp, 10*time.Minute) if err != nil { - return fmt.Errorf("failed to send OTP via WhatsApp: %v", err) + return err } - log.Printf("OTP sent to phone number: %s", request.Phone) return nil } -func (s *authService) VerifyOTP(request *dto.VerifyOTPRequest) error { - - storedOTP, err := s.redisRepo.GetData("otp:" + request.Phone) +func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + storedOTP, err := utils.GetStringData("otp:" + req.Phone) if err != nil { - return fmt.Errorf("failed to retrieve OTP from Redis: %v", err) - } - if storedOTP != request.OTP { - return fmt.Errorf("invalid OTP") + return nil, err } - temporaryData, err := s.redisRepo.GetData(request.Phone) + if storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + userID, err := utils.GetStringData("user_phone:" + req.Phone) + if err != nil || userID == "" { + return nil, errors.New("user data not found in Redis") + } + + userData, err := utils.GetJSONData("user:" + userID) + if err != nil || userData == nil { + return nil, errors.New("user data not found in Redis") + } + + user := &model.User{ + Phone: userData["phone"].(string), + RoleID: userData["roleId"].(string), + } + + createdUser, err := s.userRepo.CreateUser(user) if err != nil { - return fmt.Errorf("failed to get registration data from Redis: %v", err) - } - if temporaryData == "" { - return fmt.Errorf("no registration data found for phone: %s", request.Phone) + return nil, err } - temporaryDataStr, ok := temporaryData.(string) - if !ok { - return fmt.Errorf("failed to assert data to string") - } - - temporaryDataBytes := []byte(temporaryDataStr) - - var user model.User - err = json.Unmarshal(temporaryDataBytes, &user) + role, err := s.roleRepo.FindByID(createdUser.RoleID) if err != nil { - return fmt.Errorf("failed to unmarshal registration data: %v", err) + return nil, err } - _, err = s.userRepo.SaveUser(&user) + token, err := generateJWTToken(createdUser.ID) if err != nil { - return fmt.Errorf("failed to save user to database: %v", err) + return nil, err } - err = s.redisRepo.DeleteData(request.Phone) - if err != nil { - return fmt.Errorf("failed to delete registration data from Redis: %v", err) - } - - err = s.redisRepo.DeleteData("otp:" + request.Phone) - if err != nil { - return fmt.Errorf("failed to delete OTP from Redis: %v", err) - } - - return nil + return &dto.UserDataResponse{ + UserID: createdUser.ID, + UserRole: role.RoleName, + Token: token, + }, nil } func generateOTP() string { - - return fmt.Sprintf("%06d", RandomInt(100000, 999999)) -} - -func RandomInt(min, max int) int { rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min+1) + min + otp := fmt.Sprintf("%06d", rand.Intn(1000000)) + return otp +} + +func generateJWTToken(userID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := &jwt.RegisteredClaims{ + Issuer: userID, + ExpiresAt: jwt.NewNumericDate(expirationTime), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + signedToken, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", err + } + + return signedToken, nil } diff --git a/presentation/auth_route.go b/presentation/auth_route.go index d0f8f33..4d91242 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -7,48 +7,15 @@ import ( "rijig/internal/services" "github.com/gofiber/fiber/v2" - // "gorm.io/gorm" - // "rijig/middleware" ) func AuthRouter(api fiber.Router) { - // userRepo := repositories.NewUserRepository(config.DB) - // roleRepo := repositories.NewRoleRepository(config.DB) - // userService := services.NewUserService(userRepo, roleRepo, secretKey) - // userHandler := handler.NewUserHandler(userService) - - // api.Post("/login", userHandler.Login) - // api.Post("/register", userHandler.Register) - // api.Post("/logout", middleware.AuthMiddleware, userHandler.Logout) - // userRepo := repositories.NewUserRepository(config.DB) - // authService := services.NewAuthService(userRepo, secretKey) - - // // Inisialisasi handler - // authHandler := handler.NewAuthHandler(authService) - - // // Endpoint OTP - // authRoutes := api.Group("/auth") - // authRoutes.Post("/send-otp", authHandler.SendOTP) - // authRoutes.Post("/verify-otp", authHandler.VerifyOTP) - // userRepo := repositories.NewUserRepository(config.DB) - // authService := services.NewAuthService(userRepo) - - // authHandler := handler.NewAuthHandler(authService) - - // // Routes - // api.Post("/register", authHandler.Register) - // api.Post("/verify-otp", authHandler.VerifyOTP) userRepo := repositories.NewUserRepository(config.DB) roleRepo := repositories.NewRoleRepository(config.DB) - redisRepo := repositories.NewRedisRepository(config.RedisClient) + authService := services.NewAuthService(userRepo, roleRepo) - // Setup Service - authService := services.NewAuthService(userRepo, roleRepo, redisRepo) - - // Setup Handler authHandler := handler.NewAuthHandler(authService) - // Define Routes - api.Post("/register", authHandler.Register) // Route untuk registrasi + api.Post("/register", authHandler.RegisterUser) api.Post("/verify-otp", authHandler.VerifyOTP) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 99d4e8c..7b7bc18 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -124,18 +124,43 @@ func GetStringData(key string) (string, error) { return val, nil } -func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { - err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() - if err != nil { - return fmt.Errorf("failed to store OTP in Redis: %v", err) - } - return nil -} +// func SetStringData(key, value string, expiration time.Duration) error { +// if expiration == 0 { +// expiration = defaultExpiration +// } -func GetOTPFromRedis(phone string) (string, error) { - otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() - if err != nil { - return "", fmt.Errorf("failed to get OTP from Redis: %v", err) - } - return otpCode, nil -} +// err := config.RedisClient.Set(ctx, key, value, expiration).Err() +// if err != nil { +// return fmt.Errorf("Error setting string data in Redis with key: %s: %v", key, err) +// } + +// log.Printf("String data stored in Redis with key: %s", key) +// return nil +// } + +// GetStringData retrieves a string value from Redis by key +// func GetStringData(key string) (string, error) { +// val, err := config.RedisClient.Get(ctx, key).Result() +// if err == redis.Nil { +// return "", nil +// } else if err != nil { +// return "", fmt.Errorf("Error retrieving string data from Redis with key: %s: %v", key, err) +// } + +// return val, nil +// } +// func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { +// err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() +// if err != nil { +// return fmt.Errorf("failed to store OTP in Redis: %v", err) +// } +// return nil +// } + +// func GetOTPFromRedis(phone string) (string, error) { +// otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() +// if err != nil { +// return "", fmt.Errorf("failed to get OTP from Redis: %v", err) +// } +// return otpCode, nil +// } From ba4645fef9ad6f90edbca2311ee3b08c15ea4d76 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 22 Mar 2025 00:52:56 +0700 Subject: [PATCH 14/48] feat: add cooldown for request otp --- internal/services/auth_service.go | 39 +++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 80d4c7f..7d464d0 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -29,16 +29,35 @@ func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories. return &authService{userRepo, roleRepo} } +const otpCooldown = 30 + func (s *authService) RegisterUser(req *dto.RegisterRequest) error { + user, err := s.userRepo.GetUserByPhone(req.Phone) + if err == nil && user != nil { + return errors.New("phone number already registered") + } + + lastOtpSent, err := utils.GetStringData("otp_sent:" + req.Phone) + if err == nil && lastOtpSent != "" { + lastSentTime, err := time.Parse(time.RFC3339, lastOtpSent) + if err != nil { + return errors.New("invalid OTP sent timestamp") + } + + if time.Since(lastSentTime).Seconds() < otpCooldown { + return errors.New("please wait before requesting another OTP") + } + } + userID := uuid.New().String() - user := &model.User{ + user = &model.User{ Phone: req.Phone, RoleID: req.RoleID, } - err := utils.SetJSONData("user:"+userID, user, 10*time.Minute) + err = utils.SetJSONData("user:"+userID, user, 10*time.Minute) if err != nil { return err } @@ -60,10 +79,21 @@ func (s *authService) RegisterUser(req *dto.RegisterRequest) error { return err } + err = utils.SetStringData("otp_sent:"+req.Phone, time.Now().Format(time.RFC3339), 10*time.Minute) + if err != nil { + return err + } + return nil } func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + isLoggedIn, err := utils.GetStringData("user_logged_in:" + req.Phone) + if err == nil && isLoggedIn == "true" { + return nil, errors.New("you are already logged in") + } + storedOTP, err := utils.GetStringData("otp:" + req.Phone) if err != nil { return nil, err @@ -107,6 +137,11 @@ func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataRespons return nil, err } + err = utils.SetStringData("user_logged_in:"+req.Phone, "true", 0) + if err != nil { + return nil, err + } + return &dto.UserDataResponse{ UserID: createdUser.ID, UserRole: role.RoleName, From e65d6383fcb702f7934e113ac2e6a914297490b6 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 24 Mar 2025 05:46:54 +0700 Subject: [PATCH 15/48] update: completely auth statemrnt and session management --- dto/auth_dto.go | 3 +- internal/handler/auth_handler.go | 47 +++++-- internal/repositories/auth_repo.go | 15 ++- internal/services/auth_service.go | 202 +++++++++++++++++------------ presentation/auth_route.go | 6 +- 5 files changed, 178 insertions(+), 95 deletions(-) diff --git a/dto/auth_dto.go b/dto/auth_dto.go index de3674d..367e0e9 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -11,7 +11,8 @@ type RegisterRequest struct { } type VerifyOTPRequest struct { - Phone string `json:"phone"` + RoleID string `json:"role_id"` + Phone string `json:"phone"` OTP string `json:"otp"` } diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index 896df83..fe3fa6f 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,6 +1,7 @@ package handler import ( + "log" "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -16,34 +17,64 @@ func NewAuthHandler(authService services.AuthService) *AuthHandler { return &AuthHandler{authService} } -func (h *AuthHandler) RegisterUser(c *fiber.Ctx) error { +func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { var req dto.RegisterRequest + if err := c.BodyParser(&req); err != nil { return utils.ErrorResponse(c, "Invalid request body") } - if errors, valid := req.Validate(); !valid { - return utils.ValidationErrorResponse(c, errors) + if req.Phone == "" || req.RoleID == "" { + return utils.ErrorResponse(c, "Phone number and role ID are required") } - err := h.authService.RegisterUser(&req) - if err != nil { + if err := h.authService.RegisterOrLogin(&req); err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, nil, "Kode OTP telah dikirimkan ke nomor WhatsApp anda") + return utils.SuccessResponse(c, nil, "OTP sent successfully") } -func (h *AuthHandler) VerifyOTP(c *fiber.Ctx) error { +func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { var req dto.VerifyOTPRequest + if err := c.BodyParser(&req); err != nil { return utils.ErrorResponse(c, "Invalid request body") } + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + response, err := h.authService.VerifyOTP(&req) if err != nil { return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, response, "Registration successful") + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + phoneKey := "user_phone:" + userID + phone, err := utils.GetStringData(phoneKey) + if err != nil || phone == "" { + + log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) + return utils.ErrorResponse(c, "Phone number is missing or invalid session data") + } + + err = h.authService.Logout(userID, phone) + if err != nil { + + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") } diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go index 24ed934..a9466ca 100644 --- a/internal/repositories/auth_repo.go +++ b/internal/repositories/auth_repo.go @@ -9,6 +9,7 @@ import ( type UserRepository interface { CreateUser(user *model.User) (*model.User, error) GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error) } type userRepository struct { @@ -32,4 +33,16 @@ func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) { return nil, err } return &user, nil -} \ No newline at end of file +} + +func (r *userRepository) GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 7d464d0..92da706 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -12,12 +12,14 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/google/uuid" ) +const otpCooldown = 30 * time.Second + type AuthService interface { - RegisterUser(req *dto.RegisterRequest) error + RegisterOrLogin(req *dto.RegisterRequest) error VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, phone string) error } type authService struct { @@ -29,77 +31,66 @@ func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories. return &authService{userRepo, roleRepo} } -const otpCooldown = 30 +func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { -func (s *authService) RegisterUser(req *dto.RegisterRequest) error { - - user, err := s.userRepo.GetUserByPhone(req.Phone) - if err == nil && user != nil { - return errors.New("phone number already registered") + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err } - lastOtpSent, err := utils.GetStringData("otp_sent:" + req.Phone) - if err == nil && lastOtpSent != "" { - lastSentTime, err := time.Parse(time.RFC3339, lastOtpSent) - if err != nil { - return errors.New("invalid OTP sent timestamp") - } - - if time.Since(lastSentTime).Seconds() < otpCooldown { - return errors.New("please wait before requesting another OTP") - } + user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) + if err != nil { + return fmt.Errorf("failed to check existing user: %w", err) } - userID := uuid.New().String() + if user != nil { + return s.sendOTP(req.Phone) + } user = &model.User{ Phone: req.Phone, RoleID: req.RoleID, } - err = utils.SetJSONData("user:"+userID, user, 10*time.Minute) + createdUser, err := s.userRepo.CreateUser(user) if err != nil { + return fmt.Errorf("failed to create new user: %w", err) + } + + if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { return err } - err = utils.SetStringData("user_phone:"+req.Phone, userID, 10*time.Minute) - if err != nil { - return err + return s.sendOTP(req.Phone) +} + +func (s *authService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil } - - otp := generateOTP() - - err = config.SendWhatsAppMessage(req.Phone, fmt.Sprintf("Your OTP is: %s", otp)) - if err != nil { - return err + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") } - - err = utils.SetStringData("otp:"+req.Phone, otp, 10*time.Minute) - if err != nil { - return err - } - - err = utils.SetStringData("otp_sent:"+req.Phone, time.Now().Format(time.RFC3339), 10*time.Minute) - if err != nil { - return err - } - return nil } +func (s *authService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - isLoggedIn, err := utils.GetStringData("user_logged_in:" + req.Phone) - if err == nil && isLoggedIn == "true" { - return nil, errors.New("you are already logged in") - } - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil { - return nil, err - } - - if storedOTP == "" { + if err != nil || storedOTP == "" { return nil, errors.New("OTP expired or not found") } @@ -107,71 +98,116 @@ func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataRespons return nil, errors.New("invalid OTP") } - userID, err := utils.GetStringData("user_phone:" + req.Phone) - if err != nil || userID == "" { - return nil, errors.New("user data not found in Redis") + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) } - userData, err := utils.GetJSONData("user:" + userID) - if err != nil || userData == nil { - return nil, errors.New("user data not found in Redis") + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) } - user := &model.User{ - Phone: userData["phone"].(string), - RoleID: userData["roleId"].(string), + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: req.RoleID, + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser } - createdUser, err := s.userRepo.CreateUser(user) + token, err := s.generateJWTToken(user.ID) if err != nil { return nil, err } - role, err := s.roleRepo.FindByID(createdUser.RoleID) + role, err := s.roleRepo.FindByID(user.RoleID) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get role: %w", err) } - token, err := generateJWTToken(createdUser.ID) - if err != nil { - return nil, err - } - - err = utils.SetStringData("user_logged_in:"+req.Phone, "true", 0) - if err != nil { + if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { return nil, err } return &dto.UserDataResponse{ - UserID: createdUser.ID, + UserID: user.ID, UserRole: role.RoleName, Token: token, }, nil } -func generateOTP() string { - rand.Seed(time.Now().UnixNano()) - otp := fmt.Sprintf("%06d", rand.Intn(1000000)) - return otp +func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { + if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { + return fmt.Errorf("failed to store user data in Redis: %w", err) + } + + if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { + return fmt.Errorf("failed to store user phone in Redis: %w", err) + } + + return nil } -func generateJWTToken(userID string) (string, error) { - +func (s *authService) generateJWTToken(userID string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) - claims := &jwt.RegisteredClaims{ - Issuer: userID, + Subject: userID, ExpiresAt: jwt.NewNumericDate(expirationTime), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - secretKey := config.GetSecretKey() - signedToken, err := token.SignedString([]byte(secretKey)) - if err != nil { - return "", err + return token.SignedString([]byte(secretKey)) +} + +func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s", userID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, } - return signedToken, nil + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authService) Logout(userID, phone string) error { + keys := []string{ + "session:" + userID, + "session_token:" + userID, + "user_logged_in:" + userID, + "user:" + userID, + "user_phone:" + userID, + "otp_sent:" + phone, + } + + for _, key := range keys { + if err := utils.DeleteData(key); err != nil { + return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) + } + } + + return nil +} + +func generateOTP() string { + randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) + return fmt.Sprintf("%04d", randGenerator.Intn(10000)) } diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 4d91242..1ae2d9a 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -5,6 +5,7 @@ import ( "rijig/internal/handler" "rijig/internal/repositories" "rijig/internal/services" + "rijig/middleware" "github.com/gofiber/fiber/v2" ) @@ -16,6 +17,7 @@ func AuthRouter(api fiber.Router) { authHandler := handler.NewAuthHandler(authService) - api.Post("/register", authHandler.RegisterUser) - api.Post("/verify-otp", authHandler.VerifyOTP) + api.Post("/auth", authHandler.RegisterOrLoginHandler) + api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + api.Post("/verify-otp", authHandler.VerifyOTPHandler) } From 7b9247014ab557dd5c741b1375ce1588c246bad1 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 5 May 2025 02:18:17 +0700 Subject: [PATCH 16/48] refact:auth optimize and session management --- cmd/main.go | 42 ++- config/database.go | 2 + config/redis.go | 13 +- config/server.go | 4 +- dto/auth/auth_admin_dto.go | 130 +++++++ dto/auth/auth_masyarakat_dto.go | 1 + dto/auth/auth_pengelola_dto.go | 133 +++++++ dto/auth/auth_pengepul_dto.go | 1 + dto/auth_dto.go | 11 +- dto/user_dto.go | 6 - internal/handler/auth/auth_admin_handler.go | 80 +++++ .../handler/auth/auth_masyarakat_handler.go | 81 +++++ .../handler/auth/auth_pengepul_handler.go | 81 +++++ .../handler/auth/auth_pnegelola_handler.go | 81 +++++ internal/handler/auth_handler.go | 116 +++--- internal/repositories/auth/auth_admin_repo.go | 93 +++++ .../repositories/auth/auth_masyarakat_repo.go | 48 +++ .../repositories/auth/auth_pengelola_repo.go | 48 +++ .../repositories/auth/auth_pengepul_repo.go | 48 +++ internal/repositories/role_repo.go | 10 + internal/services/auth/auth_admin_service.go | 191 ++++++++++ .../services/auth/auth_masyarakat_service.go | 170 +++++++++ .../services/auth/auth_pengelola_service.go | 170 +++++++++ .../services/auth/auth_pengepul_service.go | 170 +++++++++ internal/services/auth/otp.go | 14 + internal/services/auth_service.go | 340 +++++++++--------- internal/services/user_service.go | 16 +- middleware/auth_middleware.go | 17 +- model/company_profile_model.go | 24 ++ model/identitycard_model.go | 25 ++ model/role_model.go | 2 +- model/user_model.go | 27 +- presentation/auth/auth_admin_route.go | 35 ++ presentation/auth/auth_masyarakat_route.go | 26 ++ presentation/auth/auth_pengelola_route.go | 26 ++ presentation/auth/auth_pengepul_route.go | 26 ++ presentation/auth_route.go | 34 +- presentation/userpin_route.go | 9 +- router/setup_routes.go.go | 9 +- utils/redis_caching.go | 90 ++--- utils/regexp_formatter.go | 44 +++ utils/role_permission.go | 8 +- 42 files changed, 2140 insertions(+), 362 deletions(-) create mode 100644 dto/auth/auth_admin_dto.go create mode 100644 dto/auth/auth_masyarakat_dto.go create mode 100644 dto/auth/auth_pengelola_dto.go create mode 100644 dto/auth/auth_pengepul_dto.go create mode 100644 internal/handler/auth/auth_admin_handler.go create mode 100644 internal/handler/auth/auth_masyarakat_handler.go create mode 100644 internal/handler/auth/auth_pengepul_handler.go create mode 100644 internal/handler/auth/auth_pnegelola_handler.go create mode 100644 internal/repositories/auth/auth_admin_repo.go create mode 100644 internal/repositories/auth/auth_masyarakat_repo.go create mode 100644 internal/repositories/auth/auth_pengelola_repo.go create mode 100644 internal/repositories/auth/auth_pengepul_repo.go create mode 100644 internal/services/auth/auth_admin_service.go create mode 100644 internal/services/auth/auth_masyarakat_service.go create mode 100644 internal/services/auth/auth_pengelola_service.go create mode 100644 internal/services/auth/auth_pengepul_service.go create mode 100644 internal/services/auth/otp.go create mode 100644 model/company_profile_model.go create mode 100644 model/identitycard_model.go create mode 100644 presentation/auth/auth_admin_route.go create mode 100644 presentation/auth/auth_masyarakat_route.go create mode 100644 presentation/auth/auth_pengelola_route.go create mode 100644 presentation/auth/auth_pengepul_route.go create mode 100644 utils/regexp_formatter.go diff --git a/cmd/main.go b/cmd/main.go index d4ee0e9..024c357 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,27 +13,33 @@ func main() { app := fiber.New() app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:3000", - AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", - AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", - AllowCredentials: true, + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,PATCH,DELETE", + AllowHeaders: "Content-Type,x-api-key", })) - app.Use(func(c *fiber.Ctx) error { - c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - c.Set("Access-Control-Allow-Credentials", "true") - return c.Next() - }) + // app.Use(cors.New(cors.Config{ + // AllowOrigins: "http://localhost:3000", + // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", + // AllowCredentials: true, + // })) - app.Options("*", func(c *fiber.Ctx) error { - c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - c.Set("Access-Control-Allow-Credentials", "true") - return c.SendStatus(fiber.StatusNoContent) - }) + // app.Use(func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.Next() + // }) + + // app.Options("*", func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.SendStatus(fiber.StatusNoContent) + // }) router.SetupRoutes(app) diff --git a/config/database.go b/config/database.go index 52ac433..34451f3 100644 --- a/config/database.go +++ b/config/database.go @@ -45,6 +45,8 @@ func ConnectDatabase() { &model.Role{}, &model.UserPin{}, &model.Address{}, + &model.IdentityCard{}, + &model.CompanyProfile{}, // =>user preparation<= // =>store preparation<= diff --git a/config/redis.go b/config/redis.go index 1d93165..61801f0 100644 --- a/config/redis.go +++ b/config/redis.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "strconv" "github.com/go-redis/redis/v8" ) @@ -13,13 +14,21 @@ var RedisClient *redis.Client var Ctx = context.Background() func ConnectRedis() { + + redisDBStr := os.Getenv("REDIS_DB") + + redisDB, err := strconv.Atoi(redisDBStr) + if err != nil { + log.Fatalf("Error converting REDIS_DB to integer: %v", err) + } + RedisClient = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")), Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, + DB: redisDB, }) - _, err := RedisClient.Ping(Ctx).Result() + _, err = RedisClient.Ping(Ctx).Result() if err != nil { log.Fatalf("Error connecting to Redis: %v", err) } diff --git a/config/server.go b/config/server.go index 10caf70..9f5cf5e 100644 --- a/config/server.go +++ b/config/server.go @@ -18,8 +18,8 @@ func StartServer(app *fiber.App) { address := fmt.Sprintf("%s:%s", host, port) - log.Printf("Server is running on http://%s", address) + log.Printf("server berjalan di http://%s", address) if err := app.Listen(address); err != nil { - log.Fatalf("Error starting server: %v", err) + log.Fatalf("gagal saat menjalankan server: %v", err) } } diff --git a/dto/auth/auth_admin_dto.go b/dto/auth/auth_admin_dto.go new file mode 100644 index 0000000..b15a248 --- /dev/null +++ b/dto/auth/auth_admin_dto.go @@ -0,0 +1,130 @@ +package dto + +import ( + "regexp" + "strings" +) + +type LoginAdminRequest struct { + Deviceid string `json:"device_id"` + Email string `json:"email"` + Password string `json:"password"` +} + +type LoginResponse struct { + UserID string `json:"user_id"` + Role string `json:"role"` + Token string `json:"token"` +} + +type RegisterAdminRequest struct { + Name string `json:"name"` + Gender string `json:"gender"` + Dateofbirth string `json:"dateofbirth"` + Placeofbirth string `json:"placeofbirth"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + PasswordConfirm string `json:"password_confirm"` +} + +type UserAdminDataResponse struct { + UserID string `json:"user_id"` + Name string `json:"name"` + Gender string `json:"gender"` + Dateofbirth string `json:"dateofbirth"` + Placeofbirth string `json:"placeofbirth"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + Role string `json:"role"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func (r *RegisterAdminRequest) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + + if strings.TrimSpace(r.Gender) == "" { + errors["gender"] = append(errors["gender"], "Gender is required") + } else if r.Gender != "male" && r.Gender != "female" { + errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'") + } + + if strings.TrimSpace(r.Dateofbirth) == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") + } + + if strings.TrimSpace(r.Placeofbirth) == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required") + } + + if strings.TrimSpace(r.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone is required") + } else if !IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") + } + + if strings.TrimSpace(r.Email) == "" { + errors["email"] = append(errors["email"], "Email is required") + } else if !IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + + if len(r.Password) < 6 { + errors["password"] = append(errors["password"], "Password must be at least 6 characters") + } else if !IsValidPassword(r.Password) { + errors["password"] = append(errors["password"], "Password must contain at least one uppercase letter, one number, and one special character") + } + + if r.Password != r.PasswordConfirm { + errors["password_confirm"] = append(errors["password_confirm"], "Password and confirmation do not match") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func IsValidPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsValidEmail(email string) bool { + re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + + if len(password) < 6 { + return false + } + + hasUpper := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + if char >= 'A' && char <= 'Z' { + hasUpper = true + } else if char >= '0' && char <= '9' { + hasDigit = true + } else if isSpecialCharacter(char) { + hasSpecial = true + } + } + + return hasUpper && hasDigit && hasSpecial +} + +func isSpecialCharacter(char rune) bool { + specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" + return strings.ContainsRune(specialChars, char) +} diff --git a/dto/auth/auth_masyarakat_dto.go b/dto/auth/auth_masyarakat_dto.go new file mode 100644 index 0000000..cbd8f99 --- /dev/null +++ b/dto/auth/auth_masyarakat_dto.go @@ -0,0 +1 @@ +package dto \ No newline at end of file diff --git a/dto/auth/auth_pengelola_dto.go b/dto/auth/auth_pengelola_dto.go new file mode 100644 index 0000000..58c916c --- /dev/null +++ b/dto/auth/auth_pengelola_dto.go @@ -0,0 +1,133 @@ +package dto + +import ( + "regexp" + "rijig/utils" +) + +type LoginPengelolaRequest struct { + Phone string `json:"phone"` +} + +func (r *LoginPengelolaRequest) ValidateLogin() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Phone == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Phone number is not valid") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type VerifLoginPengelolaRequest struct { + Phone string `json:"phone"` + Otp string `json:"verif_otp"` +} + +func (r *VerifLoginPengelolaRequest) ValidateVerifLogin() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Phone == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Phone number is not valid") + } + + if r.Otp == "" { + errors["otp"] = append(errors["otp"], "OTP is required") + } else if len(r.Otp) != 6 { + errors["otp"] = append(errors["otp"], "OTP must be 6 digits") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type LoginPengelolaResponse struct { + UserID string `json:"user_id"` + Role string `json:"role"` + Token string `json:"token"` +} + +type PengelolaIdentityCard struct { + Cardphoto string `json:"cardphoto"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + District string `json:"district"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` +} + +func (r *PengelolaIdentityCard) ValidateIDcard() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Cardphoto == "" { + errors["cardphoto"] = append(errors["cardphoto"], "Card photo is required") + } + + if r.Identificationumber == "" { + errors["identificationumber"] = append(errors["identificationumber"], "Identification number is required") + } + + if r.Dateofbirth == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") + } else if !isValidDate(r.Dateofbirth) { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type PengelolaCompanyProfile struct { + CompanyName string `json:"company_name"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` +} + +func (r *PengelolaCompanyProfile) ValidateCompany() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.CompanyName == "" { + errors["company_name"] = append(errors["company_name"], "Company name is required") + } + + if r.CompanyPhone == "" { + errors["company_phone"] = append(errors["company_phone"], "Company phone is required") + } else if !utils.IsValidPhoneNumber(r.CompanyPhone) { + errors["company_phone"] = append(errors["company_phone"], "Invalid phone number format") + } + + if r.CompanyEmail == "" { + errors["company_email"] = append(errors["company_email"], "Company email is required") + } else if !utils.IsValidEmail(r.CompanyEmail) { + errors["company_email"] = append(errors["company_email"], "Invalid email format") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func isValidDate(date string) bool { + re := regexp.MustCompile(`^\d{2}-\d{2}-\d{4}$`) + return re.MatchString(date) +} diff --git a/dto/auth/auth_pengepul_dto.go b/dto/auth/auth_pengepul_dto.go new file mode 100644 index 0000000..cbd8f99 --- /dev/null +++ b/dto/auth/auth_pengepul_dto.go @@ -0,0 +1 @@ +package dto \ No newline at end of file diff --git a/dto/auth_dto.go b/dto/auth_dto.go index 367e0e9..5782667 100644 --- a/dto/auth_dto.go +++ b/dto/auth_dto.go @@ -6,14 +6,13 @@ import ( ) type RegisterRequest struct { - RoleID string `json:"role_id"` - Phone string `json:"phone"` + Phone string `json:"phone"` } type VerifyOTPRequest struct { - RoleID string `json:"role_id"` - Phone string `json:"phone"` + Phone string `json:"phone"` OTP string `json:"otp"` + DeviceID string `json:"device_id"` } type UserDataResponse struct { @@ -25,10 +24,6 @@ type UserDataResponse struct { func (r *RegisterRequest) Validate() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.RoleID) == "" { - errors["role_id"] = append(errors["role_id"], "Role ID is required") - } - if strings.TrimSpace(r.Phone) == "" { errors["phone"] = append(errors["phone"], "Phone is required") } else if !IsValidPhoneNumber(r.Phone) { diff --git a/dto/user_dto.go b/dto/user_dto.go index b527605..0d74533 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -37,12 +37,6 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } - // if strings.TrimSpace(r.Email) == "" { - // errors["email"] = append(errors["email"], "Email is required") - // } else if !IsValidEmail(r.Email) { - // errors["email"] = append(errors["email"], "Invalid email format") - // } - if len(errors) > 0 { return errors, false } diff --git a/internal/handler/auth/auth_admin_handler.go b/internal/handler/auth/auth_admin_handler.go new file mode 100644 index 0000000..98341b4 --- /dev/null +++ b/internal/handler/auth/auth_admin_handler.go @@ -0,0 +1,80 @@ +package handler + +import ( + "log" + dto "rijig/dto/auth" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthAdminHandler struct { + UserService services.AuthAdminService +} + +func NewAuthAdminHandler(userService services.AuthAdminService) *AuthAdminHandler { + return &AuthAdminHandler{UserService: userService} +} + +func (h *AuthAdminHandler) RegisterAdmin(c *fiber.Ctx) error { + var request dto.RegisterAdminRequest + + if err := c.BodyParser(&request); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to parse request body") + } + + errors, valid := request.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + user, err := h.UserService.RegisterAdmin(&request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) + } + + return utils.SuccessResponse(c, user, "Admin registered successfully") +} + +func (h *AuthAdminHandler) LoginAdmin(c *fiber.Ctx) error { + var request dto.LoginAdminRequest + + if err := c.BodyParser(&request); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to parse request body") + } + + loginResponse, err := h.UserService.LoginAdmin(&request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) + } + + return utils.SuccessResponse(c, loginResponse, "Login successful") +} + +func (h *AuthAdminHandler) LogoutAdmin(c *fiber.Ctx) error { + // Ambil userID dari c.Locals + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + log.Println("Error: UserID is nil or empty") + return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") + } + + // Ambil deviceID dari header atau c.Locals + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + log.Printf("UserID: %s, DeviceID: %s", userID, deviceID) + + err := h.UserService.LogoutAdmin(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out") +} + diff --git a/internal/handler/auth/auth_masyarakat_handler.go b/internal/handler/auth/auth_masyarakat_handler.go new file mode 100644 index 0000000..426e689 --- /dev/null +++ b/internal/handler/auth/auth_masyarakat_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthMasyarakatHandler struct { + authMasyarakatService services.AuthMasyarakatService +} + +func NewAuthMasyarakatHandler(authMasyarakatService services.AuthMasyarakatService) *AuthMasyarakatHandler { + return &AuthMasyarakatHandler{authMasyarakatService} +} + +func (h *AuthMasyarakatHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authMasyarakatService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthMasyarakatHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authMasyarakatService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authMasyarakatService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth/auth_pengepul_handler.go b/internal/handler/auth/auth_pengepul_handler.go new file mode 100644 index 0000000..50da9ab --- /dev/null +++ b/internal/handler/auth/auth_pengepul_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthPengepulHandler struct { + authPengepulService services.AuthMasyarakatService +} + +func NewAuthPengepulHandler(authPengepulService services.AuthMasyarakatService) *AuthPengepulHandler { + return &AuthPengepulHandler{authPengepulService} +} + +func (h *AuthPengepulHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authPengepulService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthPengepulHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authPengepulService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authPengepulService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth/auth_pnegelola_handler.go b/internal/handler/auth/auth_pnegelola_handler.go new file mode 100644 index 0000000..1491f74 --- /dev/null +++ b/internal/handler/auth/auth_pnegelola_handler.go @@ -0,0 +1,81 @@ +package handler + +import ( + "log" + "rijig/dto" + services "rijig/internal/services/auth" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthPengelolaHandler struct { + authPengelolaService services.AuthMasyarakatService +} + +func NewAuthPengelolaHandler(authPengelolaService services.AuthMasyarakatService) *AuthPengelolaHandler { + return &AuthPengelolaHandler{authPengelolaService} +} + +func (h *AuthPengelolaHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { + var req dto.RegisterRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.Phone == "" { + return utils.ErrorResponse(c, "Phone number is required") + } + + if err := h.authPengelolaService.RegisterOrLogin(&req); err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "OTP sent successfully") +} + +func (h *AuthPengelolaHandler) VerifyOTPHandler(c *fiber.Ctx) error { + var req dto.VerifyOTPRequest + + if err := c.BodyParser(&req); err != nil { + return utils.ErrorResponse(c, "Invalid request body") + } + + if req.OTP == "" { + return utils.ErrorResponse(c, "OTP is required") + } + + if req.DeviceID == "" { + return utils.ErrorResponse(c, "DeviceID is required") + } + + response, err := h.authPengelolaService.VerifyOTP(&req) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, response, "Registration/Login successful") +} + +func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + deviceID, ok := c.Locals("device_id").(string) + if !ok || deviceID == "" { + log.Println("Error: DeviceID is nil or empty") + return utils.ErrorResponse(c, "DeviceID is required") + } + + err := h.authPengelolaService.Logout(userID, deviceID) + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index fe3fa6f..0782d02 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -1,80 +1,80 @@ package handler -import ( - "log" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" +// import ( +// "log" +// "rijig/dto" +// "rijig/internal/services" +// "rijig/utils" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -type AuthHandler struct { - authService services.AuthService -} +// type AuthHandler struct { +// authService services.AuthService +// } -func NewAuthHandler(authService services.AuthService) *AuthHandler { - return &AuthHandler{authService} -} +// func NewAuthHandler(authService services.AuthService) *AuthHandler { +// return &AuthHandler{authService} +// } -func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { - var req dto.RegisterRequest +// func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { +// var req dto.RegisterRequest - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } +// if err := c.BodyParser(&req); err != nil { +// return utils.ErrorResponse(c, "Invalid request body") +// } - if req.Phone == "" || req.RoleID == "" { - return utils.ErrorResponse(c, "Phone number and role ID are required") - } +// if req.Phone == "" || req.RoleID == "" { +// return utils.ErrorResponse(c, "Phone number and role ID are required") +// } - if err := h.authService.RegisterOrLogin(&req); err != nil { - return utils.ErrorResponse(c, err.Error()) - } +// if err := h.authService.RegisterOrLogin(&req); err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, nil, "OTP sent successfully") -} +// return utils.SuccessResponse(c, nil, "OTP sent successfully") +// } -func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { - var req dto.VerifyOTPRequest +// func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { +// var req dto.VerifyOTPRequest - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } +// if err := c.BodyParser(&req); err != nil { +// return utils.ErrorResponse(c, "Invalid request body") +// } - if req.OTP == "" { - return utils.ErrorResponse(c, "OTP is required") - } +// if req.OTP == "" { +// return utils.ErrorResponse(c, "OTP is required") +// } - response, err := h.authService.VerifyOTP(&req) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } +// response, err := h.authService.VerifyOTP(&req) +// if err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, response, "Registration/Login successful") -} +// return utils.SuccessResponse(c, response, "Registration/Login successful") +// } -func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { +// func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } +// userID, ok := c.Locals("userID").(string) +// if !ok || userID == "" { +// return utils.ErrorResponse(c, "User is not logged in or invalid session") +// } - phoneKey := "user_phone:" + userID - phone, err := utils.GetStringData(phoneKey) - if err != nil || phone == "" { +// phoneKey := "user_phone:" + userID +// phone, err := utils.GetStringData(phoneKey) +// if err != nil || phone == "" { - log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) - return utils.ErrorResponse(c, "Phone number is missing or invalid session data") - } +// log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) +// return utils.ErrorResponse(c, "Phone number is missing or invalid session data") +// } - err = h.authService.Logout(userID, phone) - if err != nil { +// err = h.authService.Logout(userID, phone) +// if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } +// log.Printf("Error during logout process for user %s: %v", userID, err) +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, nil, "Logged out successfully") -} +// return utils.SuccessResponse(c, nil, "Logged out successfully") +// } diff --git a/internal/repositories/auth/auth_admin_repo.go b/internal/repositories/auth/auth_admin_repo.go new file mode 100644 index 0000000..4652e1d --- /dev/null +++ b/internal/repositories/auth/auth_admin_repo.go @@ -0,0 +1,93 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthAdminRepository interface { + FindByEmail(email string) (*model.User, error) + FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) + FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) + FindByPhone(phone string) (*model.User, error) + FindByEmailOrPhone(identifier string) (*model.User, error) + FindRoleByName(roleName string) (*model.Role, error) + CreateUser(user *model.User) (*model.User, error) +} + +type authAdminRepository struct { + DB *gorm.DB +} + +func NewAuthAdminRepository(db *gorm.DB) AuthAdminRepository { + return &authAdminRepository{DB: db} +} + +func (r *authAdminRepository) FindByEmail(email string) (*model.User, error) { + var user model.User + err := r.DB.Preload("Role").Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) { + var user model.User + err := r.DB.Where("email = ? AND role_id = ?", email, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) { + var user model.User + err := r.DB.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindByPhone(phone string) (*model.User, error) { + var user model.User + err := r.DB.Where("phone = ?", phone).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) FindByEmailOrPhone(identifier string) (*model.User, error) { + var user model.User + err := r.DB.Where("email = ? OR phone = ?", identifier, identifier).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +func (r *authAdminRepository) CreateUser(user *model.User) (*model.User, error) { + err := r.DB.Create(user).Error + if err != nil { + return nil, err + } + return user, nil +} + +func (r *authAdminRepository) FindRoleByName(roleName string) (*model.Role, error) { + var role model.Role + err := r.DB.Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} diff --git a/internal/repositories/auth/auth_masyarakat_repo.go b/internal/repositories/auth/auth_masyarakat_repo.go new file mode 100644 index 0000000..f49bfd0 --- /dev/null +++ b/internal/repositories/auth/auth_masyarakat_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthMasyarakatRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authMasyarakatRepository struct { + db *gorm.DB +} + +func NewAuthMasyarakatRepositories(db *gorm.DB) AuthMasyarakatRepository { + return &authMasyarakatRepository{db} +} + +func (r *authMasyarakatRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authMasyarakatRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authMasyarakatRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/auth/auth_pengelola_repo.go b/internal/repositories/auth/auth_pengelola_repo.go new file mode 100644 index 0000000..f7b561a --- /dev/null +++ b/internal/repositories/auth/auth_pengelola_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthPengelolaRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authPengelolaRepository struct { + db *gorm.DB +} + +func NewAuthPengelolaRepositories(db *gorm.DB) AuthPengelolaRepository { + return &authPengelolaRepository{db} +} + +func (r *authPengelolaRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authPengelolaRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authPengelolaRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/auth/auth_pengepul_repo.go b/internal/repositories/auth/auth_pengepul_repo.go new file mode 100644 index 0000000..5253ee1 --- /dev/null +++ b/internal/repositories/auth/auth_pengepul_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "rijig/model" + + "gorm.io/gorm" +) + +type AuthPengepulRepository interface { + CreateUser(user *model.User) (*model.User, error) + GetUserByPhone(phone string) (*model.User, error) + GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) +} + +type authPengepulRepository struct { + db *gorm.DB +} + +func NewAuthPengepulRepositories(db *gorm.DB) AuthPengepulRepository { + return &authPengepulRepository{db} +} + +func (r *authPengepulRepository) CreateUser(user *model.User) (*model.User, error) { + if err := r.db.Create(user).Error; err != nil { + return nil, err + } + return user, nil +} + +func (r *authPengepulRepository) GetUserByPhone(phone string) (*model.User, error) { + var user model.User + if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authPengepulRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { + var user model.User + err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &user, nil +} diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go index df4299b..27698f3 100644 --- a/internal/repositories/role_repo.go +++ b/internal/repositories/role_repo.go @@ -8,6 +8,7 @@ import ( type RoleRepository interface { FindByID(id string) (*model.Role, error) + FindRoleByName(roleName string) (*model.Role, error) FindAll() ([]model.Role, error) } @@ -36,3 +37,12 @@ func (r *roleRepository) FindAll() ([]model.Role, error) { } return roles, nil } + +func (r *roleRepository) FindRoleByName(roleName string) (*model.Role, error) { + var role model.Role + err := r.DB.Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} diff --git a/internal/services/auth/auth_admin_service.go b/internal/services/auth/auth_admin_service.go new file mode 100644 index 0000000..89ac8aa --- /dev/null +++ b/internal/services/auth/auth_admin_service.go @@ -0,0 +1,191 @@ +package service + +import ( + "errors" + "fmt" + "log" + "rijig/config" + dto "rijig/dto/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +const ( + ErrEmailTaken = "email is already used" + ErrPhoneTaken = "phone number is already used" + ErrInvalidPassword = "password does not match" + ErrRoleNotFound = "role not found" + ErrFailedToGenerateToken = "failed to generate token" + ErrFailedToHashPassword = "failed to hash password" + ErrFailedToCreateUser = "failed to create user" + ErrIncorrectPassword = "incorrect password" + ErrAccountNotFound = "account not found" +) + +type AuthAdminService interface { + RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) + + LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) + LogoutAdmin(userID, deviceID string) error +} + +type authAdminService struct { + UserRepo repository.AuthAdminRepository + RoleRepo repositories.RoleRepository + SecretKey string +} + +func NewAuthAdminService(userRepo repository.AuthAdminRepository, roleRepo repositories.RoleRepository, secretKey string) AuthAdminService { + return &authAdminService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey} +} + +func (s *authAdminService) RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) { + + if existingUser, _ := s.UserRepo.FindByEmail(request.Email); existingUser != nil { + return nil, errors.New(ErrEmailTaken) + } + + if existingUser, _ := s.UserRepo.FindByPhone(request.Phone); existingUser != nil { + return nil, errors.New(ErrPhoneTaken) + } + + role, err := s.UserRepo.FindRoleByName("administrator") + if err != nil { + return nil, errors.New(ErrRoleNotFound) + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) + if err != nil { + log.Println("Error hashing password:", err) + return nil, errors.New(ErrFailedToHashPassword) + } + + user := &model.User{ + Name: request.Name, + Email: request.Email, + Phone: request.Phone, + Password: string(hashedPassword), + RoleID: role.ID, + Role: role, + Dateofbirth: request.Dateofbirth, + Placeofbirth: request.Placeofbirth, + Gender: request.Gender, + RegistrationStatus: "completed", + } + + createdUser, err := s.UserRepo.CreateUser(user) + if err != nil { + log.Println("Error creating user:", err) + return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) + } + + return createdUser, nil +} + +func (s *authAdminService) LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) { + + user, err := s.UserRepo.FindByEmail(req.Email) + if err != nil { + log.Println("User not found:", err) + return nil, errors.New(ErrAccountNotFound) + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { + log.Println("Incorrect password:", err) + return nil, errors.New(ErrIncorrectPassword) + } + + existingUser, err := s.UserRepo.FindAdminByEmailandRoleid(req.Email, "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var adminUser *model.User + if existingUser != nil { + adminUser = existingUser + } else { + + adminUser = &model.User{ + Email: req.Email, + RoleID: "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7", + } + createdUser, err := s.UserRepo.CreateUser(adminUser) + if err != nil { + return nil, err + } + adminUser = createdUser + } + + token, err := s.generateJWTToken(adminUser.ID, req.Deviceid) + if err != nil { + return nil, err + } + + role, err := s.RoleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.Deviceid + if err := s.saveSessionAdminData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.LoginResponse{ + UserID: user.ID, + Role: user.Role.RoleName, + Token: token, + }, nil +} + +func (s *authAdminService) saveSessionAdminData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authAdminService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authAdminService) LogoutAdmin(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_masyarakat_service.go b/internal/services/auth/auth_masyarakat_service.go new file mode 100644 index 0000000..407ba9b --- /dev/null +++ b/internal/services/auth/auth_masyarakat_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthMasyarakatService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authMasyarakatService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthMasyarakatService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthMasyarakatService { + return &authMasyarakatService{userRepo, roleRepo} +} + +func (s *authMasyarakatService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authMasyarakatService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authMasyarakatService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authMasyarakatService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authMasyarakatService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0e5684e4-b214-4bd0-972f-3be80c4649a0") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "0e5684e4-b214-4bd0-972f-3be80c4649a0", + PhoneVerified: true, + RegistrationStatus: "completed", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authMasyarakatService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authMasyarakatService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_pengelola_service.go b/internal/services/auth/auth_pengelola_service.go new file mode 100644 index 0000000..840f22c --- /dev/null +++ b/internal/services/auth/auth_pengelola_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthPengelolaService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authPengelolaService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthPengelolaService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengelolaService { + return &authPengelolaService{userRepo, roleRepo} +} + +func (s *authPengelolaService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authPengelolaService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authPengelolaService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authPengelolaService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authPengelolaService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0bf86966-7042-410a-a88c-d01f70832348") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "0bf86966-7042-410a-a88c-d01f70832348", + PhoneVerified: true, + RegistrationStatus: "uncompleted", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authPengelolaService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authPengelolaService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go new file mode 100644 index 0000000..0fb0f3c --- /dev/null +++ b/internal/services/auth/auth_pengepul_service.go @@ -0,0 +1,170 @@ +package service + +import ( + "errors" + "fmt" + "rijig/config" + "rijig/dto" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + "rijig/model" + "rijig/utils" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type AuthPengepulService interface { + RegisterOrLogin(req *dto.RegisterRequest) error + VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) + Logout(userID, deviceID string) error +} + +type authPengepulService struct { + userRepo repository.AuthPengelolaRepository + roleRepo repositories.RoleRepository +} + +func NewAuthPengepulService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengepulService { + return &authPengepulService{userRepo, roleRepo} +} + +func (s *authPengepulService) generateJWTToken(userID string, deviceID string) (string, error) { + + expirationTime := time.Now().Add(24 * time.Hour) + + claims := jwt.MapClaims{ + "sub": userID, + "exp": expirationTime.Unix(), + "device_id": deviceID, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + secretKey := config.GetSecretKey() + + return token.SignedString([]byte(secretKey)) +} + +func (s *authPengepulService) RegisterOrLogin(req *dto.RegisterRequest) error { + if err := s.checkOTPRequestCooldown(req.Phone); err != nil { + return err + } + return s.sendOTP(req.Phone) +} + +func (s *authPengepulService) checkOTPRequestCooldown(phone string) error { + otpSentTime, err := utils.GetStringData("otp_sent:" + phone) + if err != nil || otpSentTime == "" { + return nil + } + lastSent, _ := time.Parse(time.RFC3339, otpSentTime) + if time.Since(lastSent) < otpCooldown { + return errors.New("please wait before requesting a new OTP") + } + return nil +} + +func (s *authPengepulService) sendOTP(phone string) error { + otp := generateOTP() + if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + return err + } + + if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { + return err + } + return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +} + +func (s *authPengepulService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { + + storedOTP, err := utils.GetStringData("otp:" + req.Phone) + if err != nil || storedOTP == "" { + return nil, errors.New("OTP expired or not found") + } + + if storedOTP != req.OTP { + return nil, errors.New("invalid OTP") + } + + if err := utils.DeleteData("otp:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) + } + if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { + return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) + } + + existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "d7245535-0e9e-4d35-ab39-baece5c10b3c") + if err != nil { + return nil, fmt.Errorf("failed to check existing user: %w", err) + } + + var user *model.User + if existingUser != nil { + user = existingUser + } else { + + user = &model.User{ + Phone: req.Phone, + RoleID: "d7245535-0e9e-4d35-ab39-baece5c10b3c", + PhoneVerified: true, + RegistrationStatus: "uncompleted", + } + createdUser, err := s.userRepo.CreateUser(user) + if err != nil { + return nil, err + } + user = createdUser + } + + token, err := s.generateJWTToken(user.ID, req.DeviceID) + if err != nil { + return nil, err + } + + role, err := s.roleRepo.FindByID(user.RoleID) + if err != nil { + return nil, fmt.Errorf("failed to get role: %w", err) + } + + deviceID := req.DeviceID + if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { + return nil, err + } + + return &dto.UserDataResponse{ + UserID: user.ID, + UserRole: role.RoleName, + Token: token, + }, nil +} + +func (s *authPengepulService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) + sessionData := map[string]interface{}{ + "userID": userID, + "roleID": roleID, + "roleName": roleName, + } + + if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session data: %w", err) + } + + if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { + return fmt.Errorf("failed to set session token: %w", err) + } + + return nil +} + +func (s *authPengepulService) Logout(userID, deviceID string) error { + + err := utils.DeleteSessionData(userID, deviceID) + if err != nil { + return fmt.Errorf("failed to delete session from Redis: %w", err) + } + + return nil +} diff --git a/internal/services/auth/otp.go b/internal/services/auth/otp.go new file mode 100644 index 0000000..d96c534 --- /dev/null +++ b/internal/services/auth/otp.go @@ -0,0 +1,14 @@ +package service + +import ( + "fmt" + "math/rand" + "time" +) + +func generateOTP() string { + randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) + return fmt.Sprintf("%04d", randGenerator.Intn(10000)) +} + +const otpCooldown = 50 * time.Second diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go index 92da706..47a733d 100644 --- a/internal/services/auth_service.go +++ b/internal/services/auth_service.go @@ -1,213 +1,213 @@ package services -import ( - "errors" - "fmt" - "math/rand" - "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - "time" +// import ( +// "errors" +// "fmt" +// "math/rand" +// "rijig/config" +// "rijig/dto" +// "rijig/internal/repositories" +// "rijig/model" +// "rijig/utils" +// "time" - "github.com/golang-jwt/jwt/v5" -) +// "github.com/golang-jwt/jwt/v5" +// ) -const otpCooldown = 30 * time.Second +// const otpCooldown = 30 * time.Second -type AuthService interface { - RegisterOrLogin(req *dto.RegisterRequest) error - VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) - Logout(userID, phone string) error -} +// type AuthService interface { +// RegisterOrLogin(req *dto.RegisterRequest) error +// VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) +// Logout(userID, phone string) error +// } -type authService struct { - userRepo repositories.UserRepository - roleRepo repositories.RoleRepository -} +// type authService struct { +// userRepo repositories.UserRepository +// roleRepo repositories.RoleRepository +// } -func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { - return &authService{userRepo, roleRepo} -} +// func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { +// return &authService{userRepo, roleRepo} +// } -func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { +// func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { - if err := s.checkOTPRequestCooldown(req.Phone); err != nil { - return err - } +// if err := s.checkOTPRequestCooldown(req.Phone); err != nil { +// return err +// } - user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) - if err != nil { - return fmt.Errorf("failed to check existing user: %w", err) - } +// user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) +// if err != nil { +// return fmt.Errorf("failed to check existing user: %w", err) +// } - if user != nil { - return s.sendOTP(req.Phone) - } +// if user != nil { +// return s.sendOTP(req.Phone) +// } - user = &model.User{ - Phone: req.Phone, - RoleID: req.RoleID, - } +// user = &model.User{ +// Phone: req.Phone, +// RoleID: req.RoleID, +// } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return fmt.Errorf("failed to create new user: %w", err) - } +// createdUser, err := s.userRepo.CreateUser(user) +// if err != nil { +// return fmt.Errorf("failed to create new user: %w", err) +// } - if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { - return err - } +// if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { +// return err +// } - return s.sendOTP(req.Phone) -} +// return s.sendOTP(req.Phone) +// } -func (s *authService) checkOTPRequestCooldown(phone string) error { - otpSentTime, err := utils.GetStringData("otp_sent:" + phone) - if err != nil || otpSentTime == "" { - return nil - } - lastSent, _ := time.Parse(time.RFC3339, otpSentTime) - if time.Since(lastSent) < otpCooldown { - return errors.New("please wait before requesting a new OTP") - } - return nil -} +// func (s *authService) checkOTPRequestCooldown(phone string) error { +// otpSentTime, err := utils.GetStringData("otp_sent:" + phone) +// if err != nil || otpSentTime == "" { +// return nil +// } +// lastSent, _ := time.Parse(time.RFC3339, otpSentTime) +// if time.Since(lastSent) < otpCooldown { +// return errors.New("please wait before requesting a new OTP") +// } +// return nil +// } -func (s *authService) sendOTP(phone string) error { - otp := generateOTP() - if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - return err - } +// func (s *authService) sendOTP(phone string) error { +// otp := generateOTP() +// if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { +// return err +// } - if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { - return err - } - return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -} +// if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { +// return err +// } +// return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) +// } -func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { +// func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil || storedOTP == "" { - return nil, errors.New("OTP expired or not found") - } +// storedOTP, err := utils.GetStringData("otp:" + req.Phone) +// if err != nil || storedOTP == "" { +// return nil, errors.New("OTP expired or not found") +// } - if storedOTP != req.OTP { - return nil, errors.New("invalid OTP") - } +// if storedOTP != req.OTP { +// return nil, errors.New("invalid OTP") +// } - if err := utils.DeleteData("otp:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) - } +// if err := utils.DeleteData("otp:" + req.Phone); err != nil { +// return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) +// } - existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } +// existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) +// if err != nil { +// return nil, fmt.Errorf("failed to check existing user: %w", err) +// } - var user *model.User - if existingUser != nil { - user = existingUser - } else { +// var user *model.User +// if existingUser != nil { +// user = existingUser +// } else { - user = &model.User{ - Phone: req.Phone, - RoleID: req.RoleID, - } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return nil, err - } - user = createdUser - } +// user = &model.User{ +// Phone: req.Phone, +// RoleID: req.RoleID, +// } +// createdUser, err := s.userRepo.CreateUser(user) +// if err != nil { +// return nil, err +// } +// user = createdUser +// } - token, err := s.generateJWTToken(user.ID) - if err != nil { - return nil, err - } +// token, err := s.generateJWTToken(user.ID) +// if err != nil { +// return nil, err +// } - role, err := s.roleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } +// role, err := s.roleRepo.FindByID(user.RoleID) +// if err != nil { +// return nil, fmt.Errorf("failed to get role: %w", err) +// } - if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } +// if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { +// return nil, err +// } - return &dto.UserDataResponse{ - UserID: user.ID, - UserRole: role.RoleName, - Token: token, - }, nil -} +// return &dto.UserDataResponse{ +// UserID: user.ID, +// UserRole: role.RoleName, +// Token: token, +// }, nil +// } -func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { - if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { - return fmt.Errorf("failed to store user data in Redis: %w", err) - } +// func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { +// if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { +// return fmt.Errorf("failed to store user data in Redis: %w", err) +// } - if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { - return fmt.Errorf("failed to store user phone in Redis: %w", err) - } +// if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { +// return fmt.Errorf("failed to store user phone in Redis: %w", err) +// } - return nil -} +// return nil +// } -func (s *authService) generateJWTToken(userID string) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) - claims := &jwt.RegisteredClaims{ - Subject: userID, - ExpiresAt: jwt.NewNumericDate(expirationTime), - } +// func (s *authService) generateJWTToken(userID string) (string, error) { +// expirationTime := time.Now().Add(24 * time.Hour) +// claims := &jwt.RegisteredClaims{ +// Subject: userID, +// ExpiresAt: jwt.NewNumericDate(expirationTime), +// } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - secretKey := config.GetSecretKey() +// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) +// secretKey := config.GetSecretKey() - return token.SignedString([]byte(secretKey)) -} +// return token.SignedString([]byte(secretKey)) +// } -func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s", userID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } +// func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { +// sessionKey := fmt.Sprintf("session:%s", userID) +// sessionData := map[string]interface{}{ +// "userID": userID, +// "roleID": roleID, +// "roleName": roleName, +// } - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } +// if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { +// return fmt.Errorf("failed to set session data: %w", err) +// } - if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } +// if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { +// return fmt.Errorf("failed to set session token: %w", err) +// } - return nil -} +// return nil +// } -func (s *authService) Logout(userID, phone string) error { - keys := []string{ - "session:" + userID, - "session_token:" + userID, - "user_logged_in:" + userID, - "user:" + userID, - "user_phone:" + userID, - "otp_sent:" + phone, - } +// func (s *authService) Logout(userID, phone string) error { +// keys := []string{ +// "session:" + userID, +// "session_token:" + userID, +// "user_logged_in:" + userID, +// "user:" + userID, +// "user_phone:" + userID, +// "otp_sent:" + phone, +// } - for _, key := range keys { - if err := utils.DeleteData(key); err != nil { - return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) - } - } +// for _, key := range keys { +// if err := utils.DeleteData(key); err != nil { +// return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) +// } +// } - return nil -} +// return nil +// } -func generateOTP() string { - randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) - return fmt.Sprintf("%04d", randGenerator.Intn(10000)) -} +// func generateOTP() string { +// randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) +// return fmt.Sprintf("%04d", randGenerator.Intn(10000)) +// } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index f1ab25a..72a5994 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -45,12 +45,12 @@ func (s *userProfileService) prepareUserResponse(user *model.User) *dto.UserResp return &dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, Email: user.Email, - EmailVerified: user.EmailVerified, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: createdAt, UpdatedAt: updatedAt, @@ -100,12 +100,12 @@ func (s *userProfileService) GetAllUsers() ([]dto.UserResponseDTO, error) { for _, user := range users { response = append(response, dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, + // Email: user.Email, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: user.CreatedAt.Format(time.RFC3339), UpdatedAt: user.UpdatedAt.Format(time.RFC3339), @@ -125,12 +125,12 @@ func (s *userProfileService) GetUsersByRoleID(roleID string) ([]dto.UserResponse for _, user := range users { response = append(response, dto.UserResponseDTO{ ID: user.ID, - Username: user.Username, + // Username: user.Username, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, - Email: user.Email, - EmailVerified: user.EmailVerified, + // Email: user.Email, + // EmailVerified: user.EmailVerified, RoleName: user.Role.RoleName, CreatedAt: user.CreatedAt.Format(time.RFC3339), UpdatedAt: user.UpdatedAt.Format(time.RFC3339), diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index c1b868d..400798b 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -1,8 +1,9 @@ package middleware import ( + "fmt" + "log" "os" - "rijig/utils" "github.com/gofiber/fiber/v2" @@ -22,35 +23,39 @@ func AuthMiddleware(c *fiber.Ctx) error { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte(os.Getenv("SECRET_KEY")), nil }) + if err != nil || !token.Valid { + log.Printf("Error parsing token: %v", err) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token") } claims, ok := token.Claims.(jwt.MapClaims) - if !ok || claims["sub"] == nil { + if !ok || claims["sub"] == nil || claims["device_id"] == nil { + log.Println("Invalid token claims") return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } userID := claims["sub"].(string) - if userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid user session") - } + deviceID := claims["device_id"].(string) - sessionKey := "session:" + userID + sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) sessionData, err := utils.GetJSONData(sessionKey) if err != nil || sessionData == nil { + log.Printf("Session expired or invalid for userID: %s, deviceID: %s", userID, deviceID) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Session expired or invalid") } roleID, roleOK := sessionData["roleID"].(string) roleName, roleNameOK := sessionData["roleName"].(string) if !roleOK || !roleNameOK { + log.Println("Invalid session data for userID:", userID) return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid session data") } c.Locals("userID", userID) c.Locals("roleID", roleID) c.Locals("roleName", roleName) + c.Locals("device_id", deviceID) return c.Next() } diff --git a/model/company_profile_model.go b/model/company_profile_model.go new file mode 100644 index 0000000..521185a --- /dev/null +++ b/model/company_profile_model.go @@ -0,0 +1,24 @@ +package model + +import ( + "time" +) + +type CompanyProfile struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + CompanyName string `gorm:"not null" json:"company_name"` + CompanyAddress string `gorm:"not null" json:"company_address"` + CompanyPhone string `gorm:"not null" json:"company_phone"` + CompanyEmail string `gorm:"not null" json:"company_email"` + CompanyLogo string `gorm:"not null" json:"company_logo"` + CompanyWebsite string `json:"company_website"` + TaxID string `json:"tax_id"` + FoundedDate time.Time `json:"founded_date"` + CompanyType string `gorm:"not null" json:"company_type"` + CompanyDescription string `gorm:"type:text" json:"company_description"` + CompanyStatus string `gorm:"not null" json:"company_status"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` +} diff --git a/model/identitycard_model.go b/model/identitycard_model.go new file mode 100644 index 0000000..d7f6f31 --- /dev/null +++ b/model/identitycard_model.go @@ -0,0 +1,25 @@ +package model + +import "time" + +type IdentityCard struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + Identificationumber string `gorm:"not null" json:"identificationumber"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Gender string `gorm:"not null" json:"gender"` + BloodType string `gorm:"not null" json:"bloodtype"` + District string `gorm:"not null" json:"district"` + Village string `gorm:"not null" json:"village"` + Neighbourhood string `gorm:"not null" json:"neighbourhood"` + Religion string `gorm:"not null" json:"religion"` + Maritalstatus string `gorm:"not null" json:"maritalstatus"` + Job string `gorm:"not null" json:"job"` + Citizenship string `gorm:"not null" json:"citizenship"` + Validuntil string `gorm:"not null" json:"validuntil"` + Cardphoto string `gorm:"not null" json:"cardphoto"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/model/role_model.go b/model/role_model.go index 5f595e0..72c910e 100644 --- a/model/role_model.go +++ b/model/role_model.go @@ -5,7 +5,7 @@ import "time" type Role struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` RoleName string `gorm:"unique;not null" json:"roleName"` - Users []User `gorm:"foreignKey:RoleID" json:"users"` + // Users []User `gorm:"foreignKey:RoleID" json:"users"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/user_model.go b/model/user_model.go index 8f26826..c6fecf7 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,16 +3,19 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Username string `gorm:"not null" json:"username"` - Name string `gorm:"not null" json:"name"` - Phone string `gorm:"not null" json:"phone"` - Email string `gorm:"not null" json:"email"` - EmailVerified bool `gorm:"default:false" json:"emailVerified"` - Password string `gorm:"not null" json:"password"` - RoleID string `gorm:"not null" json:"roleId"` - Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` - 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" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + Gender string `gorm:"not null" json:"gender"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Phone string `gorm:"not null" json:"phone"` + Email string `json:"email,omitempty"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + Password string `json:"password,omitempty"` + RoleID string `gorm:"not null" json:"roleId"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/presentation/auth/auth_admin_route.go b/presentation/auth/auth_admin_route.go new file mode 100644 index 0000000..07f3324 --- /dev/null +++ b/presentation/auth/auth_admin_route.go @@ -0,0 +1,35 @@ +package presentation + +import ( + "log" + "os" + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AdminAuthRouter(api fiber.Router) { + secretKey := os.Getenv("SECRET_KEY") + if secretKey == "" { + log.Fatal("SECRET_KEY is not set in the environment variables") + os.Exit(1) + } + + adminAuthRepo := repository.NewAuthAdminRepository(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + + adminAuthService := services.NewAuthAdminService(adminAuthRepo, roleRepo, secretKey) + + adminAuthHandler := handler.NewAuthAdminHandler(adminAuthService) + + adminAuthAPI := api.Group("/admin-auth") + + adminAuthAPI.Post("/register", adminAuthHandler.RegisterAdmin) + adminAuthAPI.Post("/login", adminAuthHandler.LoginAdmin) + adminAuthAPI.Post("/logout", middleware.AuthMiddleware, adminAuthHandler.LogoutAdmin) +} diff --git a/presentation/auth/auth_masyarakat_route.go b/presentation/auth/auth_masyarakat_route.go new file mode 100644 index 0000000..dbf4e7a --- /dev/null +++ b/presentation/auth/auth_masyarakat_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthMasyarakatRouter(api fiber.Router) { + authMasyarakatRepo := repository.NewAuthMasyarakatRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authMasyarakatService := services.NewAuthMasyarakatService(authMasyarakatRepo, roleRepo) + + authHandler := handler.NewAuthMasyarakatHandler(authMasyarakatService) + + authMasyarakat := api.Group("/authmasyarakat") + + authMasyarakat.Post("/auth", authHandler.RegisterOrLoginHandler) + authMasyarakat.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authMasyarakat.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth/auth_pengelola_route.go b/presentation/auth/auth_pengelola_route.go new file mode 100644 index 0000000..358b244 --- /dev/null +++ b/presentation/auth/auth_pengelola_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthPengelolaRouter(api fiber.Router) { + authPengelolaRepo := repository.NewAuthPengelolaRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authPengelolaService := services.NewAuthPengelolaService(authPengelolaRepo, roleRepo) + + authHandler := handler.NewAuthPengelolaHandler(authPengelolaService) + + authPengelola := api.Group("/authpengelola") + + authPengelola.Post("/auth", authHandler.RegisterOrLoginHandler) + authPengelola.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authPengelola.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth/auth_pengepul_route.go b/presentation/auth/auth_pengepul_route.go new file mode 100644 index 0000000..1f60f2d --- /dev/null +++ b/presentation/auth/auth_pengepul_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + handler "rijig/internal/handler/auth" + "rijig/internal/repositories" + repository "rijig/internal/repositories/auth" + services "rijig/internal/services/auth" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthPengepulRouter(api fiber.Router) { + authPengepulRepo := repository.NewAuthPengepulRepositories(config.DB) + roleRepo := repositories.NewRoleRepository(config.DB) + authPengepulService := services.NewAuthPengepulService(authPengepulRepo, roleRepo) + + authHandler := handler.NewAuthPengepulHandler(authPengepulService) + + authPengepul := api.Group("/authpengepul") + + authPengepul.Post("/auth", authHandler.RegisterOrLoginHandler) + authPengepul.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) + authPengepul.Post("/verify-otp", authHandler.VerifyOTPHandler) +} diff --git a/presentation/auth_route.go b/presentation/auth_route.go index 1ae2d9a..ee733c1 100644 --- a/presentation/auth_route.go +++ b/presentation/auth_route.go @@ -1,23 +1,23 @@ package presentation -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" +// import ( +// "rijig/config" +// "rijig/internal/handler" +// "rijig/internal/repositories" +// "rijig/internal/services" +// "rijig/middleware" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -func AuthRouter(api fiber.Router) { - userRepo := repositories.NewUserRepository(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - authService := services.NewAuthService(userRepo, roleRepo) +// func AuthRouter(api fiber.Router) { +// userRepo := repositories.NewUserRepository(config.DB) +// roleRepo := repositories.NewRoleRepository(config.DB) +// authService := services.NewAuthService(userRepo, roleRepo) - authHandler := handler.NewAuthHandler(authService) +// authHandler := handler.NewAuthHandler(authService) - api.Post("/auth", authHandler.RegisterOrLoginHandler) - api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) - api.Post("/verify-otp", authHandler.VerifyOTPHandler) -} +// api.Post("/auth", authHandler.RegisterOrLoginHandler) +// api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) +// api.Post("/verify-otp", authHandler.VerifyOTPHandler) +// } diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go index 15dea4d..c145907 100644 --- a/presentation/userpin_route.go +++ b/presentation/userpin_route.go @@ -11,15 +11,14 @@ import ( ) func UserPinRouter(api fiber.Router) { - userPinRepo := repositories.NewUserPinRepository(config.DB) userPinService := services.NewUserPinService(userPinRepo) userPinHandler := handler.NewUserPinHandler(userPinService) - api.Post("/user/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) - api.Post("/user/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) - api.Get("/user/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) - api.Patch("/user/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) + api.Post("/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) + api.Post("/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) + api.Get("/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) + api.Patch("/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index ef85491..feae5f6 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -5,6 +5,7 @@ import ( "rijig/middleware" "rijig/presentation" + presentationn "rijig/presentation/auth" "github.com/gofiber/fiber/v2" ) @@ -15,7 +16,13 @@ func SetupRoutes(app *fiber.App) { api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) - presentation.AuthRouter(api) + // || auth router || // + // presentation.AuthRouter(api) + presentationn.AdminAuthRouter(api) + presentationn.AuthPengelolaRouter(api) + presentationn.AuthPengepulRouter(api) + presentationn.AuthMasyarakatRouter(api) + // || auth router || // presentation.UserProfileRouter(api) presentation.UserPinRouter(api) presentation.RoleRouter(api) diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 7b7bc18..99083c7 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -17,7 +17,6 @@ var ctx = context.Background() const defaultExpiration = 1 * time.Hour func SetData[T any](key string, value T, expiration time.Duration) error { - if expiration == 0 { expiration = defaultExpiration } @@ -36,6 +35,27 @@ func SetData[T any](key string, value T, expiration time.Duration) error { return nil } +func SaveSessionTokenToRedis(userID string, deviceID string, token string) error { + + sessionKey := "session:" + userID + ":" + deviceID + + err := config.RedisClient.Set(ctx, sessionKey, token, 24*time.Hour).Err() + if err != nil { + return err + } + log.Printf("Session token saved to Redis with key: %s", sessionKey) + return nil +} + +func GetSessionTokenFromRedis(userID string, deviceID string) (string, error) { + sessionKey := "session:" + userID + ":" + deviceID + token, err := config.RedisClient.Get(ctx, sessionKey).Result() + if err != nil { + return "", err + } + return token, nil +} + func GetData(key string) (string, error) { val, err := config.RedisClient.Get(ctx, key).Result() if err == redis.Nil { @@ -89,9 +109,23 @@ func GetJSONData(key string) (map[string]interface{}, error) { return data, nil } -func DeleteSessionData(userID string) error { - sessionKey := "session:" + userID - return DeleteData(sessionKey) +func DeleteSessionData(userID string, deviceID string) error { + sessionKey := "session:" + userID + ":" + deviceID + sessionTokenKey := "session_token:" + userID + ":" + deviceID + + log.Printf("Attempting to delete session data with keys: %s, %s", sessionKey, sessionTokenKey) + + err := DeleteData(sessionKey) + if err != nil { + return fmt.Errorf("failed to delete session data: %w", err) + } + err = DeleteData(sessionTokenKey) + if err != nil { + return fmt.Errorf("failed to delete session token: %w", err) + } + + log.Printf("Successfully deleted session data for userID: %s, deviceID: %s", userID, deviceID) + return nil } func logAndReturnError(message string, err error) error { @@ -124,43 +158,11 @@ func GetStringData(key string) (string, error) { return val, nil } -// func SetStringData(key, value string, expiration time.Duration) error { -// if expiration == 0 { -// expiration = defaultExpiration -// } - -// err := config.RedisClient.Set(ctx, key, value, expiration).Err() -// if err != nil { -// return fmt.Errorf("Error setting string data in Redis with key: %s: %v", key, err) -// } - -// log.Printf("String data stored in Redis with key: %s", key) -// return nil -// } - -// GetStringData retrieves a string value from Redis by key -// func GetStringData(key string) (string, error) { -// val, err := config.RedisClient.Get(ctx, key).Result() -// if err == redis.Nil { -// return "", nil -// } else if err != nil { -// return "", fmt.Errorf("Error retrieving string data from Redis with key: %s: %v", key, err) -// } - -// return val, nil -// } -// func StoreOTPInRedis(phone, otpCode string, expirationTime time.Duration) error { -// err := config.RedisClient.Set(config.Ctx, phone, otpCode, expirationTime).Err() -// if err != nil { -// return fmt.Errorf("failed to store OTP in Redis: %v", err) -// } -// return nil -// } - -// func GetOTPFromRedis(phone string) (string, error) { -// otpCode, err := config.RedisClient.Get(config.Ctx, phone).Result() -// if err != nil { -// return "", fmt.Errorf("failed to get OTP from Redis: %v", err) -// } -// return otpCode, nil -// } +func CheckSessionExists(userID string, deviceID string) (bool, error) { + sessionKey := "session:" + userID + ":" + deviceID + val, err := config.RedisClient.Exists(ctx, sessionKey).Result() + if err != nil { + return false, err + } + return val > 0, nil +} diff --git a/utils/regexp_formatter.go b/utils/regexp_formatter.go new file mode 100644 index 0000000..616675f --- /dev/null +++ b/utils/regexp_formatter.go @@ -0,0 +1,44 @@ +package utils + +import ( + "regexp" + "strings" +) + +func IsValidPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^62\d{9,13}$`) + return re.MatchString(phone) +} + +func IsValidEmail(email string) bool { + re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + + if len(password) < 6 { + return false + } + + hasUpper := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + if char >= 'A' && char <= 'Z' { + hasUpper = true + } else if char >= '0' && char <= '9' { + hasDigit = true + } else if isSpecialCharacter(char) { + hasSpecial = true + } + } + + return hasUpper && hasDigit && hasSpecial +} + +func isSpecialCharacter(char rune) bool { + specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" + return strings.ContainsRune(specialChars, char) +} diff --git a/utils/role_permission.go b/utils/role_permission.go index e892bdd..6de0e72 100644 --- a/utils/role_permission.go +++ b/utils/role_permission.go @@ -1,8 +1,8 @@ package utils const ( - RoleAdministrator = "46f75bb9-7f64-44b7-b378-091a67b3e229" - RoleMasyarakat = "6cfa867b-536c-448d-ba11-fe060b5af971" - RolePengepul = "8171883c-ea9e-4d17-9f28-a7896d88380f" - RolePengelola = "84d72ddb-68a8-430c-9b79-5d71f90cb1be" + RoleAdministrator = "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7" + RolePengelola = "0bf86966-7042-410a-a88c-d01f70832348" + RolePengepul = "d7245535-0e9e-4d35-ab39-baece5c10b3c" + RoleMasyarakat = "60e5684e4-b214-4bd0-972f-3be80c4649a0" ) From b747fb06d02f8a0aeb3cbe1af63254293035ad0d Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 5 May 2025 15:24:21 +0700 Subject: [PATCH 17/48] feat: identity card feature and fix static image url --- dto/identitycard_dto.go | 126 ++++++++++ internal/handler/identitycard_handler.go | 134 ++++++++++ internal/repositories/identitycard_repo.go | 82 ++++++ internal/services/identitycard_service.go | 276 +++++++++++++++++++++ presentation/identitycard_route.go | 27 ++ router/setup_routes.go.go | 4 +- 6 files changed, 648 insertions(+), 1 deletion(-) create mode 100644 dto/identitycard_dto.go create mode 100644 internal/handler/identitycard_handler.go create mode 100644 internal/repositories/identitycard_repo.go create mode 100644 internal/services/identitycard_service.go create mode 100644 presentation/identitycard_route.go diff --git a/dto/identitycard_dto.go b/dto/identitycard_dto.go new file mode 100644 index 0000000..14967f6 --- /dev/null +++ b/dto/identitycard_dto.go @@ -0,0 +1,126 @@ +package dto + +import ( + "strings" +) + +type ResponseIdentityCardDTO struct { + ID string `json:"id"` + UserID string `json:"userId"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + District string `json:"district"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` + Cardphoto string `json:"cardphoto"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestIdentityCardDTO struct { + UserID string `json:"userId"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + District string `json:"district"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` + Cardphoto string `json:"cardphoto"` +} + +func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]string, bool) { + errors := make(map[string][]string) + isValid := true + + // if strings.TrimSpace(r.UserID) == "" { + // errors["userId"] = append(errors["userId"], "UserID harus diisi") + // isValid = false + // } + + if strings.TrimSpace(r.Identificationumber) == "" { + errors["identificationumber"] = append(errors["identificationumber"], "Nomor identifikasi harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Placeofbirth) == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Tempat lahir harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Dateofbirth) == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Tanggal lahir harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Gender) == "" { + errors["gender"] = append(errors["gender"], "Jenis kelamin harus diisi") + isValid = false + } + + if strings.TrimSpace(r.BloodType) == "" { + errors["bloodtype"] = append(errors["bloodtype"], "Golongan darah harus diisi") + isValid = false + } + + if strings.TrimSpace(r.District) == "" { + errors["district"] = append(errors["district"], "Kecamatan harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Village) == "" { + errors["village"] = append(errors["village"], "Desa harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Neighbourhood) == "" { + errors["neighbourhood"] = append(errors["neighbourhood"], "RT/RW harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Religion) == "" { + errors["religion"] = append(errors["religion"], "Agama harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Maritalstatus) == "" { + errors["maritalstatus"] = append(errors["maritalstatus"], "Status pernikahan harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Job) == "" { + errors["job"] = append(errors["job"], "Pekerjaan harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Citizenship) == "" { + errors["citizenship"] = append(errors["citizenship"], "Kewarganegaraan harus diisi") + isValid = false + } + + if strings.TrimSpace(r.Validuntil) == "" { + errors["validuntil"] = append(errors["validuntil"], "Masa berlaku harus diisi") + isValid = false + } + + // if strings.TrimSpace(r.Cardphoto) == "" { + // errors["cardphoto"] = append(errors["cardphoto"], "Foto KTP harus diisi") + // isValid = false + // } + + return errors, isValid +} diff --git a/internal/handler/identitycard_handler.go b/internal/handler/identitycard_handler.go new file mode 100644 index 0000000..b05ba95 --- /dev/null +++ b/internal/handler/identitycard_handler.go @@ -0,0 +1,134 @@ +package handler + +import ( + "log" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type IdentityCardHandler struct { + IdentityCardService services.IdentityCardService +} + +func NewIdentityCardHandler(identityCardService services.IdentityCardService) *IdentityCardHandler { + return &IdentityCardHandler{ + IdentityCardService: identityCardService, + } +} + +func (h *IdentityCardHandler) CreateIdentityCard(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") + } + + var request dto.RequestIdentityCardDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing body: %v", err) + return utils.ErrorResponse(c, "Invalid request data") + } + + cardPhoto, err := c.FormFile("cardphoto") + if err != nil { + log.Printf("Error retrieving card photo from request: %v", err) + return utils.ErrorResponse(c, "Card photo is required") + } + + identityCard, err := h.IdentityCardService.CreateIdentityCard(userID, &request, cardPhoto) + if err != nil { + log.Printf("Error creating identity card: %v", err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.CreateResponse(c, identityCard, "Identity card created successfully") +} + +func (h *IdentityCardHandler) UpdateIdentityCard(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") + } + + id := c.Params("identity_id") + if id == "" { + return utils.ErrorResponse(c, "Identity card ID is required") + } + + var request dto.RequestIdentityCardDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing body: %v", err) + return utils.ErrorResponse(c, "Invalid request data") + } + + cardPhoto, err := c.FormFile("cardphoto") + if err != nil && err.Error() != "File not found" { + log.Printf("Error retrieving card photo: %v", err) + return utils.ErrorResponse(c, "Card photo is required") + } + + updatedCard, err := h.IdentityCardService.UpdateIdentityCard(userID, id, &request, cardPhoto) + if err != nil { + log.Printf("Error updating identity card: %v", err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, updatedCard, "Identity card updated successfully") +} + +func (h *IdentityCardHandler) GetIdentityCardById(c *fiber.Ctx) error { + + id := c.Params("identity_id") + if id == "" { + return utils.ErrorResponse(c, "Identity card ID is required") + } + + identityCard, err := h.IdentityCardService.GetIdentityCardByID(id) + if err != nil { + log.Printf("Error retrieving identity card: %v", err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, identityCard, "Identity card retrieved successfully") +} + +func (h *IdentityCardHandler) GetIdentityCard(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + identityCard, err := h.IdentityCardService.GetIdentityCardsByUserID(userID) + if err != nil { + log.Printf("Error retrieving identity card: %v", err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, identityCard, "Identity card retrieved successfully") +} + +func (h *IdentityCardHandler) DeleteIdentityCard(c *fiber.Ctx) error { + + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") + } + + id := c.Params("identity_id") + if id == "" { + return utils.ErrorResponse(c, "Identity card ID is required") + } + + err := h.IdentityCardService.DeleteIdentityCard(id) + if err != nil { + log.Printf("Error deleting identity card: %v", err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Identity card deleted successfully") +} diff --git a/internal/repositories/identitycard_repo.go b/internal/repositories/identitycard_repo.go new file mode 100644 index 0000000..763791a --- /dev/null +++ b/internal/repositories/identitycard_repo.go @@ -0,0 +1,82 @@ +package repositories + +import ( + "errors" + "fmt" + "log" + "rijig/model" + + "gorm.io/gorm" +) + +type IdentityCardRepository interface { + CreateIdentityCard(identityCard *model.IdentityCard) (*model.IdentityCard, error) + GetIdentityCardByID(id string) (*model.IdentityCard, error) + GetIdentityCardsByUserID(userID string) ([]model.IdentityCard, error) + UpdateIdentityCard(id string, updatedCard *model.IdentityCard) (*model.IdentityCard, error) + DeleteIdentityCard(id string) error +} + +type identityCardRepository struct { + db *gorm.DB +} + +func NewIdentityCardRepository(db *gorm.DB) IdentityCardRepository { + return &identityCardRepository{ + db: db, + } +} + +func (r *identityCardRepository) CreateIdentityCard(identityCard *model.IdentityCard) (*model.IdentityCard, error) { + if err := r.db.Create(identityCard).Error; err != nil { + log.Printf("Error creating identity card: %v", err) + return nil, fmt.Errorf("failed to create identity card: %w", err) + } + return identityCard, nil +} + +func (r *identityCardRepository) GetIdentityCardByID(id string) (*model.IdentityCard, error) { + var identityCard model.IdentityCard + if err := r.db.Where("id = ?", id).First(&identityCard).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("identity card not found with id %s", id) + } + log.Printf("Error fetching identity card by ID: %v", err) + return nil, fmt.Errorf("error fetching identity card by ID: %w", err) + } + return &identityCard, nil +} + +func (r *identityCardRepository) GetIdentityCardsByUserID(userID string) ([]model.IdentityCard, error) { + var identityCards []model.IdentityCard + if err := r.db.Where("user_id = ?", userID).Find(&identityCards).Error; err != nil { + log.Printf("Error fetching identity cards by userID: %v", err) + return nil, fmt.Errorf("error fetching identity cards by userID: %w", err) + } + return identityCards, nil +} + +func (r *identityCardRepository) UpdateIdentityCard(id string, updatedCard *model.IdentityCard) (*model.IdentityCard, error) { + var existingCard model.IdentityCard + if err := r.db.Where("id = ?", id).First(&existingCard).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("identity card with ID %s not found", id) + } + log.Printf("Error fetching identity card for update: %v", err) + return nil, fmt.Errorf("error fetching identity card for update: %w", err) + } + + if err := r.db.Save(&existingCard).Error; err != nil { + log.Printf("Error updating identity card: %v", err) + return nil, fmt.Errorf("failed to update identity card: %w", err) + } + return &existingCard, nil +} + +func (r *identityCardRepository) DeleteIdentityCard(id string) error { + if err := r.db.Where("id = ?", id).Delete(&model.IdentityCard{}).Error; err != nil { + log.Printf("Error deleting identity card: %v", err) + return fmt.Errorf("failed to delete identity card: %w", err) + } + return nil +} diff --git a/internal/services/identitycard_service.go b/internal/services/identitycard_service.go new file mode 100644 index 0000000..b5e653b --- /dev/null +++ b/internal/services/identitycard_service.go @@ -0,0 +1,276 @@ +package services + +import ( + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" +) + +type IdentityCardService interface { + CreateIdentityCard(userID string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) + GetIdentityCardByID(id string) (*dto.ResponseIdentityCardDTO, error) + GetIdentityCardsByUserID(userID string) ([]dto.ResponseIdentityCardDTO, error) + UpdateIdentityCard(userID string, id string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) + DeleteIdentityCard(id string) error +} + +type identityCardService struct { + identityCardRepo repositories.IdentityCardRepository +} + +func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository) IdentityCardService { + return &identityCardService{ + identityCardRepo: identityCardRepo, + } +} + +func FormatResponseIdentityCars(identityCard *model.IdentityCard) (*dto.ResponseIdentityCardDTO, error) { + + createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt) + + idcardResponseDTO := &dto.ResponseIdentityCardDTO{ + ID: identityCard.ID, + UserID: identityCard.UserID, + Identificationumber: identityCard.Identificationumber, + Placeofbirth: identityCard.Placeofbirth, + Dateofbirth: identityCard.Dateofbirth, + Gender: identityCard.Gender, + BloodType: identityCard.BloodType, + District: identityCard.District, + Village: identityCard.Village, + Neighbourhood: identityCard.Neighbourhood, + Religion: identityCard.Religion, + Maritalstatus: identityCard.Maritalstatus, + Job: identityCard.Job, + Citizenship: identityCard.Citizenship, + Validuntil: identityCard.Validuntil, + Cardphoto: identityCard.Cardphoto, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return idcardResponseDTO, nil +} + +func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *multipart.FileHeader) (string, error) { + pathImage := "/uploads/identitycards/" + cardPhotoDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(cardPhotoDir); os.IsNotExist(err) { + + if err := os.MkdirAll(cardPhotoDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for identity card photo: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(cardPhoto.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + cardPhotoFileName := fmt.Sprintf("%s_cardphoto%s", userID, extension) + cardPhotoPath := filepath.Join(cardPhotoDir, cardPhotoFileName) + + src, err := cardPhoto.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(cardPhotoPath) + if err != nil { + return "", fmt.Errorf("failed to create card photo file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save card photo: %v", err) + } + + cardPhotoURL := fmt.Sprintf("%s%s", pathImage, cardPhotoFileName) + + return cardPhotoURL, nil +} + +func deleteIdentityCardImage(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *identityCardService) CreateIdentityCard(userID string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) { + + errors, valid := request.ValidateIdentityCardInput() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + + identityCard := &model.IdentityCard{ + UserID: userID, + Identificationumber: request.Identificationumber, + Placeofbirth: request.Placeofbirth, + Dateofbirth: request.Dateofbirth, + Gender: request.Gender, + BloodType: request.BloodType, + District: request.District, + Village: request.Village, + Neighbourhood: request.Neighbourhood, + Religion: request.Religion, + Maritalstatus: request.Maritalstatus, + Job: request.Job, + Citizenship: request.Citizenship, + Validuntil: request.Validuntil, + Cardphoto: cardPhotoPath, + } + + identityCard, err = s.identityCardRepo.CreateIdentityCard(identityCard) + if err != nil { + log.Printf("Error creating identity card: %v", err) + return nil, fmt.Errorf("failed to create identity card: %v", err) + } + + idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) + + return idcardResponseDTO, nil +} + +func (s *identityCardService) GetIdentityCardByID(id string) (*dto.ResponseIdentityCardDTO, error) { + + identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) + if err != nil { + log.Printf("Error fetching identity card: %v", err) + return nil, fmt.Errorf("failed to fetch identity card") + } + + idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) + + return idcardResponseDTO, nil + +} + +func (s *identityCardService) GetIdentityCardsByUserID(userID string) ([]dto.ResponseIdentityCardDTO, error) { + + identityCards, err := s.identityCardRepo.GetIdentityCardsByUserID(userID) + if err != nil { + log.Printf("Error fetching identity cards by userID: %v", err) + return nil, fmt.Errorf("failed to fetch identity cards by userID") + } + + var response []dto.ResponseIdentityCardDTO + for _, card := range identityCards { + + idcardResponseDTO, err := FormatResponseIdentityCars(&card) + if err != nil { + log.Printf("Error creating response DTO for identity card ID %v: %v", card.ID, err) + + continue + } + response = append(response, *idcardResponseDTO) + } + + return response, nil +} + +func (s *identityCardService) UpdateIdentityCard(userID string, id string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) { + + errors, valid := request.ValidateIdentityCardInput() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) + if err != nil { + return nil, fmt.Errorf("identity card not found: %v", err) + } + + if identityCard.Cardphoto != "" { + err := deleteIdentityCardImage(identityCard.Cardphoto) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + var cardPhotoPath string + if cardPhoto != nil { + cardPhotoPath, err = s.saveIdentityCardImage(userID, cardPhoto) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + } + + identityCard.Identificationumber = request.Identificationumber + identityCard.Placeofbirth = request.Placeofbirth + identityCard.Dateofbirth = request.Dateofbirth + identityCard.Gender = request.Gender + identityCard.BloodType = request.BloodType + identityCard.District = request.District + identityCard.Village = request.Village + identityCard.Neighbourhood = request.Neighbourhood + identityCard.Religion = request.Religion + identityCard.Maritalstatus = request.Maritalstatus + identityCard.Job = request.Job + identityCard.Citizenship = request.Citizenship + identityCard.Validuntil = request.Validuntil + if cardPhotoPath != "" { + identityCard.Cardphoto = cardPhotoPath + } + + identityCard, err = s.identityCardRepo.UpdateIdentityCard(id, identityCard) + if err != nil { + log.Printf("Error updating identity card: %v", err) + return nil, fmt.Errorf("failed to update identity card: %v", err) + } + + idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) + + return idcardResponseDTO, nil +} + +func (s *identityCardService) DeleteIdentityCard(id string) error { + + identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) + if err != nil { + return fmt.Errorf("identity card not found: %v", err) + } + + if identityCard.Cardphoto != "" { + err := deleteIdentityCardImage(identityCard.Cardphoto) + if err != nil { + return fmt.Errorf("failed to delete card photo: %v", err) + } + } + + err = s.identityCardRepo.DeleteIdentityCard(id) + if err != nil { + return fmt.Errorf("failed to delete identity card: %v", err) + } + + return nil +} diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go new file mode 100644 index 0000000..dfb187d --- /dev/null +++ b/presentation/identitycard_route.go @@ -0,0 +1,27 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func IdentityCardRouter(api fiber.Router) { + identityCardRepo := repositories.NewIdentityCardRepository(config.DB) + identityCardService := services.NewIdentityCardService(identityCardRepo) + identityCardHandler := handler.NewIdentityCardHandler(identityCardService) + + identityCardApi := api.Group("/identitycard") + identityCardApi.Use(middleware.AuthMiddleware) + + identityCardApi.Post("/create", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.CreateIdentityCard) + identityCardApi.Get("/get/:identity_id", identityCardHandler.GetIdentityCardById) + identityCardApi.Get("/get", identityCardHandler.GetIdentityCard) + identityCardApi.Put("/update/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.UpdateIdentityCard) + identityCardApi.Delete("/delete/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.DeleteIdentityCard) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index feae5f6..8846a88 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,10 +11,10 @@ import ( ) func SetupRoutes(app *fiber.App) { - app.Static(os.Getenv("BASE_URL")+"/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) + api.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") // || auth router || // // presentation.AuthRouter(api) @@ -23,6 +23,8 @@ func SetupRoutes(app *fiber.App) { presentationn.AuthPengepulRouter(api) presentationn.AuthMasyarakatRouter(api) // || auth router || // + presentation.IdentityCardRouter(api) + presentation.UserProfileRouter(api) presentation.UserPinRouter(api) presentation.RoleRouter(api) From 9a2481be092a1f93d58aaaa8d0bbbf5aae9c3922 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 5 May 2025 18:56:48 +0700 Subject: [PATCH 18/48] feat: company profile feature --- dto/company_profile_dto.go | 65 +++++++ internal/handler/company_profile_handler.go | 100 +++++++++++ internal/repositories/company_profile_repo.go | 78 +++++++++ internal/services/company_profile_service.go | 163 ++++++++++++++++++ internal/services/identitycard_service.go | 15 +- model/company_profile_model.go | 11 +- presentation/auth/auth_admin_route.go | 2 +- presentation/company_profile_route.go | 27 +++ presentation/identitycard_route.go | 3 +- router/setup_routes.go.go | 3 +- 10 files changed, 457 insertions(+), 10 deletions(-) create mode 100644 dto/company_profile_dto.go create mode 100644 internal/handler/company_profile_handler.go create mode 100644 internal/repositories/company_profile_repo.go create mode 100644 internal/services/company_profile_service.go create mode 100644 presentation/company_profile_route.go diff --git a/dto/company_profile_dto.go b/dto/company_profile_dto.go new file mode 100644 index 0000000..dacd91d --- /dev/null +++ b/dto/company_profile_dto.go @@ -0,0 +1,65 @@ +package dto + +import ( + "strings" +) + +type ResponseCompanyProfileDTO struct { + ID string `json:"id"` + UserID string `json:"userId"` + CompanyName string `json:"company_name"` + CompanyAddress string `json:"company_address"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` + CompanyLogo string `json:"company_logo,omitempty"` + CompanyWebsite string `json:"company_website,omitempty"` + TaxID string `json:"taxId,omitempty"` + FoundedDate string `json:"founded_date,omitempty"` + CompanyType string `json:"company_type,omitempty"` + CompanyDescription string `json:"company_description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestCompanyProfileDTO struct { + CompanyName string `json:"company_name"` + CompanyAddress string `json:"company_address"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` + CompanyLogo string `json:"company_logo,omitempty"` + CompanyWebsite string `json:"company_website,omitempty"` + TaxID string `json:"taxId,omitempty"` + FoundedDate string `json:"founded_date,omitempty"` + CompanyType string `json:"company_type,omitempty"` + CompanyDescription string `json:"company_description"` +} + +func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CompanyName) == "" { + errors["company_Name"] = append(errors["company_name"], "Company name is required") + } + + if strings.TrimSpace(r.CompanyAddress) == "" { + errors["company_Address"] = append(errors["company_address"], "Company address is required") + } + + if strings.TrimSpace(r.CompanyPhone) == "" { + errors["company_Phone"] = append(errors["company_phone"], "Company phone is required") + } + + if strings.TrimSpace(r.CompanyEmail) == "" { + errors["company_Email"] = append(errors["company_email"], "Company email is required") + } + + if strings.TrimSpace(r.CompanyDescription) == "" { + errors["company_Description"] = append(errors["company_description"], "Company description is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/company_profile_handler.go b/internal/handler/company_profile_handler.go new file mode 100644 index 0000000..2d8d7aa --- /dev/null +++ b/internal/handler/company_profile_handler.go @@ -0,0 +1,100 @@ +package handler + +import ( + "fmt" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CompanyProfileHandler struct { + companyProfileService services.CompanyProfileService +} + +func NewCompanyProfileHandler(service services.CompanyProfileService) *CompanyProfileHandler { + return &CompanyProfileHandler{ + companyProfileService: service, + } +} + +func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + var requestDTO dto.RequestCompanyProfileDTO + if err := c.BodyParser(&requestDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid input data"}}) + } + + companyProfileResponse, err := h.companyProfileService.CreateCompanyProfile(userID, &requestDTO) + if err != nil { + return utils.ErrorResponse(c, fmt.Sprintf("Failed to create company profile: %v", err)) + } + + return utils.SuccessResponse(c, companyProfileResponse, "Company profile created successfully") +} + +func (h *CompanyProfileHandler) GetCompanyProfileByID(c *fiber.Ctx) error { + id := c.Params("company_id") + + companyProfileResponse, err := h.companyProfileService.GetCompanyProfileByID(id) + if err != nil { + return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch company profile: %v", err)) + } + + return utils.SuccessResponse(c, companyProfileResponse, "Company profile fetched successfully") +} + +func (h *CompanyProfileHandler) GetCompanyProfilesByUserID(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + companyProfilesResponse, err := h.companyProfileService.GetCompanyProfilesByUserID(userID) + if err != nil { + return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch company profiles: %v", err)) + } + + return utils.NonPaginatedResponse(c, companyProfilesResponse, len(companyProfilesResponse), "Company profiles fetched successfully") +} + +func (h *CompanyProfileHandler) UpdateCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + id := c.Params("company_id") + + var requestDTO dto.RequestCompanyProfileDTO + if err := c.BodyParser(&requestDTO); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid input data"}}) + } + + companyProfileResponse, err := h.companyProfileService.UpdateCompanyProfile(id, &requestDTO) + if err != nil { + return utils.ErrorResponse(c, fmt.Sprintf("Failed to update company profile: %v", err)) + } + + return utils.SuccessResponse(c, companyProfileResponse, "Company profile updated successfully") +} + +func (h *CompanyProfileHandler) DeleteCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + id := c.Params("company_id") + + err := h.companyProfileService.DeleteCompanyProfile(id) + if err != nil { + return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete company profile: %v", err)) + } + + return utils.SuccessResponse(c, nil, "Company profile deleted successfully") +} diff --git a/internal/repositories/company_profile_repo.go b/internal/repositories/company_profile_repo.go new file mode 100644 index 0000000..bf0d4ea --- /dev/null +++ b/internal/repositories/company_profile_repo.go @@ -0,0 +1,78 @@ +package repositories + +import ( + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type CompanyProfileRepository interface { + CreateCompanyProfile(companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) + GetCompanyProfileByID(id string) (*model.CompanyProfile, error) + GetCompanyProfilesByUserID(userID string) ([]model.CompanyProfile, error) + UpdateCompanyProfile(id string, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) + DeleteCompanyProfile(id string) error +} + +type companyProfileRepository struct { + DB *gorm.DB +} + +func NewCompanyProfileRepository(db *gorm.DB) CompanyProfileRepository { + return &companyProfileRepository{ + DB: db, + } +} + +func (r *companyProfileRepository) CreateCompanyProfile(companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) { + err := r.DB.Create(companyProfile).Error + if err != nil { + return nil, fmt.Errorf("failed to create company profile: %v", err) + } + return companyProfile, nil +} + +func (r *companyProfileRepository) GetCompanyProfileByID(id string) (*model.CompanyProfile, error) { + var companyProfile model.CompanyProfile + err := r.DB.Preload("User").First(&companyProfile, "id = ?", id).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("company profile with ID %s not found", id) + } + return nil, fmt.Errorf("error fetching company profile: %v", err) + } + return &companyProfile, nil +} + +func (r *companyProfileRepository) GetCompanyProfilesByUserID(userID string) ([]model.CompanyProfile, error) { + var companyProfiles []model.CompanyProfile + err := r.DB.Preload("User").Where("user_id = ?", userID).Find(&companyProfiles).Error + if err != nil { + return nil, fmt.Errorf("error fetching company profiles for userID %s: %v", userID, err) + } + return companyProfiles, nil +} + +func (r *companyProfileRepository) UpdateCompanyProfile(id string, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) { + var existingProfile model.CompanyProfile + err := r.DB.First(&existingProfile, "id = ?", id).Error + if err != nil { + return nil, fmt.Errorf("company profile not found: %v", err) + } + + err = r.DB.Model(&existingProfile).Updates(companyProfile).Error + if err != nil { + return nil, fmt.Errorf("failed to update company profile: %v", err) + } + + return &existingProfile, nil +} + +func (r *companyProfileRepository) DeleteCompanyProfile(id string) error { + err := r.DB.Delete(&model.CompanyProfile{}, "id = ?", id).Error + if err != nil { + return fmt.Errorf("failed to delete company profile: %v", err) + } + return nil +} diff --git a/internal/services/company_profile_service.go b/internal/services/company_profile_service.go new file mode 100644 index 0000000..6c2714b --- /dev/null +++ b/internal/services/company_profile_service.go @@ -0,0 +1,163 @@ +package services + +import ( + "fmt" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" +) + +type CompanyProfileService interface { + CreateCompanyProfile(userID string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) + GetCompanyProfileByID(id string) (*dto.ResponseCompanyProfileDTO, error) + GetCompanyProfilesByUserID(userID string) ([]dto.ResponseCompanyProfileDTO, error) + UpdateCompanyProfile(id string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) + DeleteCompanyProfile(id string) error +} + +type companyProfileService struct { + companyProfileRepo repositories.CompanyProfileRepository +} + +func NewCompanyProfileService(companyProfileRepo repositories.CompanyProfileRepository) CompanyProfileService { + return &companyProfileService{ + companyProfileRepo: companyProfileRepo, + } +} + +func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*dto.ResponseCompanyProfileDTO, error) { + + createdAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.UpdatedAt) + + responseDTO := &dto.ResponseCompanyProfileDTO{ + ID: companyProfile.ID, + UserID: companyProfile.UserID, + CompanyName: companyProfile.CompanyName, + CompanyAddress: companyProfile.CompanyAddress, + CompanyPhone: companyProfile.CompanyPhone, + CompanyEmail: companyProfile.CompanyEmail, + CompanyLogo: companyProfile.CompanyLogo, + CompanyWebsite: companyProfile.CompanyWebsite, + TaxID: companyProfile.TaxID, + FoundedDate: companyProfile.FoundedDate, + CompanyType: companyProfile.CompanyType, + CompanyDescription: companyProfile.CompanyDescription, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return responseDTO, nil +} + +func (s *companyProfileService) CreateCompanyProfile(userID string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) { + + errors, valid := request.ValidateCompanyProfileInput() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + companyProfile := &model.CompanyProfile{ + UserID: userID, + CompanyName: request.CompanyName, + CompanyAddress: request.CompanyAddress, + CompanyPhone: request.CompanyPhone, + CompanyEmail: request.CompanyEmail, + CompanyLogo: request.CompanyLogo, + CompanyWebsite: request.CompanyWebsite, + TaxID: request.TaxID, + FoundedDate: request.FoundedDate, + CompanyType: request.CompanyType, + CompanyDescription: request.CompanyDescription, + } + + createdCompanyProfile, err := s.companyProfileRepo.CreateCompanyProfile(companyProfile) + if err != nil { + return nil, fmt.Errorf("failed to create company profile: %v", err) + } + + responseDTO, err := FormatResponseCompanyProfile(createdCompanyProfile) + if err != nil { + return nil, fmt.Errorf("failed to format company profile response: %v", err) + } + + return responseDTO, nil +} + +func (s *companyProfileService) GetCompanyProfileByID(id string) (*dto.ResponseCompanyProfileDTO, error) { + + companyProfile, err := s.companyProfileRepo.GetCompanyProfileByID(id) + if err != nil { + return nil, fmt.Errorf("error retrieving company profile by ID: %v", err) + } + + responseDTO, err := FormatResponseCompanyProfile(companyProfile) + if err != nil { + return nil, fmt.Errorf("error formatting company profile response: %v", err) + } + + return responseDTO, nil +} + +func (s *companyProfileService) GetCompanyProfilesByUserID(userID string) ([]dto.ResponseCompanyProfileDTO, error) { + + companyProfiles, err := s.companyProfileRepo.GetCompanyProfilesByUserID(userID) + if err != nil { + return nil, fmt.Errorf("error retrieving company profiles by userID: %v", err) + } + + var responseDTOs []dto.ResponseCompanyProfileDTO + for _, companyProfile := range companyProfiles { + responseDTO, err := FormatResponseCompanyProfile(&companyProfile) + if err != nil { + return nil, fmt.Errorf("error formatting company profile response: %v", err) + } + responseDTOs = append(responseDTOs, *responseDTO) + } + + return responseDTOs, nil +} + +func (s *companyProfileService) UpdateCompanyProfile(id string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) { + + errors, valid := request.ValidateCompanyProfileInput() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + companyProfile := &model.CompanyProfile{ + CompanyName: request.CompanyName, + CompanyAddress: request.CompanyAddress, + CompanyPhone: request.CompanyPhone, + CompanyEmail: request.CompanyEmail, + CompanyLogo: request.CompanyLogo, + CompanyWebsite: request.CompanyWebsite, + TaxID: request.TaxID, + FoundedDate: request.FoundedDate, + CompanyType: request.CompanyType, + CompanyDescription: request.CompanyDescription, + } + + updatedCompanyProfile, err := s.companyProfileRepo.UpdateCompanyProfile(id, companyProfile) + if err != nil { + return nil, fmt.Errorf("failed to update company profile: %v", err) + } + + responseDTO, err := FormatResponseCompanyProfile(updatedCompanyProfile) + if err != nil { + return nil, fmt.Errorf("failed to format company profile response: %v", err) + } + + return responseDTO, nil +} + +func (s *companyProfileService) DeleteCompanyProfile(id string) error { + + err := s.companyProfileRepo.DeleteCompanyProfile(id) + if err != nil { + return fmt.Errorf("failed to delete company profile: %v", err) + } + + return nil +} diff --git a/internal/services/identitycard_service.go b/internal/services/identitycard_service.go index b5e653b..f6b62ff 100644 --- a/internal/services/identitycard_service.go +++ b/internal/services/identitycard_service.go @@ -22,11 +22,13 @@ type IdentityCardService interface { type identityCardService struct { identityCardRepo repositories.IdentityCardRepository + userRepo repositories.UserProfileRepository } -func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository) IdentityCardService { +func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfileRepository) IdentityCardService { return &identityCardService{ identityCardRepo: identityCardRepo, + userRepo: userRepo, } } @@ -156,6 +158,17 @@ func (s *identityCardService) CreateIdentityCard(userID string, request *dto.Req return nil, fmt.Errorf("failed to create identity card: %v", err) } + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("failde to fint user: %v", err) + } + + user.RegistrationStatus = "onreview" + + err = s.userRepo.Update(user) + if err != nil { + return nil, fmt.Errorf("failed to update user: %v", err) + } idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) return idcardResponseDTO, nil diff --git a/model/company_profile_model.go b/model/company_profile_model.go index 521185a..915652e 100644 --- a/model/company_profile_model.go +++ b/model/company_profile_model.go @@ -12,13 +12,12 @@ type CompanyProfile struct { CompanyAddress string `gorm:"not null" json:"company_address"` CompanyPhone string `gorm:"not null" json:"company_phone"` CompanyEmail string `gorm:"not null" json:"company_email"` - CompanyLogo string `gorm:"not null" json:"company_logo"` - CompanyWebsite string `json:"company_website"` - TaxID string `json:"tax_id"` - FoundedDate time.Time `json:"founded_date"` - CompanyType string `gorm:"not null" json:"company_type"` + CompanyLogo string `json:"company_logo,omitempty"` + CompanyWebsite string `json:"company_website,omitempty"` + TaxID string `json:"tax_id,omitempty"` + FoundedDate string `json:"founded_date,omitempty"` + CompanyType string `json:"company_type,omitempty"` CompanyDescription string `gorm:"type:text" json:"company_description"` - CompanyStatus string `gorm:"not null" json:"company_status"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` } diff --git a/presentation/auth/auth_admin_route.go b/presentation/auth/auth_admin_route.go index 07f3324..f77535e 100644 --- a/presentation/auth/auth_admin_route.go +++ b/presentation/auth/auth_admin_route.go @@ -13,7 +13,7 @@ import ( "github.com/gofiber/fiber/v2" ) -func AdminAuthRouter(api fiber.Router) { +func AuthAdminRouter(api fiber.Router) { secretKey := os.Getenv("SECRET_KEY") if secretKey == "" { log.Fatal("SECRET_KEY is not set in the environment variables") diff --git a/presentation/company_profile_route.go b/presentation/company_profile_route.go new file mode 100644 index 0000000..013a463 --- /dev/null +++ b/presentation/company_profile_route.go @@ -0,0 +1,27 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func CompanyProfileRouter(api fiber.Router) { + + companyProfileRepo := repositories.NewCompanyProfileRepository(config.DB) + companyProfileService := services.NewCompanyProfileService(companyProfileRepo) + companyProfileHandler := handler.NewCompanyProfileHandler(companyProfileService) + + companyProfileAPI := api.Group("/company-profile") + companyProfileAPI.Use(middleware.AuthMiddleware) + + companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile) + companyProfileAPI.Get("/get/:company_id", companyProfileHandler.GetCompanyProfileByID) + companyProfileAPI.Get("/get", companyProfileHandler.GetCompanyProfilesByUserID) + companyProfileAPI.Put("/update/:company_id", companyProfileHandler.UpdateCompanyProfile) + companyProfileAPI.Delete("/delete/:company_id", companyProfileHandler.DeleteCompanyProfile) +} diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go index dfb187d..d77b848 100644 --- a/presentation/identitycard_route.go +++ b/presentation/identitycard_route.go @@ -13,7 +13,8 @@ import ( func IdentityCardRouter(api fiber.Router) { identityCardRepo := repositories.NewIdentityCardRepository(config.DB) - identityCardService := services.NewIdentityCardService(identityCardRepo) + userRepo := repositories.NewUserProfileRepository(config.DB) + identityCardService := services.NewIdentityCardService(identityCardRepo, userRepo) identityCardHandler := handler.NewIdentityCardHandler(identityCardService) identityCardApi := api.Group("/identitycard") diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 8846a88..998a2dd 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -18,12 +18,13 @@ func SetupRoutes(app *fiber.App) { // || auth router || // // presentation.AuthRouter(api) - presentationn.AdminAuthRouter(api) + presentationn.AuthAdminRouter(api) presentationn.AuthPengelolaRouter(api) presentationn.AuthPengepulRouter(api) presentationn.AuthMasyarakatRouter(api) // || auth router || // presentation.IdentityCardRouter(api) + presentation.CompanyProfileRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api) From 9f1a4eb8fad8246af08aa087b09c4e1c4190bd8c Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Tue, 6 May 2025 09:31:13 +0700 Subject: [PATCH 19/48] fix&refact: improve code and add field in trash c and trash cdetail --- dto/trash_dto.go | 10 +- dto/user_dto.go | 28 +- internal/handler/trash_handler.go | 17 +- internal/handler/user_handler.go | 140 +++---- internal/repositories/trash_repo.go | 20 + internal/repositories/user_repo.go | 57 ++- internal/services/identitycard_service.go | 4 +- internal/services/trash_service.go | 126 ++++++- internal/services/user_service.go | 439 +++++++++------------- model/role_model.go | 5 +- model/trash_model.go | 1 + presentation/identitycard_route.go | 2 +- presentation/user_route.go | 20 +- 13 files changed, 433 insertions(+), 436 deletions(-) diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 085fe59..9df527f 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -4,13 +4,15 @@ import "strings" type RequestTrashCategoryDTO struct { Name string `json:"name"` + Icon string `json:"icon"` } type ResponseTrashCategoryDTO struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` Details []ResponseTrashDetailDTO `json:"details,omitempty"` } diff --git a/dto/user_dto.go b/dto/user_dto.go index 0d74533..9558bd6 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -1,7 +1,7 @@ package dto import ( - "regexp" + "rijig/utils" "strings" ) @@ -18,13 +18,13 @@ type UserResponseDTO struct { UpdatedAt string `json:"updatedAt"` } -type UpdateUserDTO struct { +type RequestUserDTO struct { Name string `json:"name"` Phone string `json:"phone"` Email string `json:"email"` } -func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { +func (r *RequestUserDTO) Validate() (map[string][]string, bool) { errors := make(map[string][]string) if strings.TrimSpace(r.Name) == "" { @@ -33,10 +33,14 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { if strings.TrimSpace(r.Phone) == "" { errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !IsValidPhoneNumber(r.Phone) { + } else if !utils.IsValidPhoneNumber(r.Phone) { errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") } + if strings.TrimSpace(r.Email) != "" && !utils.IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + if len(errors) > 0 { return errors, false } @@ -44,18 +48,6 @@ func (r *UpdateUserDTO) Validate() (map[string][]string, bool) { return nil, true } -func IsUpdateValidPhoneNumber(phone string) bool { - - re := regexp.MustCompile(`^\+62\d{9,13}$`) - return re.MatchString(phone) -} - -func IsUPdateValidEmail(email string) bool { - - re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) - return re.MatchString(email) -} - type UpdatePasswordDTO struct { OldPassword string `json:"old_password"` NewPassword string `json:"new_password"` @@ -71,8 +63,8 @@ func (u *UpdatePasswordDTO) Validate() (map[string][]string, bool) { if u.NewPassword == "" { errors["new_password"] = append(errors["new_password"], "New password is required") - } else if len(u.NewPassword) < 8 { - errors["new_password"] = append(errors["new_password"], "Password must be at least 8 characters long") + } else if !utils.IsValidPassword(u.NewPassword) { + errors["new_password"] = append(errors["new_password"], "Password must contain at least one uppercase letter, one digit, and one special character") } if u.ConfirmNewPassword == "" { diff --git a/internal/handler/trash_handler.go b/internal/handler/trash_handler.go index afc85b5..e3cace8 100644 --- a/internal/handler/trash_handler.go +++ b/internal/handler/trash_handler.go @@ -1,6 +1,7 @@ package handler import ( + "log" "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -22,7 +23,13 @@ func (h *TrashHandler) CreateCategory(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } - categoryResponse, err := h.TrashService.CreateCategory(request) + iconTrash, err := c.FormFile("icon") + if err != nil { + log.Printf("Error retrieving card photo from request: %v", err) + return utils.ErrorResponse(c, "Card photo is required") + } + + categoryResponse, err := h.TrashService.CreateCategory(request, iconTrash) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to create category: "+err.Error()) } @@ -84,7 +91,13 @@ func (h *TrashHandler) UpdateCategory(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) } - updatedCategory, err := h.TrashService.UpdateCategory(id, request) + iconTrash, err := c.FormFile("icon") + if err != nil && err.Error() != "File not found" { + log.Printf("Error retrieving icon trash from request: %v", err) + return utils.ErrorResponse(c, "icon trash is required") + } + + updatedCategory, err := h.TrashService.UpdateCategory(id, request, iconTrash) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating category: "+err.Error()) } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index e479047..9b60bec 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -4,138 +4,98 @@ import ( "rijig/dto" "rijig/internal/services" "rijig/utils" + "strconv" "github.com/gofiber/fiber/v2" ) -type UserProfileHandler struct { - UserProfileService services.UserProfileService +type UserHandler struct { + userService services.UserService } -func NewUserProfileHandler(userProfileService services.UserProfileService) *UserProfileHandler { - return &UserProfileHandler{UserProfileService: userProfileService} +func NewUserHandler(userService services.UserService) *UserHandler { + return &UserHandler{userService: userService} } -func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { +func (h *UserHandler) UpdateUserAvatarHandler(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + avatar, err := c.FormFile("avatar") + if err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file provided") + } + + updatedUser, err := h.userService.UpdateUserAvatar(userID, avatar) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.SuccessResponse(c, updatedUser, "Avatar updated successfully") +} + +func (h *UserHandler) GetUserByIDHandler(c *fiber.Ctx) error { + // userID := c.Params("id") userID, ok := c.Locals("userID").(string) if !ok || userID == "" { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") } - userProfile, err := h.UserProfileService.GetUserProfile(userID) + user, err := h.userService.GetUserByID(userID) if err != nil { return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) } - return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") + return utils.SuccessResponse(c, user, "User retrieved successfully") } -func (h *UserProfileHandler) GetUserProfileById(c *fiber.Ctx) error { - userID := c.Params("userid") - if userID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{"userid": {"user ID is required"}}) +func (h *UserHandler) GetAllUsersHandler(c *fiber.Ctx) error { + + page := 1 + limit := 10 + + if p := c.Query("page"); p != "" { + page, _ = strconv.Atoi(p) } - // userID, ok := c.Locals("userID").(string) - // if !ok || userID == "" { - // return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - // } - - userProfile, err := h.UserProfileService.GetUserProfile(userID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + if l := c.Query("limit"); l != "" { + limit, _ = strconv.Atoi(l) } - return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") -} - -func (h *UserProfileHandler) GetAllUsers(c *fiber.Ctx) error { - users, err := h.UserProfileService.GetAllUsers() + users, err := h.userService.GetAllUsers(page, limit) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.SuccessResponse(c, users, "All users retrieved successfully") + return utils.PaginatedResponse(c, users, page, limit, len(users), "Users retrieved successfully") } -func (h *UserProfileHandler) GetUsersByRoleID(c *fiber.Ctx) error { - roleID := c.Params("roleid") - if roleID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{"roleId": {"Role ID is required"}}) +func (h *UserHandler) UpdateUserHandler(c *fiber.Ctx) error { + var request dto.RequestUserDTO + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") } - users, err := h.UserProfileService.GetUsersByRoleID(roleID) + userID := c.Locals("userID").(string) + updatedUser, err := h.userService.UpdateUser(userID, &request) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.SuccessResponse(c, users, "Users retrieved successfully") + return utils.SuccessResponse(c, updatedUser, "User profile updated successfully") } -func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { - var updateData dto.UpdateUserDTO - if err := c.BodyParser(&updateData); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) +func (h *UserHandler) UpdateUserPasswordHandler(c *fiber.Ctx) error { + var request dto.UpdatePasswordDTO + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") } - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - errors, valid := updateData.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - userResponse, err := h.UserProfileService.UpdateUserProfile(userID, updateData) - if err != nil { - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) - } - - return utils.SuccessResponse(c, userResponse, "User profile updated successfully") -} - -// func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { -// var passwordData dto.UpdatePasswordDTO -// if err := c.BodyParser(&passwordData); err != nil { -// return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) -// } - -// userID, ok := c.Locals("userID").(string) -// if !ok || userID == "" { -// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") -// } - -// errors, valid := passwordData.Validate() -// if !valid { -// return utils.ValidationErrorResponse(c, errors) -// } - -// message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData) -// if err != nil { -// return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) -// } - -// return utils.GenericResponse(c, fiber.StatusOK, message) -// } -func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - file, err := c.FormFile("avatar") - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file uploaded") - } - - message, err := h.UserProfileService.UpdateUserAvatar(userID, file) + userID := c.Locals("userID").(string) + err := h.userService.UpdateUserPassword(userID, request.OldPassword, request.NewPassword, request.ConfirmNewPassword) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.GenericResponse(c, fiber.StatusOK, message) + return utils.SuccessResponse(c, nil, "Password updated successfully") } diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 1fd4861..380716a 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -1,7 +1,9 @@ package repositories import ( + "errors" "fmt" + "log" "rijig/model" @@ -16,6 +18,7 @@ type TrashRepository interface { GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) UpdateCategoryName(id string, newName string) error + UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) UpdateTrashDetail(id string, description string, price float64) error DeleteCategory(id string) error DeleteTrashDetail(id string) error @@ -84,6 +87,23 @@ func (r *trashRepository) UpdateCategoryName(id string, newName string) error { return nil } +func (r *trashRepository) UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) { + var existingtrashCtgry model.TrashCategory + if err := r.DB.Where("id = ?", id).First(&existingtrashCtgry).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("trashCategory with ID %s not found", id) + } + log.Printf("Error fetching trash category for update: %v", err) + return nil, fmt.Errorf("error fetching trash category for update: %w", err) + } + + if err := r.DB.Save(&existingtrashCtgry).Error; err != nil { + log.Printf("Error updating trash category: %v", err) + return nil, fmt.Errorf("failed to update trash category: %w", err) + } + return &existingtrashCtgry, nil +} + func (r *trashRepository) UpdateTrashDetail(id string, description string, price float64) error { if err := r.DB.Model(&model.TrashDetail{}).Where("id = ?", id).Updates(model.TrashDetail{Description: description, Price: price}).Error; err != nil { return fmt.Errorf("failed to update trash detail: %v", err) diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go index 4243219..8c7e085 100644 --- a/internal/repositories/user_repo.go +++ b/internal/repositories/user_repo.go @@ -2,77 +2,76 @@ package repositories import ( "fmt" - "rijig/model" "gorm.io/gorm" ) -type UserProfileRepository interface { +type UserProfilRepository interface { FindByID(userID string) (*model.User, error) + FindAll(page, limit int) ([]model.User, error) Update(user *model.User) error UpdateAvatar(userID, avatarURL string) error - - FindAll() ([]model.User, error) - FindByRoleID(roleID string) ([]model.User, error) + UpdatePassword(userID string, newPassword string) error } -type userProfileRepository struct { +type userProfilRepository struct { DB *gorm.DB } -func NewUserProfileRepository(db *gorm.DB) UserProfileRepository { - return &userProfileRepository{DB: db} +func NewUserProfilRepository(db *gorm.DB) UserProfilRepository { + return &userProfilRepository{DB: db} } -func (r *userProfileRepository) FindByID(userID string) (*model.User, error) { +func (r *userProfilRepository) FindByID(userID string) (*model.User, error) { var user model.User err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("user with ID %s not found", userID) } - return nil, err + return nil, fmt.Errorf("error finding user with ID %s: %v", userID, err) } if user.Role == nil { - return nil, fmt.Errorf("role not found for this user") + return nil, fmt.Errorf("role not found for user ID %s", userID) } return &user, nil } -func (r *userProfileRepository) Update(user *model.User) error { +func (r *userProfilRepository) FindAll(page, limit int) ([]model.User, error) { + var users []model.User + offset := (page - 1) * limit + err := r.DB.Preload("Role").Offset(offset).Limit(limit).Find(&users).Error + if err != nil { + return nil, fmt.Errorf("error finding all users: %v", err) + } + return users, nil +} + +func (r *userProfilRepository) Update(user *model.User) error { err := r.DB.Save(user).Error if err != nil { - return err + return fmt.Errorf("error updating user: %v", err) } return nil } -func (r *userProfileRepository) UpdateAvatar(userID, avatarURL string) error { +func (r *userProfilRepository) UpdateAvatar(userID, avatarURL string) error { var user model.User err := r.DB.Model(&user).Where("id = ?", userID).Update("avatar", avatarURL).Error if err != nil { - return err + return fmt.Errorf("error updating avatar for user ID %s: %v", userID, err) } return nil } -func (r *userProfileRepository) FindAll() ([]model.User, error) { - var users []model.User - err := r.DB.Preload("Role").Find(&users).Error +func (r *userProfilRepository) UpdatePassword(userID string, newPassword string) error { + var user model.User + err := r.DB.Model(&user).Where("id = ?", userID).Update("password", newPassword).Error if err != nil { - return nil, err + return fmt.Errorf("error updating password for user ID %s: %v", userID, err) } - return users, nil -} - -func (r *userProfileRepository) FindByRoleID(roleID string) ([]model.User, error) { - var users []model.User - err := r.DB.Preload("Role").Where("role_id = ?", roleID).Find(&users).Error - if err != nil { - return nil, err - } - return users, nil + return nil } diff --git a/internal/services/identitycard_service.go b/internal/services/identitycard_service.go index f6b62ff..b2435cd 100644 --- a/internal/services/identitycard_service.go +++ b/internal/services/identitycard_service.go @@ -22,10 +22,10 @@ type IdentityCardService interface { type identityCardService struct { identityCardRepo repositories.IdentityCardRepository - userRepo repositories.UserProfileRepository + userRepo repositories.UserProfilRepository } -func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfileRepository) IdentityCardService { +func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfilRepository) IdentityCardService { return &identityCardService{ identityCardRepo: identityCardRepo, userRepo: userRepo, diff --git a/internal/services/trash_service.go b/internal/services/trash_service.go index dcc05ca..cd66add 100644 --- a/internal/services/trash_service.go +++ b/internal/services/trash_service.go @@ -2,23 +2,29 @@ package services import ( "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" "time" "rijig/dto" "rijig/internal/repositories" "rijig/model" "rijig/utils" + + "github.com/google/uuid" ) type TrashService interface { - CreateCategory(request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) + CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) AddDetailToCategory(request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO, error) GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDTO, error) - UpdateCategory(id string, request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) + UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) UpdateDetail(id string, request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) DeleteCategory(id string) error @@ -33,14 +39,81 @@ func NewTrashService(trashRepo repositories.TrashRepository) TrashService { return &trashService{TrashRepo: trashRepo} } -func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) { +func (s *trashService) saveIconOfTrash(iconTrash *multipart.FileHeader) (string, error) { + pathImage := "/uploads/icontrash/" + iconTrashDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(iconTrashDir); os.IsNotExist(err) { + + if err := os.MkdirAll(iconTrashDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for icon trash: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(iconTrash.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + iconTrashFIleName := fmt.Sprintf("%s_icontrash%s", uuid.New().String(), extension) + iconTrashPath := filepath.Join(iconTrashDir, iconTrashFIleName) + + src, err := iconTrash.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(iconTrashPath) + if err != nil { + return "", fmt.Errorf("failed to create icon trash file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save icon trash: %v", err) + } + + iconTrashUrl := fmt.Sprintf("%s%s", pathImage, iconTrashFIleName) + + return iconTrashUrl, nil +} + +func deleteIconTrashFIle(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { errors, valid := request.ValidateTrashCategoryInput() if !valid { return nil, fmt.Errorf("validation error: %v", errors) } + icontrashPath, err := s.saveIconOfTrash(iconTrash) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) + } + category := model.TrashCategory{ Name: request.Name, + Icon: icontrashPath, } if err := s.TrashRepo.CreateCategory(&category); err != nil { @@ -53,6 +126,7 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -70,6 +144,7 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO) (*dto categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: c.ID, Name: c.Name, + Icon: c.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, }) @@ -129,6 +204,7 @@ func (s *trashService) AddDetailToCategory(request dto.RequestTrashDetailDTO) (* categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, } @@ -153,6 +229,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: categoryData["id"].(string), Name: categoryData["name"].(string), + Icon: categoryData["icon"].(string), CreatedAt: categoryData["createdAt"].(string), UpdatedAt: categoryData["updatedAt"].(string), }) @@ -172,6 +249,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, }) @@ -196,6 +274,7 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO return &dto.ResponseTrashCategoryDTO{ ID: categoryData["id"].(string), Name: categoryData["name"].(string), + Icon: categoryData["icon"].(string), CreatedAt: categoryData["createdAt"].(string), UpdatedAt: categoryData["updatedAt"].(string), Details: details, @@ -213,6 +292,7 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO categoryDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -220,13 +300,15 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO if category.Details != nil { var detailsDTO []dto.ResponseTrashDetailDTO for _, detail := range category.Details { + createdAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) detailsDTO = append(detailsDTO, dto.ResponseTrashDetailDTO{ ID: detail.ID, CategoryID: detail.CategoryID, Description: detail.Description, Price: detail.Price, - CreatedAt: detail.CreatedAt.Format("02-01-2006 15:04"), - UpdatedAt: detail.UpdatedAt.Format("02-01-2006 15:04"), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }) } categoryDTO.Details = detailsDTO @@ -281,27 +363,49 @@ func (s *trashService) GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDT return detailDTO, nil } -func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategoryDTO) (*dto.ResponseTrashCategoryDTO, error) { +func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { errors, valid := request.ValidateTrashCategoryInput() if !valid { return nil, fmt.Errorf("validation error: %v", errors) } - if err := s.TrashRepo.UpdateCategoryName(id, request.Name); err != nil { - return nil, fmt.Errorf("failed to update category: %v", err) - } - category, err := s.TrashRepo.GetCategoryByID(id) if err != nil { return nil, fmt.Errorf("category not found: %v", err) } + if category.Icon != "" { + err := deleteIconTrashFIle(category.Icon) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + var iconTrashPath string + if iconPath != nil { + iconTrashPath, err = s.saveIconOfTrash(iconPath) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + } + + if iconTrashPath != "" { + category.Icon = iconTrashPath + } + + category, err = s.TrashRepo.UpdateCategory(id, category) + if err != nil { + log.Printf("Error updating trash category: %v", err) + return nil, fmt.Errorf("failed to update category: %v", err) + } + createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -319,6 +423,7 @@ func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategor categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ ID: c.ID, Name: c.Name, + Icon: c.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, }) @@ -376,6 +481,7 @@ func (s *trashService) UpdateDetail(id string, request dto.RequestTrashDetailDTO categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ ID: category.ID, Name: category.Name, + Icon: category.Icon, CreatedAt: ccreatedAt, UpdatedAt: cupdatedAt, } diff --git a/internal/services/user_service.go b/internal/services/user_service.go index 72a5994..05c2774 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -1,310 +1,193 @@ package services import ( - "encoding/json" - "errors" "fmt" "log" "mime/multipart" "os" "path/filepath" - "time" - "rijig/dto" "rijig/internal/repositories" "rijig/model" "rijig/utils" - // "golang.org/x/crypto/bcrypt" ) -var allowedExtensions = []string{".jpg", ".jpeg", ".png"} - -type UserProfileService interface { - GetUserProfile(userID string) (*dto.UserResponseDTO, error) - UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) - // UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) - UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) - - GetAllUsers() ([]dto.UserResponseDTO, error) - GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) +type UserService interface { + GetUserByID(userID string) (*dto.UserResponseDTO, error) + GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) + UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) + UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) + UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error } -type userProfileService struct { - UserRepo repositories.UserRepository - RoleRepo repositories.RoleRepository - UserProfileRepo repositories.UserProfileRepository +type userService struct { + userRepo repositories.UserProfilRepository } -func NewUserProfileService(userProfileRepo repositories.UserProfileRepository) UserProfileService { - return &userProfileService{UserProfileRepo: userProfileRepo} +func NewUserService(userRepo repositories.UserProfilRepository) UserService { + return &userService{userRepo: userRepo} } -func (s *userProfileService) prepareUserResponse(user *model.User) *dto.UserResponseDTO { +func (s *userService) GetUserByID(userID string) (*dto.UserResponseDTO, error) { + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("error retrieving user by ID: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("error formatting user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) { + users, err := s.userRepo.FindAll(page, limit) + if err != nil { + return nil, fmt.Errorf("error retrieving all users: %v", err) + } + + var userDTOs []dto.UserResponseDTO + for _, user := range users { + userDTO, err := s.formatUserResponse(&user) + if err != nil { + log.Printf("Error formatting user response for userID %s: %v", user.ID, err) + continue + } + userDTOs = append(userDTOs, *userDTO) + } + + return userDTOs, nil +} + +func (s *userService) UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) { + + errors, valid := request.Validate() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %v", err) + } + + user.Name = request.Name + user.Phone = request.Phone + user.Email = request.Email + + err = s.userRepo.Update(user) + if err != nil { + return nil, fmt.Errorf("error updating user: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("error formatting updated user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) { + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return nil, fmt.Errorf("user not found: %v", err) + } + + if *user.Avatar != "" { + err := s.deleteAvatarImage(*user.Avatar) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + avatarURL, err := s.saveAvatarImage(userID, avatar) + if err != nil { + return nil, fmt.Errorf("failed to save avatar image: %v", err) + } + + err = s.userRepo.UpdateAvatar(userID, avatarURL) + if err != nil { + return nil, fmt.Errorf("failed to update avatar in the database: %v", err) + } + + userDTO, err := s.formatUserResponse(user) + if err != nil { + return nil, fmt.Errorf("failed to format user response: %v", err) + } + + return userDTO, nil +} + +func (s *userService) UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error { + + // errors, valid := utils.ValidatePasswordUpdate(oldPassword, newPassword, confirmNewPassword) + // if !valid { + // return fmt.Errorf("password validation error: %v", errors) + // } + + user, err := s.userRepo.FindByID(userID) + if err != nil { + return fmt.Errorf("user not found: %v", err) + } + + if user.Password != oldPassword { + return fmt.Errorf("old password is incorrect") + } + + err = s.userRepo.UpdatePassword(userID, newPassword) + if err != nil { + return fmt.Errorf("error updating password: %v", err) + } + + return nil +} + +func (s *userService) formatUserResponse(user *model.User) (*dto.UserResponseDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - return &dto.UserResponseDTO{ + userDTO := &dto.UserResponseDTO{ ID: user.ID, - // Username: user.Username, + Username: user.Name, Avatar: user.Avatar, Name: user.Name, Phone: user.Phone, Email: user.Email, - // EmailVerified: user.EmailVerified, + EmailVerified: user.PhoneVerified, RoleName: user.Role.RoleName, CreatedAt: createdAt, UpdatedAt: updatedAt, } + + return userDTO, nil } -func (s *userProfileService) GetUserProfile(userID string) (*dto.UserResponseDTO, error) { +func (s *userService) saveAvatarImage(userID string, avatar *multipart.FileHeader) (string, error) { - cacheKey := fmt.Sprintf("userProfile:%s", userID) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { + pathImage := "/uploads/avatars/" + avatarDir := "./public" + os.Getenv("BASE_URL") + pathImage - userResponse := &dto.UserResponseDTO{} - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), userResponse); err != nil { - return nil, err - } - return userResponse, nil - } - } - - user, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return nil, errors.New("user not found") - } - - userResponse := s.prepareUserResponse(user) - - cacheData := map[string]interface{}{ - "data": userResponse, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching user profile to Redis: %v\n", err) - } - - return userResponse, nil -} - -func (s *userProfileService) GetAllUsers() ([]dto.UserResponseDTO, error) { - users, err := s.UserProfileRepo.FindAll() - if err != nil { - return nil, err - } - - var response []dto.UserResponseDTO - for _, user := range users { - response = append(response, dto.UserResponseDTO{ - ID: user.ID, - // Username: user.Username, - Avatar: user.Avatar, - Name: user.Name, - Phone: user.Phone, - // Email: user.Email, - // EmailVerified: user.EmailVerified, - RoleName: user.Role.RoleName, - CreatedAt: user.CreatedAt.Format(time.RFC3339), - UpdatedAt: user.UpdatedAt.Format(time.RFC3339), - }) - } - - return response, nil -} - -func (s *userProfileService) GetUsersByRoleID(roleID string) ([]dto.UserResponseDTO, error) { - users, err := s.UserProfileRepo.FindByRoleID(roleID) - if err != nil { - return nil, err - } - - var response []dto.UserResponseDTO - for _, user := range users { - response = append(response, dto.UserResponseDTO{ - ID: user.ID, - // Username: user.Username, - Avatar: user.Avatar, - Name: user.Name, - Phone: user.Phone, - // Email: user.Email, - // EmailVerified: user.EmailVerified, - RoleName: user.Role.RoleName, - CreatedAt: user.CreatedAt.Format(time.RFC3339), - UpdatedAt: user.UpdatedAt.Format(time.RFC3339), - }) - } - - return response, nil -} - -func (s *userProfileService) UpdateUserProfile(userID string, updateData dto.UpdateUserDTO) (*dto.UserResponseDTO, error) { - user, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return nil, errors.New("user not found") - } - - validationErrors, valid := updateData.Validate() - if !valid { - return nil, fmt.Errorf("validation failed: %v", validationErrors) - } - - if updateData.Name != "" { - user.Name = updateData.Name - } - - // if updateData.Phone != "" && updateData.Phone != user.Phone { - // if err := s.updatePhoneIfNeeded(user, updateData.Phone); err != nil { - // return nil, err - // } - // user.Phone = updateData.Phone - // } - - // if updateData.Email != "" && updateData.Email != user.Email { - // if err := s.updateEmailIfNeeded(user, updateData.Email); err != nil { - // return nil, err - // } - // user.Email = updateData.Email - // } - - err = s.UserProfileRepo.Update(user) - if err != nil { - return nil, fmt.Errorf("failed to update user: %v", err) - } - - userResponse := s.prepareUserResponse(user) - - cacheKey := fmt.Sprintf("userProfile:%s", userID) - cacheData := map[string]interface{}{ - "data": userResponse, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error updating cached user profile in Redis: %v\n", err) - } - - return userResponse, nil -} - -// func (s *userProfileService) updatePhoneIfNeeded(user *model.User, newPhone string) error { -// existingPhone, _ := s.UserRepo.FindByPhoneAndRole(newPhone, user.RoleID) -// if existingPhone != nil { -// return fmt.Errorf("phone number is already used for this role") -// } -// return nil -// } - -// func (s *userProfileService) updateEmailIfNeeded(user *model.User, newEmail string) error { -// existingEmail, _ := s.UserRepo.FindByEmailAndRole(newEmail, user.RoleID) -// if existingEmail != nil { -// return fmt.Errorf("email is already used for this role") -// } -// return nil -// } - -// func (s *userProfileService) UpdateUserPassword(userID string, passwordData dto.UpdatePasswordDTO) (string, error) { - -// validationErrors, valid := passwordData.Validate() -// if !valid { -// return "", fmt.Errorf("validation failed: %v", validationErrors) -// } - -// user, err := s.UserProfileRepo.FindByID(userID) -// if err != nil { -// return "", errors.New("user not found") -// } - -// if !CheckPasswordHash(passwordData.OldPassword, user.Password) { -// return "", errors.New("old password is incorrect") -// } - -// hashedPassword, err := bcrypt.GenerateFromPassword([]byte(passwordData.NewPassword), bcrypt.DefaultCost) -// if err != nil { -// return "", fmt.Errorf("failed to hash new password: %v", err) -// } - -// user.Password = string(hashedPassword) -// err = s.UserProfileRepo.Update(user) -// if err != nil { -// return "", fmt.Errorf("failed to update password: %v", err) -// } - -// return "Password berhasil diupdate", nil -// } - -func (s *userProfileService) UpdateUserAvatar(userID string, file *multipart.FileHeader) (string, error) { - baseURL := os.Getenv("BASE_URL") - if baseURL == "" { - return "", fmt.Errorf("BASE_URL is not set in environment variables") - } - - avatarDir := filepath.Join("./public", baseURL, "/uploads/avatars") - if err := ensureAvatarDirectoryExists(avatarDir); err != nil { - return "", err - } - - if err := validateAvatarFile(file); err != nil { - return "", err - } - - updatedUser, err := s.UserProfileRepo.FindByID(userID) - if err != nil { - return "", fmt.Errorf("failed to retrieve user data: %v", err) - } - - if updatedUser.Avatar != nil && *updatedUser.Avatar != "" { - oldAvatarPath := filepath.Join("./public", *updatedUser.Avatar) - if _, err := os.Stat(oldAvatarPath); err == nil { - - if err := os.Remove(oldAvatarPath); err != nil { - return "", fmt.Errorf("failed to remove old avatar: %v", err) - } - } else { - - log.Printf("Old avatar file not found: %s", oldAvatarPath) - } - } - - avatarURL, err := saveAvatarFile(file, userID, avatarDir) - if err != nil { - return "", err - } - - err = s.UserProfileRepo.UpdateAvatar(userID, avatarURL) - if err != nil { - return "", fmt.Errorf("failed to update avatar in the database: %v", err) - } - - return "Foto profil berhasil diupdate", nil -} - -func ensureAvatarDirectoryExists(avatarDir string) error { if _, err := os.Stat(avatarDir); os.IsNotExist(err) { if err := os.MkdirAll(avatarDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create avatar directory: %v", err) + return "", fmt.Errorf("failed to create directory for avatar: %v", err) } } - return nil -} -func validateAvatarFile(file *multipart.FileHeader) error { - extension := filepath.Ext(file.Filename) - for _, ext := range allowedExtensions { - if extension == ext { - return nil - } + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(avatar.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } - return fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") -} -func saveAvatarFile(file *multipart.FileHeader, userID, avatarDir string) (string, error) { - extension := filepath.Ext(file.Filename) avatarFileName := fmt.Sprintf("%s_avatar%s", userID, extension) avatarPath := filepath.Join(avatarDir, avatarFileName) - src, err := file.Open() + src, err := avatar.Open() if err != nil { return "", fmt.Errorf("failed to open uploaded file: %v", err) } @@ -312,15 +195,37 @@ func saveAvatarFile(file *multipart.FileHeader, userID, avatarDir string) (strin dst, err := os.Create(avatarPath) if err != nil { - return "", fmt.Errorf("failed to create file: %v", err) + return "", fmt.Errorf("failed to create avatar file: %v", err) } defer dst.Close() - _, err = dst.ReadFrom(src) - if err != nil { - return "", fmt.Errorf("failed to save avatar file: %v", err) + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save avatar: %v", err) } - relativePath := filepath.Join("/uploads/avatars", avatarFileName) - return relativePath, nil + avatarURL := fmt.Sprintf("%s%s", pathImage, avatarFileName) + + return avatarURL, nil +} + +func (s *userService) deleteAvatarImage(avatarPath string) error { + + if avatarPath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + avatarPath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete avatar image: %v", err) + } + + log.Printf("Avatar image deleted successfully: %s", absolutePath) + return nil } diff --git a/model/role_model.go b/model/role_model.go index 72c910e..14c6ca0 100644 --- a/model/role_model.go +++ b/model/role_model.go @@ -3,9 +3,8 @@ package model import "time" type Role struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - RoleName string `gorm:"unique;not null" json:"roleName"` - // Users []User `gorm:"foreignKey:RoleID" json:"users"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RoleName string `gorm:"unique;not null" json:"roleName"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/trash_model.go b/model/trash_model.go index 5bd05fb..7a90d0d 100644 --- a/model/trash_model.go +++ b/model/trash_model.go @@ -5,6 +5,7 @@ import "time" type TrashCategory struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` Name string `gorm:"not null" json:"name"` + Icon string `json:"icon,omitempty"` Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go index d77b848..16a00a2 100644 --- a/presentation/identitycard_route.go +++ b/presentation/identitycard_route.go @@ -13,7 +13,7 @@ import ( func IdentityCardRouter(api fiber.Router) { identityCardRepo := repositories.NewIdentityCardRepository(config.DB) - userRepo := repositories.NewUserProfileRepository(config.DB) + userRepo := repositories.NewUserProfilRepository(config.DB) identityCardService := services.NewIdentityCardService(identityCardRepo, userRepo) identityCardHandler := handler.NewIdentityCardHandler(identityCardService) diff --git a/presentation/user_route.go b/presentation/user_route.go index 86277eb..afe7d3e 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -11,19 +11,19 @@ import ( ) func UserProfileRouter(api fiber.Router) { - userProfileRepo := repositories.NewUserProfileRepository(config.DB) - userProfileService := services.NewUserProfileService(userProfileRepo) - userProfileHandler := handler.NewUserProfileHandler(userProfileService) + userProfileRepo := repositories.NewUserProfilRepository(config.DB) + userProfileService := services.NewUserService(userProfileRepo) + userProfileHandler := handler.NewUserHandler(userProfileService) userProfilRoute := api.Group("/user") - userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserProfile) + userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserByIDHandler) - userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsers) - userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) - userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) + userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsersHandler) + // userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) + // userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) - userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserProfile) - // userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPassword) - userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatar) + userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserHandler) + userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPasswordHandler) + userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatarHandler) } From 30d082f4e9c3c20d8985a0e8256e83a48f5f4288 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Tue, 6 May 2025 22:49:31 +0700 Subject: [PATCH 20/48] feat: add feature how to and how about --- config/database.go | 2 + dto/about_dto.go | 71 +++++ internal/handler/about_handler.go | 170 +++++++++++ internal/repositories/about_repo.go | 100 +++++++ internal/services/about_service.go | 426 ++++++++++++++++++++++++++++ model/about_model.go | 23 ++ presentation/about_route.go | 37 +++ router/setup_routes.go.go | 1 + 8 files changed, 830 insertions(+) create mode 100644 dto/about_dto.go create mode 100644 internal/handler/about_handler.go create mode 100644 internal/repositories/about_repo.go create mode 100644 internal/services/about_service.go create mode 100644 model/about_model.go create mode 100644 presentation/about_route.go diff --git a/config/database.go b/config/database.go index 34451f3..9f89033 100644 --- a/config/database.go +++ b/config/database.go @@ -60,6 +60,8 @@ func ConnectDatabase() { &model.Article{}, &model.Banner{}, &model.InitialCoint{}, + &model.About{}, + &model.AboutDetail{}, // =>Trash Model<= &model.TrashCategory{}, diff --git a/dto/about_dto.go b/dto/about_dto.go new file mode 100644 index 0000000..898e647 --- /dev/null +++ b/dto/about_dto.go @@ -0,0 +1,71 @@ +package dto + +import ( + "strings" +) + +type RequestAboutDTO struct { + Title string `json:"title"` + CoverImage string `json:"cover_image"` + // AboutDetail []RequestAboutDetailDTO `json:"about_detail"` +} + +func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Title) == "" { + errors["title"] = append(errors["title"], "Title is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +type ResponseAboutDTO struct { + ID string `json:"id"` + Title string `json:"title"` + CoverImage string `json:"cover_image"` + AboutDetail *[]ResponseAboutDetailDTO `json:"about_detail"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type RequestAboutDetailDTO struct { + AboutId string `json:"about_id"` + ImageDetail string `json:"image_detail"` + Description string `json:"description"` +} + +func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.AboutId) == "" { + errors["about_id"] = append(errors["about_id"], "About ID is required") + } + + if strings.TrimSpace(r.ImageDetail) == "" { + errors["image_detail"] = append(errors["image_detail"], "Image detail is required") + } + + if strings.TrimSpace(r.Description) == "" { + errors["description"] = append(errors["description"], "Description is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +type ResponseAboutDetailDTO struct { + ID string `json:"id"` + AboutID string `json:"about_id"` + ImageDetail string `json:"image_detail"` + Description string `json:"description"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/handler/about_handler.go b/internal/handler/about_handler.go new file mode 100644 index 0000000..ca4f581 --- /dev/null +++ b/internal/handler/about_handler.go @@ -0,0 +1,170 @@ +package handler + +import ( + "fmt" + "log" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AboutHandler struct { + AboutService services.AboutService +} + +func NewAboutHandler(aboutService services.AboutService) *AboutHandler { + return &AboutHandler{ + AboutService: aboutService, + } +} + +func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { + var request dto.RequestAboutDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.ErrorResponse(c, "Invalid input data") + } + + aboutCoverImage, err := c.FormFile("cover_image") + if err != nil { + log.Printf("Error retrieving cover image about from request: %v", err) + return utils.ErrorResponse(c, "cover_iamge is required") + } + + response, err := h.AboutService.CreateAbout(request, aboutCoverImage) + if err != nil { + log.Printf("Error creating About: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to create About: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully created About") +} + +func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { + id := c.Params("id") + + var request dto.RequestAboutDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.ErrorResponse(c, "Invalid input data") + } + + aboutCoverImage, err := c.FormFile("cover_image") + if err != nil { + log.Printf("Error retrieving cover image about from request: %v", err) + return utils.ErrorResponse(c, "cover_iamge is required") + } + + response, err := h.AboutService.UpdateAbout(id, request, aboutCoverImage) + if err != nil { + log.Printf("Error updating About: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to update About: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully updated About") +} + +func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error { + + response, err := h.AboutService.GetAllAbout() + if err != nil { + log.Printf("Error fetching all About: %v", err) + return utils.ErrorResponse(c, "Failed to fetch About list") + } + + return utils.PaginatedResponse(c, response, 1, len(response), len(response), "Successfully fetched About list") +} + +func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.AboutService.GetAboutByID(id) + if err != nil { + log.Printf("Error fetching About by ID: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully fetched About") +} + +func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.AboutService.GetAboutDetailById(id) + if err != nil { + log.Printf("Error fetching About detail by ID: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully fetched About") +} + +func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error { + id := c.Params("id") + + if err := h.AboutService.DeleteAbout(id); err != nil { + log.Printf("Error deleting About: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete About: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted About") +} + +func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { + var request dto.RequestAboutDetailDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.ErrorResponse(c, "Invalid input data") + } + + aboutDetailImage, err := c.FormFile("image_detail") + if err != nil { + log.Printf("Error retrieving image detail from request: %v", err) + return utils.ErrorResponse(c, "image_detail is required") + } + + response, err := h.AboutService.CreateAboutDetail(request, aboutDetailImage) + if err != nil { + log.Printf("Error creating AboutDetail: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to create AboutDetail: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully created AboutDetail") +} + +func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { + id := c.Params("id") + + var request dto.RequestAboutDetailDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.ErrorResponse(c, "Invalid input data") + } + + aboutDetailImage, err := c.FormFile("image_detail") + if err != nil { + log.Printf("Error retrieving image detail from request: %v", err) + return utils.ErrorResponse(c, "image_detail is required") + } + + response, err := h.AboutService.UpdateAboutDetail(id, request, aboutDetailImage) + if err != nil { + log.Printf("Error updating AboutDetail: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to update AboutDetail: %v", err)) + } + + return utils.SuccessResponse(c, response, "Successfully updated AboutDetail") +} + +func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error { + id := c.Params("id") + + if err := h.AboutService.DeleteAboutDetail(id); err != nil { + log.Printf("Error deleting AboutDetail: %v", err) + return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail") +} diff --git a/internal/repositories/about_repo.go b/internal/repositories/about_repo.go new file mode 100644 index 0000000..3535af9 --- /dev/null +++ b/internal/repositories/about_repo.go @@ -0,0 +1,100 @@ +package repositories + +import ( + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type AboutRepository interface { + CreateAbout(about *model.About) error + CreateAboutDetail(aboutDetail *model.AboutDetail) error + GetAllAbout() ([]model.About, error) + GetAboutByID(id string) (*model.About, error) + GetAboutDetailByID(id string) (*model.AboutDetail, error) + UpdateAbout(id string, about *model.About) (*model.About, error) + UpdateAboutDetail(id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) + DeleteAbout(id string) error + DeleteAboutDetail(id string) error +} + +type aboutRepository struct { + DB *gorm.DB +} + +func NewAboutRepository(db *gorm.DB) AboutRepository { + return &aboutRepository{DB: db} +} + +func (r *aboutRepository) CreateAbout(about *model.About) error { + if err := r.DB.Create(&about).Error; err != nil { + return fmt.Errorf("failed to create About: %v", err) + } + return nil +} + +func (r *aboutRepository) CreateAboutDetail(aboutDetail *model.AboutDetail) error { + if err := r.DB.Create(&aboutDetail).Error; err != nil { + return fmt.Errorf("failed to create AboutDetail: %v", err) + } + return nil +} + +func (r *aboutRepository) GetAllAbout() ([]model.About, error) { + var abouts []model.About + if err := r.DB.Find(&abouts).Error; err != nil { + return nil, fmt.Errorf("failed to fetch all About records: %v", err) + } + return abouts, nil +} + +func (r *aboutRepository) GetAboutByID(id string) (*model.About, error) { + var about model.About + if err := r.DB.Preload("AboutDetail").Where("id = ?", id).First(&about).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("about with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch About by ID: %v", err) + } + return &about, nil +} + +func (r *aboutRepository) GetAboutDetailByID(id string) (*model.AboutDetail, error) { + var aboutDetail model.AboutDetail + if err := r.DB.Where("id = ?", id).First(&aboutDetail).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("aboutdetail with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch About by ID: %v", err) + } + return &aboutDetail, nil +} + +func (r *aboutRepository) UpdateAbout(id string, about *model.About) (*model.About, error) { + if err := r.DB.Model(&about).Where("id = ?", id).Updates(about).Error; err != nil { + return nil, fmt.Errorf("failed to update About: %v", err) + } + return about, nil +} + +func (r *aboutRepository) UpdateAboutDetail(id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) { + if err := r.DB.Model(&aboutDetail).Where("id = ?", id).Updates(aboutDetail).Error; err != nil { + return nil, fmt.Errorf("failed to update AboutDetail: %v", err) + } + return aboutDetail, nil +} + +func (r *aboutRepository) DeleteAbout(id string) error { + if err := r.DB.Where("id = ?", id).Delete(&model.About{}).Error; err != nil { + return fmt.Errorf("failed to delete About: %v", err) + } + return nil +} + +func (r *aboutRepository) DeleteAboutDetail(id string) error { + if err := r.DB.Where("id = ?", id).Delete(&model.AboutDetail{}).Error; err != nil { + return fmt.Errorf("failed to delete AboutDetail: %v", err) + } + return nil +} diff --git a/internal/services/about_service.go b/internal/services/about_service.go new file mode 100644 index 0000000..1d1fcab --- /dev/null +++ b/internal/services/about_service.go @@ -0,0 +1,426 @@ +package services + +import ( + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" + + "github.com/google/uuid" +) + +type AboutService interface { + CreateAbout(request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) + UpdateAbout(id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) + GetAllAbout() ([]dto.ResponseAboutDTO, error) + GetAboutByID(id string) (*dto.ResponseAboutDTO, error) + GetAboutDetailById(id string) (*dto.ResponseAboutDetailDTO, error) + DeleteAbout(id string) error + + CreateAboutDetail(request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) + UpdateAboutDetail(id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) + DeleteAboutDetail(id string) error +} + +type aboutService struct { + aboutRepo repositories.AboutRepository +} + +func NewAboutService(aboutRepo repositories.AboutRepository) AboutService { + return &aboutService{aboutRepo: aboutRepo} +} + +func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutDetailDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) + + response := &dto.ResponseAboutDetailDTO{ + ID: about.ID, + AboutID: about.AboutID, + ImageDetail: about.ImageDetail, + Description: about.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func formatResponseAboutDTO(about *model.About) (*dto.ResponseAboutDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) + + response := &dto.ResponseAboutDTO{ + ID: about.ID, + Title: about.Title, + CoverImage: about.CoverImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader) (string, error) { + pathImage := "/uploads/coverabout/" + coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { + + if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { + return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverImageAbout.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_coverabout%s", uuid.New().String(), extension) + coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) + + src, err := coverImageAbout.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return "", fmt.Errorf("failed to create cover image about file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save cover image about: %v", err) + } + + coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) + + return coverImageAboutUrl, nil +} + +func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) { + pathImage := "/uploads/coverabout/coveraboutdetail" + coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { + + if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { + return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverImageAbout.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_coveraboutdetail_%s", uuid.New().String(), extension) + coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) + + src, err := coverImageAbout.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return "", fmt.Errorf("failed to create cover image about file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save cover image about: %v", err) + } + + coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) + + return coverImageAboutUrl, nil +} + +func deleteCoverImageAbout(coverimageAboutPath string) error { + if coverimageAboutPath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + coverimageAboutPath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *aboutService) CreateAbout(request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { + errors, valid := request.ValidateAbout() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + coverImageAboutPath, err := s.saveCoverImageAbout(coverImageAbout) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan cover image about: %v ", err) + } + + about := model.About{ + Title: request.Title, + CoverImage: coverImageAboutPath, + } + + if err := s.aboutRepo.CreateAbout(&about); err != nil { + return nil, fmt.Errorf("failed to create About: %v", err) + } + + response, err := formatResponseAboutDTO(&about) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + return response, nil +} + +func (s *aboutService) UpdateAbout(id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { + + errors, valid := request.ValidateAbout() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + about, err := s.aboutRepo.GetAboutByID(id) + if err != nil { + return nil, fmt.Errorf("about not found: %v", err) + } + + if about.CoverImage != "" { + err := deleteCoverImageAbout(about.CoverImage) + if err != nil { + return nil, fmt.Errorf("gagal mengahpus gambar lama: %v", err) + } + } + + var coverImageAboutPath string + if coverImageAbout != nil { + coverImageAboutPath, err = s.saveCoverImageAbout(coverImageAbout) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) + } + } + + about.Title = request.Title + if coverImageAboutPath != "" { + about.CoverImage = coverImageAboutPath + } + + updatedAbout, err := s.aboutRepo.UpdateAbout(id, about) + if err != nil { + return nil, fmt.Errorf("failed to update About: %v", err) + } + + response, err := formatResponseAboutDTO(updatedAbout) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + return response, nil +} + +func (s *aboutService) GetAllAbout() ([]dto.ResponseAboutDTO, error) { + + aboutList, err := s.aboutRepo.GetAllAbout() + if err != nil { + return nil, fmt.Errorf("failed to get About list: %v", err) + } + + var aboutDTOList []dto.ResponseAboutDTO + for _, about := range aboutList { + response, err := formatResponseAboutDTO(&about) + if err != nil { + log.Printf("Error formatting About response: %v", err) + continue + } + aboutDTOList = append(aboutDTOList, *response) + } + + return aboutDTOList, nil +} + +func (s *aboutService) GetAboutByID(id string) (*dto.ResponseAboutDTO, error) { + + about, err := s.aboutRepo.GetAboutByID(id) + if err != nil { + return nil, fmt.Errorf("about not found: %v", err) + } + + response, err := formatResponseAboutDTO(about) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + return response, nil +} + +func (s *aboutService) GetAboutDetailById(id string) (*dto.ResponseAboutDetailDTO, error) { + + about, err := s.aboutRepo.GetAboutDetailByID(id) + if err != nil { + return nil, fmt.Errorf("about not found: %v", err) + } + + response, err := formatResponseAboutDetailDTO(about) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + return response, nil +} + +func (s *aboutService) DeleteAbout(id string) error { + about, err := s.aboutRepo.GetAboutByID(id) + if err != nil { + return fmt.Errorf("about not found: %v", err) + } + + if about.CoverImage != "" { + err := deleteCoverImageAbout(about.CoverImage) + if err != nil { + return fmt.Errorf("gagal mengahpus gambar lama: %v", err) + } + } + + if err := s.aboutRepo.DeleteAbout(id); err != nil { + return fmt.Errorf("failed to delete About: %v", err) + } + + return nil +} + +func (s *aboutService) CreateAboutDetail(request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { + + errors, valid := request.ValidateAboutDetail() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + _, err := s.aboutRepo.GetAboutByID(request.AboutId) + if err != nil { + + return nil, fmt.Errorf("about_id tidak ditemukan: %v", err) + } + + coverImageAboutDetailPath, err := s.saveCoverImageAboutDetail(coverImageAboutDetail) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan cover image about detail: %v ", err) + } + + aboutDetail := model.AboutDetail{ + AboutID: request.AboutId, + ImageDetail: coverImageAboutDetailPath, + Description: request.Description, + } + + if err := s.aboutRepo.CreateAboutDetail(&aboutDetail); err != nil { + return nil, fmt.Errorf("failed to create AboutDetail: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.UpdatedAt) + + response := &dto.ResponseAboutDetailDTO{ + ID: aboutDetail.ID, + AboutID: aboutDetail.AboutID, + ImageDetail: aboutDetail.ImageDetail, + Description: aboutDetail.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func (s *aboutService) UpdateAboutDetail(id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { + + errors, valid := request.ValidateAboutDetail() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + aboutDetail, err := s.aboutRepo.GetAboutDetailByID(id) + if err != nil { + return nil, fmt.Errorf("about detail tidakck ditemukan: %v", err) + } + + if aboutDetail.ImageDetail != "" { + err := deleteCoverImageAbout(aboutDetail.ImageDetail) + if err != nil { + return nil, fmt.Errorf("gagal menghapus gambar lama: %v", err) + } + } + + var coverImageAboutDeatilPath string + if imageDetail != nil { + coverImageAboutDeatilPath, err = s.saveCoverImageAbout(imageDetail) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) + } + } + + aboutDetail.Description = request.Description + if coverImageAboutDeatilPath != "" { + aboutDetail.ImageDetail = coverImageAboutDeatilPath + } + + aboutDetail, err = s.aboutRepo.UpdateAboutDetail(id, aboutDetail) + if err != nil { + log.Printf("Error updating about detail: %v", err) + return nil, fmt.Errorf("failed to update about detail: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.UpdatedAt) + + response := &dto.ResponseAboutDetailDTO{ + ID: aboutDetail.ID, + AboutID: aboutDetail.AboutID, + ImageDetail: aboutDetail.ImageDetail, + Description: aboutDetail.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func (s *aboutService) DeleteAboutDetail(id string) error { + aboutDetail, err := s.aboutRepo.GetAboutDetailByID(id) + if err != nil { + return fmt.Errorf("about detail tidakck ditemukan: %v", err) + } + + if aboutDetail.ImageDetail != "" { + err := deleteCoverImageAbout(aboutDetail.ImageDetail) + if err != nil { + return fmt.Errorf("gagal menghapus gambar lama: %v", err) + } + } + + if err := s.aboutRepo.DeleteAboutDetail(id); err != nil { + return fmt.Errorf("failed to delete AboutDetail: %v", err) + } + return nil +} diff --git a/model/about_model.go b/model/about_model.go new file mode 100644 index 0000000..746d2fa --- /dev/null +++ b/model/about_model.go @@ -0,0 +1,23 @@ +package model + +import ( + "time" +) + +type About struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Title string `gorm:"not null" json:"title"` + CoverImage string `json:"cover_image"` + AboutDetail []AboutDetail `gorm:"foreignKey:AboutID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"about_detail"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` +} + +type AboutDetail struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + AboutID string `gorm:"not null" json:"about_id"` + ImageDetail string `json:"image_detail"` + Description string `json:"description"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` +} diff --git a/presentation/about_route.go b/presentation/about_route.go new file mode 100644 index 0000000..6616d4e --- /dev/null +++ b/presentation/about_route.go @@ -0,0 +1,37 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func AboutRouter(api fiber.Router) { + + aboutRepo := repositories.NewAboutRepository(config.DB) + aboutService := services.NewAboutService(aboutRepo) + aboutHandler := handler.NewAboutHandler(aboutService) + + aboutRoutes := api.Group("/about") + aboutRoute := api.Group("/about") + aboutRoutes.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) + + aboutRoute.Get("/", aboutHandler.GetAllAbout) + aboutRoute.Get("/:id", aboutHandler.GetAboutByID) + aboutRoutes.Post("/", aboutHandler.CreateAbout) + aboutRoutes.Put("/:id", aboutHandler.UpdateAbout) + aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) + + aboutDetailRoutes := api.Group("/about-detail") + aboutDetailRoutes.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) + aboutDetailRoute := api.Group("/about-detail") + aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) + aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) + aboutDetailRoutes.Put("/:id", aboutHandler.UpdateAboutDetail) + aboutDetailRoutes.Delete("/:id", aboutHandler.DeleteAboutDetail) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 998a2dd..a1f0305 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -34,6 +34,7 @@ func SetupRoutes(app *fiber.App) { presentation.ArticleRouter(api) presentation.BannerRouter(api) presentation.InitialCointRoute(api) + presentation.AboutRouter(api) presentation.TrashRouter(api) presentation.StoreRouter(api) presentation.ProductRouter(api) From 9e2ff76c3d52b670502969f07122b7465dcc46e5 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 7 May 2025 13:29:50 +0700 Subject: [PATCH 21/48] feat: add feature coverage area (uncompleted yet) --- config/database.go | 1 + dto/address_dto.go | 2 +- dto/coveragearea_dto.go | 34 ++++++ internal/handler/address_handler.go | 4 +- internal/handler/coveragearea_handler.go | 93 ++++++++++++++++ internal/repositories/coveragearea_repo.go | 69 ++++++++++++ internal/services/coveragearea_service.go | 124 +++++++++++++++++++++ model/coveragearea_model.go | 11 ++ presentation/coveragearea_route.go | 24 ++++ router/setup_routes.go.go | 1 + 10 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 dto/coveragearea_dto.go create mode 100644 internal/handler/coveragearea_handler.go create mode 100644 internal/repositories/coveragearea_repo.go create mode 100644 internal/services/coveragearea_service.go create mode 100644 model/coveragearea_model.go create mode 100644 presentation/coveragearea_route.go diff --git a/config/database.go b/config/database.go index 9f89033..263659e 100644 --- a/config/database.go +++ b/config/database.go @@ -62,6 +62,7 @@ func ConnectDatabase() { &model.InitialCoint{}, &model.About{}, &model.AboutDetail{}, + &model.CoverageArea{}, // =>Trash Model<= &model.TrashCategory{}, diff --git a/dto/address_dto.go b/dto/address_dto.go index eef67f0..bcf5400 100644 --- a/dto/address_dto.go +++ b/dto/address_dto.go @@ -26,7 +26,7 @@ type CreateAddressDTO struct { Geography string `json:"geography"` } -func (r *CreateAddressDTO) Validate() (map[string][]string, bool) { +func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { errors := make(map[string][]string) if strings.TrimSpace(r.Province) == "" { diff --git a/dto/coveragearea_dto.go b/dto/coveragearea_dto.go new file mode 100644 index 0000000..3d1f46d --- /dev/null +++ b/dto/coveragearea_dto.go @@ -0,0 +1,34 @@ +package dto + +import "strings" + +type RequestCoverageArea struct { + Province string `json:"province"` + Regency string `json:"regency"` +} + +type ResponseCoverageArea struct { + ID string `json:"id"` + Province string `json:"province"` + Regency string `json:"regency"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +func (r *RequestCoverageArea) ValidateCoverageArea() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Province) == "" { + errors["province"] = append(errors["province"], "nama provinsi harus diisi") + } + + if strings.TrimSpace(r.Regency) == "" { + errors["regency"] = append(errors["regency"], "nama regency harus diisi") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/address_handler.go b/internal/handler/address_handler.go index 46a550d..de69c47 100644 --- a/internal/handler/address_handler.go +++ b/internal/handler/address_handler.go @@ -22,7 +22,7 @@ func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } - errors, valid := requestAddressDTO.Validate() + errors, valid := requestAddressDTO.ValidateAddress() if !valid { return utils.ValidationErrorResponse(c, errors) } @@ -67,7 +67,7 @@ func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } - errors, valid := addressDTO.Validate() + errors, valid := addressDTO.ValidateAddress() if !valid { return utils.ValidationErrorResponse(c, errors) } diff --git a/internal/handler/coveragearea_handler.go b/internal/handler/coveragearea_handler.go new file mode 100644 index 0000000..08cb17d --- /dev/null +++ b/internal/handler/coveragearea_handler.go @@ -0,0 +1,93 @@ +package handler + +import ( + "fmt" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CoverageAreaHandler struct { + service services.CoverageAreaService +} + +func NewCoverageAreaHandler(service services.CoverageAreaService) *CoverageAreaHandler { + return &CoverageAreaHandler{service: service} +} + +func (h *CoverageAreaHandler) CreateCoverageArea(c *fiber.Ctx) error { + var request dto.RequestCoverageArea + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"Invalid request body"}, + }) + } + + errors, valid := request.ValidateCoverageArea() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + response, err := h.service.CreateCoverageArea(request) + if err != nil { + return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating coverage area: %v", err)) + } + + return utils.SuccessResponse(c, response, "Coverage area created successfully") +} + +func (h *CoverageAreaHandler) GetCoverageAreaByID(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.service.GetCoverageAreaByID(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) + } + + return utils.SuccessResponse(c, response, "Coverage area found") +} + +func (h *CoverageAreaHandler) GetAllCoverageAreas(c *fiber.Ctx) error { + + response, err := h.service.GetAllCoverageAreas() + if err != nil { + return utils.InternalServerErrorResponse(c, "Error fetching coverage areas") + } + + return utils.SuccessResponse(c, response, "Coverage areas fetched successfully") +} + +func (h *CoverageAreaHandler) UpdateCoverageArea(c *fiber.Ctx) error { + id := c.Params("id") + var request dto.RequestCoverageArea + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"Invalid request body"}, + }) + } + + errors, valid := request.ValidateCoverageArea() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + response, err := h.service.UpdateCoverageArea(id, request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) + } + + return utils.SuccessResponse(c, response, "Coverage area updated successfully") +} + +func (h *CoverageAreaHandler) DeleteCoverageArea(c *fiber.Ctx) error { + id := c.Params("id") + + err := h.service.DeleteCoverageArea(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Coverage area deleted successfully") +} diff --git a/internal/repositories/coveragearea_repo.go b/internal/repositories/coveragearea_repo.go new file mode 100644 index 0000000..1ddf70d --- /dev/null +++ b/internal/repositories/coveragearea_repo.go @@ -0,0 +1,69 @@ +package repositories + +import ( + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type CoverageAreaRepository interface { + CreateCoverage(coverage *model.CoverageArea) error + FindCoverageById(id string) (*model.CoverageArea, error) + FindAllCoverage() ([]model.CoverageArea, error) + UpdateCoverage(id string, coverage *model.CoverageArea) error + DeleteCoverage(id string) error +} + +type coverageAreaRepository struct { + DB *gorm.DB +} + +func NewCoverageAreaRepository(db *gorm.DB) CoverageAreaRepository { + return &coverageAreaRepository{DB: db} +} + +func (r *coverageAreaRepository) CreateCoverage(coverage *model.CoverageArea) error { + if err := r.DB.Create(coverage).Error; err != nil { + return fmt.Errorf("failed to create coverage: %v", err) + } + return nil +} + +func (r *coverageAreaRepository) FindCoverageById(id string) (*model.CoverageArea, error) { + var coverage model.CoverageArea + err := r.DB.Where("id = ?", id).First(&coverage).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("coverage with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch coverage by ID: %v", err) + } + return &coverage, nil +} + +func (r *coverageAreaRepository) FindAllCoverage() ([]model.CoverageArea, error) { + var coverage []model.CoverageArea + err := r.DB.Find(&coverage).Error + if err != nil { + return nil, fmt.Errorf("failed to fetch coverage: %v", err) + } + + return coverage, nil +} + +func (r *coverageAreaRepository) UpdateCoverage(id string, coverage *model.CoverageArea) error { + err := r.DB.Model(&model.CoverageArea{}).Where("id = ?", id).Updates(coverage).Error + if err != nil { + return fmt.Errorf("failed to update coverage: %v", err) + } + return nil +} + +func (r *coverageAreaRepository) DeleteCoverage(id string) error { + result := r.DB.Delete(&model.CoverageArea{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete coverage: %v", result.Error) + } + return nil +} diff --git a/internal/services/coveragearea_service.go b/internal/services/coveragearea_service.go new file mode 100644 index 0000000..f80faea --- /dev/null +++ b/internal/services/coveragearea_service.go @@ -0,0 +1,124 @@ +package services + +import ( + "fmt" + "log" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" +) + +type CoverageAreaService interface { + CreateCoverageArea(request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) + GetCoverageAreaByID(id string) (*dto.ResponseCoverageArea, error) + GetAllCoverageAreas() ([]dto.ResponseCoverageArea, error) + UpdateCoverageArea(id string, request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) + DeleteCoverageArea(id string) error +} + +type coverageAreaService struct { + repo repositories.CoverageAreaRepository +} + +func NewCoverageAreaService(repo repositories.CoverageAreaRepository) CoverageAreaService { + return &coverageAreaService{repo: repo} +} + +func ConvertCoverageAreaToResponse(coverage *model.CoverageArea) *dto.ResponseCoverageArea { + createdAt, _ := utils.FormatDateToIndonesianFormat(coverage.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(coverage.UpdatedAt) + + return &dto.ResponseCoverageArea{ + ID: coverage.ID, + Province: coverage.Province, + Regency: coverage.Regency, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} + +func (s *coverageAreaService) CreateCoverageArea(request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) { + errors, valid := request.ValidateCoverageArea() + if !valid { + return nil, fmt.Errorf("validation errors: %v", errors) + } + + coverage := model.CoverageArea{ + Province: request.Province, + Regency: request.Regency, + } + + if err := s.repo.CreateCoverage(&coverage); err != nil { + return nil, fmt.Errorf("failed to create coverage area: %v", err) + } + + response := ConvertCoverageAreaToResponse(&coverage) + + return response, nil +} + +func (s *coverageAreaService) GetCoverageAreaByID(id string) (*dto.ResponseCoverageArea, error) { + coverage, err := s.repo.FindCoverageById(id) + if err != nil { + return nil, err + } + + response := ConvertCoverageAreaToResponse(coverage) + + return response, nil +} + +func (s *coverageAreaService) GetAllCoverageAreas() ([]dto.ResponseCoverageArea, error) { + coverageAreas, err := s.repo.FindAllCoverage() + if err != nil { + return nil, err + } + + var response []dto.ResponseCoverageArea + for _, coverage := range coverageAreas { + + response = append(response, *ConvertCoverageAreaToResponse(&coverage)) + } + + return response, nil +} + +func (s *coverageAreaService) UpdateCoverageArea(id string, request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) { + + errors, valid := request.ValidateCoverageArea() + if !valid { + return nil, fmt.Errorf("validation errors: %v", errors) + } + + coverage, err := s.repo.FindCoverageById(id) + if err != nil { + return nil, fmt.Errorf("coverage area with ID %s not found: %v", id, err) + } + + coverage.Province = request.Province + coverage.Regency = request.Regency + + if err := s.repo.UpdateCoverage(id, coverage); err != nil { + return nil, fmt.Errorf("failed to update coverage area: %v", err) + } + + response := ConvertCoverageAreaToResponse(coverage) + + return response, nil +} + +func (s *coverageAreaService) DeleteCoverageArea(id string) error { + + coverage, err := s.repo.FindCoverageById(id) + if err != nil { + return fmt.Errorf("coverage area with ID %s not found: %v", id, err) + } + + if err := s.repo.DeleteCoverage(id); err != nil { + return fmt.Errorf("failed to delete coverage area: %v", err) + } + + log.Printf("Coverage area with ID %s successfully deleted", coverage.ID) + return nil +} diff --git a/model/coveragearea_model.go b/model/coveragearea_model.go new file mode 100644 index 0000000..4cff160 --- /dev/null +++ b/model/coveragearea_model.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type CoverageArea struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + Province string `gorm:"not null" json:"province"` + Regency string `gorm:"not null" json:"regency"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/coveragearea_route.go b/presentation/coveragearea_route.go new file mode 100644 index 0000000..aa91c14 --- /dev/null +++ b/presentation/coveragearea_route.go @@ -0,0 +1,24 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + + "github.com/gofiber/fiber/v2" +) + +func CoverageAreaRouter(api fiber.Router) { + coverageAreaRepo := repositories.NewCoverageAreaRepository(config.DB) + coverageAreaService := services.NewCoverageAreaService(coverageAreaRepo) + coverageAreaHandler := handler.NewCoverageAreaHandler(coverageAreaService) + + coverage := api.Group("/coveragearea") + + coverage.Post("/", coverageAreaHandler.CreateCoverageArea) + coverage.Get("/", coverageAreaHandler.GetAllCoverageAreas) + coverage.Get("/:id", coverageAreaHandler.GetCoverageAreaByID) + coverage.Put("/:id", coverageAreaHandler.UpdateCoverageArea) + coverage.Delete("/:id", coverageAreaHandler.DeleteCoverageArea) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index a1f0305..f313766 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -36,6 +36,7 @@ func SetupRoutes(app *fiber.App) { presentation.InitialCointRoute(api) presentation.AboutRouter(api) presentation.TrashRouter(api) + presentation.CoverageAreaRouter(api) presentation.StoreRouter(api) presentation.ProductRouter(api) } From 1c0636bba4008596ea9f4f08b036a2355f5f791a Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 7 May 2025 14:03:20 +0700 Subject: [PATCH 22/48] refact: add existing value validation in coverage area --- internal/repositories/coveragearea_repo.go | 13 +++++++ internal/services/coveragearea_service.go | 45 ++++++++++++++++++---- presentation/coveragearea_route.go | 3 +- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/internal/repositories/coveragearea_repo.go b/internal/repositories/coveragearea_repo.go index 1ddf70d..02674a9 100644 --- a/internal/repositories/coveragearea_repo.go +++ b/internal/repositories/coveragearea_repo.go @@ -8,6 +8,7 @@ import ( ) type CoverageAreaRepository interface { + FindCoverageByProvinceAndRegency(province, regency string) (*model.CoverageArea, error) CreateCoverage(coverage *model.CoverageArea) error FindCoverageById(id string) (*model.CoverageArea, error) FindAllCoverage() ([]model.CoverageArea, error) @@ -23,6 +24,18 @@ func NewCoverageAreaRepository(db *gorm.DB) CoverageAreaRepository { return &coverageAreaRepository{DB: db} } +func (r *coverageAreaRepository) FindCoverageByProvinceAndRegency(province, regency string) (*model.CoverageArea, error) { + var coverage model.CoverageArea + err := r.DB.Where("province = ? AND regency = ?", province, regency).First(&coverage).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &coverage, nil +} + func (r *coverageAreaRepository) CreateCoverage(coverage *model.CoverageArea) error { if err := r.DB.Create(coverage).Error; err != nil { return fmt.Errorf("failed to create coverage: %v", err) diff --git a/internal/services/coveragearea_service.go b/internal/services/coveragearea_service.go index f80faea..1a0f522 100644 --- a/internal/services/coveragearea_service.go +++ b/internal/services/coveragearea_service.go @@ -18,11 +18,12 @@ type CoverageAreaService interface { } type coverageAreaService struct { - repo repositories.CoverageAreaRepository + repo repositories.CoverageAreaRepository + WilayahRepo repositories.WilayahIndonesiaRepository } -func NewCoverageAreaService(repo repositories.CoverageAreaRepository) CoverageAreaService { - return &coverageAreaService{repo: repo} +func NewCoverageAreaService(repo repositories.CoverageAreaRepository, WilayahRepo repositories.WilayahIndonesiaRepository) CoverageAreaService { + return &coverageAreaService{repo: repo, WilayahRepo: WilayahRepo} } func ConvertCoverageAreaToResponse(coverage *model.CoverageArea) *dto.ResponseCoverageArea { @@ -44,9 +45,24 @@ func (s *coverageAreaService) CreateCoverageArea(request dto.RequestCoverageArea return nil, fmt.Errorf("validation errors: %v", errors) } + province, _, err := s.WilayahRepo.FindProvinceByID(request.Province, 0, 0) + if err != nil { + return nil, fmt.Errorf("invalid province_id") + } + + regency, _, err := s.WilayahRepo.FindRegencyByID(request.Regency, 0, 0) + if err != nil { + return nil, fmt.Errorf("invalid regency_id") + } + + existingCoverage, err := s.repo.FindCoverageByProvinceAndRegency(province.Name, regency.Name) + if err == nil && existingCoverage != nil { + return nil, fmt.Errorf("coverage area with province %s and regency %s already exists", province.Name, regency.Name) + } + coverage := model.CoverageArea{ - Province: request.Province, - Regency: request.Regency, + Province: province.Name, + Regency: regency.Name, } if err := s.repo.CreateCoverage(&coverage); err != nil { @@ -96,8 +112,23 @@ func (s *coverageAreaService) UpdateCoverageArea(id string, request dto.RequestC return nil, fmt.Errorf("coverage area with ID %s not found: %v", id, err) } - coverage.Province = request.Province - coverage.Regency = request.Regency + province, _, err := s.WilayahRepo.FindProvinceByID(request.Province, 0, 0) + if err != nil { + return nil, fmt.Errorf("invalid province_id") + } + + regency, _, err := s.WilayahRepo.FindRegencyByID(request.Regency, 0, 0) + if err != nil { + return nil, fmt.Errorf("invalid regency_id") + } + + existingCoverage, err := s.repo.FindCoverageByProvinceAndRegency(province.Name, regency.Name) + if err == nil && existingCoverage != nil { + return nil, fmt.Errorf("coverage area with province %s and regency %s already exists", province.Name, regency.Name) + } + + coverage.Province = province.Name + coverage.Regency = regency.Name if err := s.repo.UpdateCoverage(id, coverage); err != nil { return nil, fmt.Errorf("failed to update coverage area: %v", err) diff --git a/presentation/coveragearea_route.go b/presentation/coveragearea_route.go index aa91c14..a1603ed 100644 --- a/presentation/coveragearea_route.go +++ b/presentation/coveragearea_route.go @@ -11,7 +11,8 @@ import ( func CoverageAreaRouter(api fiber.Router) { coverageAreaRepo := repositories.NewCoverageAreaRepository(config.DB) - coverageAreaService := services.NewCoverageAreaService(coverageAreaRepo) + wilayahRepo := repositories.NewWilayahIndonesiaRepository(config.DB) + coverageAreaService := services.NewCoverageAreaService(coverageAreaRepo, wilayahRepo) coverageAreaHandler := handler.NewCoverageAreaHandler(coverageAreaService) coverage := api.Group("/coveragearea") From 02db7d5dcad212b0b1eb3eab0cfc231489a60650 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 7 May 2025 21:54:58 +0700 Subject: [PATCH 23/48] feat: add feature request pickup(uncompleted yet) --- config/database.go | 5 + dto/requestpickup_dto.go | 80 +++++++++ internal/handler/requestpickup_handler.go | 101 +++++++++++ internal/repositories/requestpickup_repo.go | 114 ++++++++++++ internal/services/requestpickup_service.go | 181 ++++++++++++++++++++ model/requestpickup_model.go | 24 +++ presentation/requestpickup_route.go | 31 ++++ router/setup_routes.go.go | 1 + 8 files changed, 537 insertions(+) create mode 100644 dto/requestpickup_dto.go create mode 100644 internal/handler/requestpickup_handler.go create mode 100644 internal/repositories/requestpickup_repo.go create mode 100644 internal/services/requestpickup_service.go create mode 100644 model/requestpickup_model.go create mode 100644 presentation/requestpickup_route.go diff --git a/config/database.go b/config/database.go index 263659e..9bb56e9 100644 --- a/config/database.go +++ b/config/database.go @@ -47,7 +47,12 @@ func ConnectDatabase() { &model.Address{}, &model.IdentityCard{}, &model.CompanyProfile{}, + // =>user preparation<= + // =>requestpickup preparation<= + &model.RequestPickup{}, + &model.RequestPickupItem{}, + // =>requestpickup preparation<= // =>store preparation<= &model.Store{}, diff --git a/dto/requestpickup_dto.go b/dto/requestpickup_dto.go new file mode 100644 index 0000000..73764bf --- /dev/null +++ b/dto/requestpickup_dto.go @@ -0,0 +1,80 @@ +package dto + +import ( + "fmt" + "strings" +) + +type RequestPickup struct { + RequestItems []RequestPickupItem `json:"request_items"` + EvidenceImage string `json:"evidence_image"` + AddressID string `json:"address_id"` +} + +type RequestPickupItem struct { + TrashCategoryID string `json:"trash_category_id"` + EstimatedAmount float64 `json:"estimated_amount"` +} + +type ResponseRequestPickup struct { + ID string `json:"id"` + UserId string `json:"user_id"` + AddressID string `json:"address_id"` + EvidenceImage string `json:"evidence_image"` + StatusPickup string `json:"status_pickup"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + RequestItems []ResponseRequestPickupItem `json:"request_items"` +} + +type ResponseRequestPickupItem struct { + ID string `json:"id"` + // TrashCategoryID string `json:"trash_category_id"` + TrashCategoryName string `json:"trash_category_name"` + EstimatedAmount float64 `json:"estimated_amount"` +} + +func (r *RequestPickup) ValidateRequestPickup() (map[string][]string, bool) { + errors := make(map[string][]string) + + if len(r.RequestItems) == 0 { + errors["request_items"] = append(errors["request_items"], "At least one item must be provided") + } + + if strings.TrimSpace(r.AddressID) == "" { + errors["address_id"] = append(errors["address_id"], "Address ID must be provided") + } + + for i, item := range r.RequestItems { + itemErrors, valid := item.ValidateRequestPickupItem(i) + if !valid { + for field, msgs := range itemErrors { + errors[field] = append(errors[field], msgs...) + } + } + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func (r *RequestPickupItem) ValidateRequestPickupItem(index int) (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.TrashCategoryID) == "" { + errors["trash_category_id"] = append(errors["trash_category_id"], fmt.Sprintf("Trash category ID cannot be empty (Item %d)", index+1)) + } + + if r.EstimatedAmount < 2 { + errors["estimated_amount"] = append(errors["estimated_amount"], fmt.Sprintf("Estimated amount must be >= 2.0 kg (Item %d)", index+1)) + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/requestpickup_handler.go b/internal/handler/requestpickup_handler.go new file mode 100644 index 0000000..4ce19fe --- /dev/null +++ b/internal/handler/requestpickup_handler.go @@ -0,0 +1,101 @@ +package handler + +import ( + "fmt" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type RequestPickupHandler struct { + service services.RequestPickupService +} + +func NewRequestPickupHandler(service services.RequestPickupService) *RequestPickupHandler { + return &RequestPickupHandler{service: service} +} + +func (h *RequestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + var request dto.RequestPickup + + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") + } + + errors, valid := request.ValidateRequestPickup() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + response, err := h.service.CreateRequestPickup(request, userID) + if err != nil { + return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating request pickup: %v", err)) + } + + return utils.SuccessResponse(c, response, "Request pickup created successfully") +} + +func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.service.GetRequestPickupByID(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) + } + + return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") +} + +func (h *RequestPickupHandler) GetAllRequestPickups(c *fiber.Ctx) error { + + response, err := h.service.GetAllRequestPickups() + if err != nil { + return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error fetching all request pickups: %v", err)) + } + + return utils.SuccessResponse(c, response, "All request pickups retrieved successfully") +} + +func (h *RequestPickupHandler) UpdateRequestPickup(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + id := c.Params("id") + var request dto.RequestPickup + + if err := c.BodyParser(&request); err != nil { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") + } + + errors, valid := request.ValidateRequestPickup() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + response, err := h.service.UpdateRequestPickup(id, request) + if err != nil { + return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error updating request pickup: %v", err)) + } + + return utils.SuccessResponse(c, response, "Request pickup updated successfully") +} + +func (h *RequestPickupHandler) DeleteRequestPickup(c *fiber.Ctx) error { + id := c.Params("id") + + err := h.service.DeleteRequestPickup(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Request pickup deleted successfully") +} diff --git a/internal/repositories/requestpickup_repo.go b/internal/repositories/requestpickup_repo.go new file mode 100644 index 0000000..912c83e --- /dev/null +++ b/internal/repositories/requestpickup_repo.go @@ -0,0 +1,114 @@ +package repositories + +import ( + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type RequestPickupRepository interface { + CreateRequestPickup(request *model.RequestPickup) error + CreateRequestPickupItem(item *model.RequestPickupItem) error + FindRequestPickupByID(id string) (*model.RequestPickup, error) + FindAllRequestPickups() ([]model.RequestPickup, error) + UpdateRequestPickup(id string, request *model.RequestPickup) error + DeleteRequestPickup(id string) error + FindRequestPickupByAddressAndCategory(addressID string, trashCategoryID string) (*model.RequestPickup, error) + FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) +} + +type requestPickupRepository struct { + DB *gorm.DB +} + +func NewRequestPickupRepository(db *gorm.DB) RequestPickupRepository { + return &requestPickupRepository{DB: db} +} + +func (r *requestPickupRepository) CreateRequestPickup(request *model.RequestPickup) error { + if err := r.DB.Create(request).Error; err != nil { + return fmt.Errorf("failed to create request pickup: %v", err) + } + + for _, item := range request.RequestItems { + item.RequestPickupId = request.ID + if err := r.DB.Create(&item).Error; err != nil { + return fmt.Errorf("failed to create request pickup item: %v", err) + } + } + + return nil +} + +func (r *requestPickupRepository) CreateRequestPickupItem(item *model.RequestPickupItem) error { + if err := r.DB.Create(item).Error; err != nil { + return fmt.Errorf("failed to create request pickup item: %v", err) + } + return nil +} + +func (r *requestPickupRepository) FindRequestPickupByID(id string) (*model.RequestPickup, error) { + var request model.RequestPickup + err := r.DB.Preload("RequestItems").First(&request, "id = ?", id).Error + if err != nil { + return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) + } + return &request, nil +} + +func (r *requestPickupRepository) FindAllRequestPickups() ([]model.RequestPickup, error) { + var requests []model.RequestPickup + err := r.DB.Preload("RequestItems").Find(&requests).Error + if err != nil { + return nil, fmt.Errorf("failed to fetch all request pickups: %v", err) + } + return requests, nil +} + +func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model.RequestPickup) error { + err := r.DB.Model(&model.RequestPickup{}).Where("id = ?", id).Updates(request).Error + if err != nil { + return fmt.Errorf("failed to update request pickup: %v", err) + } + return nil +} + +func (r *requestPickupRepository) DeleteRequestPickup(id string) error { + + if err := r.DB.Where("request_pickup_id = ?", id).Delete(&model.RequestPickupItem{}).Error; err != nil { + return fmt.Errorf("failed to delete request pickup items: %v", err) + } + + err := r.DB.Delete(&model.RequestPickup{}, "id = ?", id).Error + if err != nil { + return fmt.Errorf("failed to delete request pickup: %v", err) + } + return nil +} + +func (r *requestPickupRepository) FindRequestPickupByAddressAndCategory(addressID string, trashCategoryID string) (*model.RequestPickup, error) { + var request model.RequestPickup + err := r.DB.Joins("JOIN request_pickup_items ON request_pickups.id = request_pickup_items.request_pickup_id"). + Where("request_pickups.address_id = ? AND request_pickup_items.trash_category_id = ?", addressID, trashCategoryID). + First(&request).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("error checking request pickup for address %s and category %s: %v", addressID, trashCategoryID, err) + } + return &request, nil +} + +func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) { + var request model.RequestPickup + err := r.DB.Where("user_id = ? AND status_pickup = ?", userId, status).First(&request).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to check existing request pickup: %v", err) + } + return &request, nil +} diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go new file mode 100644 index 0000000..86d4276 --- /dev/null +++ b/internal/services/requestpickup_service.go @@ -0,0 +1,181 @@ +package services + +import ( + "fmt" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" +) + +type RequestPickupService interface { + CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) + GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) + GetAllRequestPickups() ([]dto.ResponseRequestPickup, error) + UpdateRequestPickup(id string, request dto.RequestPickup) (*dto.ResponseRequestPickup, error) + DeleteRequestPickup(id string) error +} + +type requestPickupService struct { + repo repositories.RequestPickupRepository + repoAddress repositories.AddressRepository + repoTrash repositories.TrashRepository +} + +func NewRequestPickupService(repo repositories.RequestPickupRepository, + repoAddress repositories.AddressRepository, + repoTrash repositories.TrashRepository) RequestPickupService { + return &requestPickupService{repo: repo, repoAddress: repoAddress, repoTrash: repoTrash} +} + +func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) { + + errors, valid := request.ValidateRequestPickup() + if !valid { + return nil, fmt.Errorf("validation errors: %v", errors) + } + + findAddress, err := s.repoAddress.FindAddressByID(request.AddressID) + if err != nil { + return nil, fmt.Errorf("address with ID %s not found", request.AddressID) + } + + existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_pengepul") + if err != nil { + return nil, fmt.Errorf("error checking for existing request pickup: %v", err) + } + if existingRequest != nil { + return nil, fmt.Errorf("there is already a pending pickup request for address %s", request.AddressID) + } + + modelRequest := model.RequestPickup{ + UserId: UserId, + AddressId: findAddress.ID, + EvidenceImage: request.EvidenceImage, + } + + err = s.repo.CreateRequestPickup(&modelRequest) + if err != nil { + return nil, fmt.Errorf("failed to create request pickup: %v", err) + } + + response := &dto.ResponseRequestPickup{ + ID: modelRequest.ID, + UserId: UserId, + AddressID: modelRequest.AddressId, + EvidenceImage: modelRequest.EvidenceImage, + StatusPickup: modelRequest.StatusPickup, + CreatedAt: modelRequest.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: modelRequest.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + for _, item := range request.RequestItems { + + findTrashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryID) + if err != nil { + return nil, fmt.Errorf("trash category with ID %s not found", item.TrashCategoryID) + } + + modelItem := model.RequestPickupItem{ + RequestPickupId: modelRequest.ID, + TrashCategoryId: findTrashCategory.ID, + EstimatedAmount: item.EstimatedAmount, + } + err = s.repo.CreateRequestPickupItem(&modelItem) + if err != nil { + return nil, fmt.Errorf("failed to create request pickup item: %v", err) + } + + response.RequestItems = append(response.RequestItems, dto.ResponseRequestPickupItem{ + ID: modelItem.ID, + TrashCategoryName: findTrashCategory.Name, + EstimatedAmount: modelItem.EstimatedAmount, + }) + } + + return response, nil +} + +func (s *requestPickupService) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) { + + request, err := s.repo.FindRequestPickupByID(id) + if err != nil { + return nil, fmt.Errorf("error fetching request pickup with ID %s: %v", id, err) + } + + response := &dto.ResponseRequestPickup{ + ID: request.ID, + UserId: request.UserId, + AddressID: request.AddressId, + EvidenceImage: request.EvidenceImage, + StatusPickup: request.StatusPickup, + CreatedAt: request.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: request.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return response, nil +} + +func (s *requestPickupService) GetAllRequestPickups() ([]dto.ResponseRequestPickup, error) { + + requests, err := s.repo.FindAllRequestPickups() + if err != nil { + return nil, fmt.Errorf("error fetching all request pickups: %v", err) + } + + var response []dto.ResponseRequestPickup + for _, request := range requests { + response = append(response, dto.ResponseRequestPickup{ + ID: request.ID, + UserId: request.UserId, + AddressID: request.AddressId, + EvidenceImage: request.EvidenceImage, + StatusPickup: request.StatusPickup, + CreatedAt: request.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: request.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return response, nil +} + +func (s *requestPickupService) UpdateRequestPickup(id string, request dto.RequestPickup) (*dto.ResponseRequestPickup, error) { + + errors, valid := request.ValidateRequestPickup() + if !valid { + return nil, fmt.Errorf("validation errors: %v", errors) + } + + existingRequest, err := s.repo.FindRequestPickupByID(id) + if err != nil { + return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) + } + + existingRequest.EvidenceImage = request.EvidenceImage + + err = s.repo.UpdateRequestPickup(id, existingRequest) + if err != nil { + return nil, fmt.Errorf("failed to update request pickup: %v", err) + } + + response := &dto.ResponseRequestPickup{ + ID: existingRequest.ID, + UserId: existingRequest.UserId, + AddressID: existingRequest.AddressId, + EvidenceImage: existingRequest.EvidenceImage, + StatusPickup: existingRequest.StatusPickup, + CreatedAt: existingRequest.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: existingRequest.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + return response, nil +} + +func (s *requestPickupService) DeleteRequestPickup(id string) error { + + err := s.repo.DeleteRequestPickup(id) + if err != nil { + return fmt.Errorf("failed to delete request pickup with ID %s: %v", id, err) + } + + return nil +} diff --git a/model/requestpickup_model.go b/model/requestpickup_model.go new file mode 100644 index 0000000..584cbb2 --- /dev/null +++ b/model/requestpickup_model.go @@ -0,0 +1,24 @@ +package model + +import ( + "time" +) + +type RequestPickup struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserId string `gorm:"not null" json:"user_id"` + AddressId string `gorm:"not null" json:"address_id"` + RequestItems []RequestPickupItem `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;" json:"request_items"` + EvidenceImage string `json:"evidence_image"` + StatusPickup string `gorm:"default:'waiting_pengepul'" json:"status_pickup"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` +} + +type RequestPickupItem struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RequestPickupId string `gorm:"not null" json:"request_pickup_id"` + TrashCategoryId string `gorm:"not null" json:"trash_category_id"` + TrashDetailId string `json:"trash_detail_id,omitempty"` + EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` +} diff --git a/presentation/requestpickup_route.go b/presentation/requestpickup_route.go new file mode 100644 index 0000000..6c63262 --- /dev/null +++ b/presentation/requestpickup_route.go @@ -0,0 +1,31 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func RequestPickupRouter(api fiber.Router) { + + requestRepo := repositories.NewRequestPickupRepository(config.DB) + repoTrash := repositories.NewTrashRepository(config.DB) + repoAddress := repositories.NewAddressRepository(config.DB) + + requestPickupServices := services.NewRequestPickupService(requestRepo, repoAddress, repoTrash) + + requestPickupHandler := handler.NewRequestPickupHandler(requestPickupServices) + + requestPickupAPI := api.Group("/requestpickup") + requestPickupAPI.Use(middleware.AuthMiddleware) + + requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) + // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) + // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) + // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) + // requestPickupAPI.Delete("/:id", requestPickupHandler.DeleteRequestPickup) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index f313766..8b3a343 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -25,6 +25,7 @@ func SetupRoutes(app *fiber.App) { // || auth router || // presentation.IdentityCardRouter(api) presentation.CompanyProfileRouter(api) + presentation.RequestPickupRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api) From 47ccae326a1c9e8591e11d7e994d6307d3784167 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 8 May 2025 12:33:25 +0700 Subject: [PATCH 24/48] feat&refact: add haversine algoritm and refact filed schema in address --- dto/address_dto.go | 13 ++++++++---- go.mod | 1 + go.sum | 2 ++ internal/services/address_service.go | 30 ++++++++++++++++++---------- model/address_model.go | 3 ++- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/dto/address_dto.go b/dto/address_dto.go index bcf5400..f33016e 100644 --- a/dto/address_dto.go +++ b/dto/address_dto.go @@ -11,7 +11,8 @@ type AddressResponseDTO struct { Village string `json:"village"` PostalCode string `json:"postalCode"` Detail string `json:"detail"` - Geography string `json:"geography"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` } @@ -23,7 +24,8 @@ type CreateAddressDTO struct { Village string `json:"village_id"` PostalCode string `json:"postalCode"` Detail string `json:"detail"` - Geography string `json:"geography"` + Latitude string `json:"latitude"` + Longitude string `json:"longitude"` } func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { @@ -49,8 +51,11 @@ func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { if strings.TrimSpace(r.Detail) == "" { errors["detail"] = append(errors["detail"], "Detail address is required") } - if strings.TrimSpace(r.Geography) == "" { - errors["geography"] = append(errors["geography"], "Geographic coordinates are required") + if strings.TrimSpace(r.Latitude) == "" { + errors["latitude"] = append(errors["latitude"], "Geographic coordinates are required") + } + if strings.TrimSpace(r.Longitude) == "" { + errors["longitude"] = append(errors["longitude"], "Geographic coordinates are required") } if len(errors) > 0 { diff --git a/go.mod b/go.mod index 687dde0..86771ff 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( ) require ( + github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 // indirect golang.org/x/term v0.30.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 83b4326..1b59849 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w= +github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= diff --git a/internal/services/address_service.go b/internal/services/address_service.go index 0f73781..2fa6315 100644 --- a/internal/services/address_service.go +++ b/internal/services/address_service.go @@ -60,7 +60,8 @@ func (s *addressService) CreateAddress(userID string, addressDTO dto.CreateAddre Village: village.Name, PostalCode: addressDTO.PostalCode, Detail: addressDTO.Detail, - Geography: addressDTO.Geography, + Latitude: addressDTO.Latitude, + Longitude: addressDTO.Longitude, } err = s.AddressRepo.CreateAddress(&address) @@ -83,7 +84,8 @@ func (s *addressService) CreateAddress(userID string, addressDTO dto.CreateAddre Village: address.Village, PostalCode: address.PostalCode, Detail: address.Detail, - Geography: address.Geography, + Latitude: address.Latitude, + Longitude: address.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -116,7 +118,8 @@ func (s *addressService) CreateAddress(userID string, addressDTO dto.CreateAddre Village: addr.Village, PostalCode: addr.PostalCode, Detail: addr.Detail, - Geography: addr.Geography, + Latitude: addr.Latitude, + Longitude: addr.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, }) @@ -152,7 +155,8 @@ func (s *addressService) GetAddressByUserID(userID string) ([]dto.AddressRespons Village: addressData["village"].(string), PostalCode: addressData["postalCode"].(string), Detail: addressData["detail"].(string), - Geography: addressData["geography"].(string), + Latitude: addressData["latitude"].(string), + Longitude: addressData["longitude"].(string), CreatedAt: addressData["createdAt"].(string), UpdatedAt: addressData["updatedAt"].(string), }) @@ -181,7 +185,8 @@ func (s *addressService) GetAddressByUserID(userID string) ([]dto.AddressRespons Village: address.Village, PostalCode: address.PostalCode, Detail: address.Detail, - Geography: address.Geography, + Latitude: address.Latitude, + Longitude: address.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, }) @@ -222,7 +227,8 @@ func (s *addressService) GetAddressByID(userID, id string) (*dto.AddressResponse Village: addressData["village"].(string), PostalCode: addressData["postalCode"].(string), Detail: addressData["detail"].(string), - Geography: addressData["geography"].(string), + Latitude: addressData["latitude"].(string), + Longitude: addressData["longitude"].(string), CreatedAt: addressData["createdAt"].(string), UpdatedAt: addressData["updatedAt"].(string), } @@ -242,7 +248,8 @@ func (s *addressService) GetAddressByID(userID, id string) (*dto.AddressResponse Village: address.Village, PostalCode: address.PostalCode, Detail: address.Detail, - Geography: address.Geography, + Latitude: address.Latitude, + Longitude: address.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -295,7 +302,8 @@ func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateA address.Village = village.Name address.PostalCode = addressDTO.PostalCode address.Detail = addressDTO.Detail - address.Geography = addressDTO.Geography + address.Latitude = addressDTO.Latitude + address.Longitude = addressDTO.Longitude address.UpdatedAt = time.Now() err = s.AddressRepo.UpdateAddress(address) @@ -321,7 +329,8 @@ func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateA Village: address.Village, PostalCode: address.PostalCode, Detail: address.Detail, - Geography: address.Geography, + Latitude: address.Latitude, + Longitude: address.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, } @@ -353,7 +362,8 @@ func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateA Village: addr.Village, PostalCode: addr.PostalCode, Detail: addr.Detail, - Geography: addr.Geography, + Latitude: addr.Latitude, + Longitude: addr.Longitude, CreatedAt: createdAt, UpdatedAt: updatedAt, }) diff --git a/model/address_model.go b/model/address_model.go index 1da69ec..a381737 100644 --- a/model/address_model.go +++ b/model/address_model.go @@ -12,7 +12,8 @@ type Address struct { Village string `gorm:"not null" json:"village"` PostalCode string `gorm:"not null" json:"postalCode"` Detail string `gorm:"not null" json:"detail"` - Geography string `gorm:"not null" json:"geography"` + Latitude string `gorm:"not null" json:"latitude"` + Longitude string `gorm:"not null" json:"longitude"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } From 61dda3f4eb669fddbbd307ffb7495070f8d8d1c9 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Tue, 13 May 2025 02:08:35 +0700 Subject: [PATCH 25/48] feat&fix: fixing debug and add confirmed feature for request pickup --- config/database.go | 1 + dto/address_dto.go | 59 +++++---- dto/collector_dto.go | 31 +++++ dto/requestpickup_dto.go | 21 ++-- go.mod | 1 - go.sum | 2 - internal/handler/collector_handler.go | 36 ++++++ internal/handler/requestpickup_handler.go | 67 ++++------- internal/repositories/collector_repo.go | 105 ++++++++++++++++ internal/repositories/requestpickup_repo.go | 68 +++++++---- internal/services/address_service.go | 8 +- .../services/auth/auth_pengepul_service.go | 7 +- internal/services/collector_service.go | 108 +++++++++++++++++ internal/services/requestpickup_service.go | 112 +++++++++++------- model/address_model.go | 4 +- model/collector_model.go | 11 ++ model/requestpickup_model.go | 32 +++-- presentation/collector_route.go | 26 ++++ presentation/requestpickup_route.go | 6 + router/setup_routes.go.go | 1 + utils/havershine.go | 39 ++++++ 21 files changed, 575 insertions(+), 170 deletions(-) create mode 100644 dto/collector_dto.go create mode 100644 internal/handler/collector_handler.go create mode 100644 internal/repositories/collector_repo.go create mode 100644 internal/services/collector_service.go create mode 100644 model/collector_model.go create mode 100644 presentation/collector_route.go create mode 100644 utils/havershine.go diff --git a/config/database.go b/config/database.go index 9bb56e9..e9e920c 100644 --- a/config/database.go +++ b/config/database.go @@ -42,6 +42,7 @@ func ConnectDatabase() { // ==============main feature============== // =>user preparation<= &model.User{}, + &model.Collector{}, &model.Role{}, &model.UserPin{}, &model.Address{}, diff --git a/dto/address_dto.go b/dto/address_dto.go index f33016e..b689ad5 100644 --- a/dto/address_dto.go +++ b/dto/address_dto.go @@ -3,29 +3,29 @@ package dto import "strings" type AddressResponseDTO struct { - UserID string `json:"user_id"` - ID string `json:"address_id"` - Province string `json:"province"` - Regency string `json:"regency"` - District string `json:"district"` - Village string `json:"village"` - PostalCode string `json:"postalCode"` - Detail string `json:"detail"` - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + UserID string `json:"user_id"` + ID string `json:"address_id"` + Province string `json:"province"` + Regency string `json:"regency"` + District string `json:"district"` + Village string `json:"village"` + PostalCode string `json:"postalCode"` + Detail string `json:"detail"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } type CreateAddressDTO struct { - Province string `json:"province_id"` - Regency string `json:"regency_id"` - District string `json:"district_id"` - Village string `json:"village_id"` - PostalCode string `json:"postalCode"` - Detail string `json:"detail"` - Latitude string `json:"latitude"` - Longitude string `json:"longitude"` + Province string `json:"province_id"` + Regency string `json:"regency_id"` + District string `json:"district_id"` + Village string `json:"village_id"` + PostalCode string `json:"postalCode"` + Detail string `json:"detail"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` } func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { @@ -34,28 +34,35 @@ func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { if strings.TrimSpace(r.Province) == "" { errors["province_id"] = append(errors["province_id"], "Province ID is required") } + if strings.TrimSpace(r.Regency) == "" { errors["regency_id"] = append(errors["regency_id"], "Regency ID is required") } + if strings.TrimSpace(r.District) == "" { errors["district_id"] = append(errors["district_id"], "District ID is required") } + if strings.TrimSpace(r.Village) == "" { errors["village_id"] = append(errors["village_id"], "Village ID is required") } + if strings.TrimSpace(r.PostalCode) == "" { - errors["postalCode"] = append(errors["village_id"], "PostalCode ID is required") + errors["postalCode"] = append(errors["postalCode"], "PostalCode is required") } else if len(r.PostalCode) < 5 { - errors["postalCode"] = append(errors["postalCode"], "kode pos belum sesuai") + errors["postalCode"] = append(errors["postalCode"], "PostalCode must be at least 5 characters") } + if strings.TrimSpace(r.Detail) == "" { errors["detail"] = append(errors["detail"], "Detail address is required") } - if strings.TrimSpace(r.Latitude) == "" { - errors["latitude"] = append(errors["latitude"], "Geographic coordinates are required") + + if r.Latitude == 0 { + errors["latitude"] = append(errors["latitude"], "Latitude is required") } - if strings.TrimSpace(r.Longitude) == "" { - errors["longitude"] = append(errors["longitude"], "Geographic coordinates are required") + + if r.Longitude == 0 { + errors["longitude"] = append(errors["longitude"], "Longitude is required") } if len(errors) > 0 { diff --git a/dto/collector_dto.go b/dto/collector_dto.go new file mode 100644 index 0000000..c1f96fb --- /dev/null +++ b/dto/collector_dto.go @@ -0,0 +1,31 @@ +package dto + +import "strings" + +type RequestCollectorDTO struct { + UserId string `json:"user_id"` + AddressId string `json:"address_id"` +} + +type ResponseCollectorDTO struct { + ID string `json:"collector_id"` + UserId string `json:"user_id"` + AddressId string `json:"address_id"` + JobStatus string `json:"job_status"` + Rating float32 `json:"rating"` + // CreatedAt string `json:"createdAt"` + // UpdatedAt string `json:"updatedAt"` +} + +func (r *RequestCollectorDTO) ValidateRequestColector() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.AddressId) == "" { + errors["address_id"] = append(errors["address_id"], "address_id harus diisi") + } + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/dto/requestpickup_dto.go b/dto/requestpickup_dto.go index 73764bf..643f95c 100644 --- a/dto/requestpickup_dto.go +++ b/dto/requestpickup_dto.go @@ -9,6 +9,7 @@ type RequestPickup struct { RequestItems []RequestPickupItem `json:"request_items"` EvidenceImage string `json:"evidence_image"` AddressID string `json:"address_id"` + RequestMethod string `json:"request_method"` } type RequestPickupItem struct { @@ -17,18 +18,20 @@ type RequestPickupItem struct { } type ResponseRequestPickup struct { - ID string `json:"id"` - UserId string `json:"user_id"` - AddressID string `json:"address_id"` - EvidenceImage string `json:"evidence_image"` - StatusPickup string `json:"status_pickup"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - RequestItems []ResponseRequestPickupItem `json:"request_items"` + ID string `json:"id,omitempty"` + UserId string `json:"user_id,omitempty"` + AddressID string `json:"address_id,omitempty"` + EvidenceImage string `json:"evidence_image,omitempty"` + StatusPickup string `json:"status_pickup,omitempty"` + CollectorID string `json:"collectorid,omitempty"` + ConfirmedByCollectorAt string `json:"confirmedat,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + RequestItems []ResponseRequestPickupItem `json:"request_items,omitempty"` } type ResponseRequestPickupItem struct { - ID string `json:"id"` + ID string `json:"id"` // TrashCategoryID string `json:"trash_category_id"` TrashCategoryName string `json:"trash_category_name"` EstimatedAmount float64 `json:"estimated_amount"` diff --git a/go.mod b/go.mod index 86771ff..687dde0 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( ) require ( - github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 // indirect golang.org/x/term v0.30.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum index 1b59849..83b4326 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w= -github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= diff --git a/internal/handler/collector_handler.go b/internal/handler/collector_handler.go new file mode 100644 index 0000000..cb2d738 --- /dev/null +++ b/internal/handler/collector_handler.go @@ -0,0 +1,36 @@ +package handler + +import ( + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CollectorHandler struct { + service services.CollectorService +} + +func NewCollectorHandler(service services.CollectorService) *CollectorHandler { + return &CollectorHandler{service} +} + +func (h *CollectorHandler) ConfirmRequestPickup(c *fiber.Ctx) error { + + collectorId, ok := c.Locals("userID").(string) + if !ok || collectorId == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + requestPickupId := c.Params("id") + if requestPickupId == "" { + return utils.ErrorResponse(c, "RequestPickup ID is required") + } + + req, err := h.service.ConfirmRequestPickup(requestPickupId, collectorId) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, req, "Request pickup confirmed successfully") +} diff --git a/internal/handler/requestpickup_handler.go b/internal/handler/requestpickup_handler.go index 4ce19fe..7558093 100644 --- a/internal/handler/requestpickup_handler.go +++ b/internal/handler/requestpickup_handler.go @@ -53,49 +53,32 @@ func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") } -func (h *RequestPickupHandler) GetAllRequestPickups(c *fiber.Ctx) error { +// func (h *RequestPickupHandler) GetAutomaticRequestByUser(c *fiber.Ctx) error { - response, err := h.service.GetAllRequestPickups() +// collectorId, ok := c.Locals("userID").(string) +// if !ok || collectorId == "" { +// return utils.ErrorResponse(c, "Unauthorized: User session not found") +// } + +// requestPickups, err := h.service.GetAllAutomaticRequestPickup(collectorId) +// if err != nil { + +// return utils.ErrorResponse(c, err.Error()) +// } + +// return utils.SuccessResponse(c, requestPickups, "Request pickups fetched successfully") +// } + +func (h *RequestPickupHandler) GetRequestPickups(c *fiber.Ctx) error { + // Get userID from Locals + collectorId := c.Locals("userID").(string) + + // Call service layer to get the request pickups + requests, err := h.service.GetRequestPickupsForCollector(collectorId) if err != nil { - return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error fetching all request pickups: %v", err)) + return utils.ErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, response, "All request pickups retrieved successfully") -} - -func (h *RequestPickupHandler) UpdateRequestPickup(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - id := c.Params("id") - var request dto.RequestPickup - - if err := c.BodyParser(&request); err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") - } - - errors, valid := request.ValidateRequestPickup() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - response, err := h.service.UpdateRequestPickup(id, request) - if err != nil { - return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error updating request pickup: %v", err)) - } - - return utils.SuccessResponse(c, response, "Request pickup updated successfully") -} - -func (h *RequestPickupHandler) DeleteRequestPickup(c *fiber.Ctx) error { - id := c.Params("id") - - err := h.service.DeleteRequestPickup(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Request pickup deleted successfully") -} + // Return response + return utils.SuccessResponse(c, requests, "Automatic request pickups retrieved successfully") +} \ No newline at end of file diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go new file mode 100644 index 0000000..0062610 --- /dev/null +++ b/internal/repositories/collector_repo.go @@ -0,0 +1,105 @@ +package repositories + +import ( + "errors" + "fmt" + "log" + "rijig/model" + "rijig/utils" + + "gorm.io/gorm" +) + +type CollectorRepository interface { + FindActiveCollectors() ([]model.Collector, error) + FindCollectorById(collector_id string) (*model.Collector, error) + CreateCollector(collector *model.Collector) error + UpdateCollector(userId string, jobStatus string) (*model.Collector, error) + FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) +} + +type collectorRepository struct { + DB *gorm.DB +} + +func NewCollectorRepository(db *gorm.DB) CollectorRepository { + return &collectorRepository{DB: db} +} + +func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) { + var collectors []model.Collector + + err := r.DB.Where("job_status = ?", "active").First(&collectors).Error + if err != nil { + return nil, fmt.Errorf("failed to fetch active collectors: %v", err) + } + + return collectors, nil +} + +func (r *collectorRepository) FindCollectorById(collector_id string) (*model.Collector, error) { + var collector model.Collector + err := r.DB.Where("user_id = ?", collector_id).First(&collector).Error + if err != nil { + return nil, fmt.Errorf("error fetching collector: %v", err) + } + fmt.Printf("menampilkan data collector %v", &collector) + return &collector, nil +} + +func (r *collectorRepository) CreateCollector(collector *model.Collector) error { + if err := r.DB.Create(collector).Error; err != nil { + return fmt.Errorf("failed to create collector: %v", err) + } + return nil +} + +func (r *collectorRepository) UpdateCollector(userId string, jobStatus string) (*model.Collector, error) { + var existingCollector model.Collector + + if err := r.DB.Where("user_id = ?", userId).First(&existingCollector).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("collector dengan user_id %s tidak ditemukan", userId) + } + log.Printf("Gagal mencari collector: %v", err) + return nil, fmt.Errorf("gagal fetching collector: %w", err) + } + + if jobStatus != "active" && jobStatus != "nonactive" { + return nil, fmt.Errorf("invalid job status: %v", jobStatus) + } + + if err := r.DB.Model(&existingCollector).Update("jobstatus", jobStatus).Error; err != nil { + log.Printf("Gagal mengupdate data collector: %v", err) + return nil, fmt.Errorf("gagal mengupdate job status untuk collector: %w", err) + } + + return &existingCollector, nil +} + +// #====experimen====# +func (r *collectorRepository) FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) { + var requests []model.RequestPickup + + err := r.DB.Preload("RequestItems"). + Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup). + Find(&requests).Error + if err != nil { + return nil, fmt.Errorf("error fetching request pickups with request_method '%s' and status '%s': %v", requestMethod, statuspickup, err) + } + + var nearbyRequests []model.RequestPickup + for _, request := range requests { + address := request.Address + + requestCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} + collectorCoord := utils.Coord{Lat: collectorLat, Lon: collectorLon} + _, km := utils.Distance(requestCoord, collectorCoord) + + if km <= maxDistance { + nearbyRequests = append(nearbyRequests, request) + } + } + + return nearbyRequests, nil +} diff --git a/internal/repositories/requestpickup_repo.go b/internal/repositories/requestpickup_repo.go index 912c83e..0eadea7 100644 --- a/internal/repositories/requestpickup_repo.go +++ b/internal/repositories/requestpickup_repo.go @@ -11,11 +11,13 @@ type RequestPickupRepository interface { CreateRequestPickup(request *model.RequestPickup) error CreateRequestPickupItem(item *model.RequestPickupItem) error FindRequestPickupByID(id string) (*model.RequestPickup, error) - FindAllRequestPickups() ([]model.RequestPickup, error) + FindAllRequestPickups(userId string) ([]model.RequestPickup, error) + FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) + FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) + GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) + GetAutomaticRequestPickupsForCollector(collectorId string) ([]model.RequestPickup, error) UpdateRequestPickup(id string, request *model.RequestPickup) error DeleteRequestPickup(id string) error - FindRequestPickupByAddressAndCategory(addressID string, trashCategoryID string) (*model.RequestPickup, error) - FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) } type requestPickupRepository struct { @@ -57,20 +59,43 @@ func (r *requestPickupRepository) FindRequestPickupByID(id string) (*model.Reque return &request, nil } -func (r *requestPickupRepository) FindAllRequestPickups() ([]model.RequestPickup, error) { +func (r *requestPickupRepository) FindAllRequestPickups(userId string) ([]model.RequestPickup, error) { var requests []model.RequestPickup - err := r.DB.Preload("RequestItems").Find(&requests).Error + err := r.DB.Preload("RequestItems").Where("user_id = ?", userId).Find(&requests).Error if err != nil { return nil, fmt.Errorf("failed to fetch all request pickups: %v", err) } return requests, nil } +func (r *requestPickupRepository) FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) { + var requests []model.RequestPickup + err := r.DB.Preload("RequestItems").Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup).Find(&requests).Error + if err != nil { + return nil, fmt.Errorf("error fetching request pickups with request_method %s: %v", requestMethod, err) + } + + return requests, nil +} + +func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) { + var request model.RequestPickup + err := r.DB.Where("user_id = ? AND status_pickup = ?", userId, status).First(&request).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to check existing request pickup: %v", err) + } + return &request, nil +} + func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model.RequestPickup) error { err := r.DB.Model(&model.RequestPickup{}).Where("id = ?", id).Updates(request).Error if err != nil { return fmt.Errorf("failed to update request pickup: %v", err) } + return nil } @@ -87,28 +112,25 @@ func (r *requestPickupRepository) DeleteRequestPickup(id string) error { return nil } -func (r *requestPickupRepository) FindRequestPickupByAddressAndCategory(addressID string, trashCategoryID string) (*model.RequestPickup, error) { - var request model.RequestPickup - err := r.DB.Joins("JOIN request_pickup_items ON request_pickups.id = request_pickup_items.request_pickup_id"). - Where("request_pickups.address_id = ? AND request_pickup_items.trash_category_id = ?", addressID, trashCategoryID). - First(&request).Error +func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector(collectorId string) ([]model.RequestPickup, error) { + var requests []model.RequestPickup + + err := r.DB.Preload("Address"). + Where("request_method = ? AND status_pickup = ?", "otomatis", "waiting_collector"). + Find(&requests).Error if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, fmt.Errorf("error checking request pickup for address %s and category %s: %v", addressID, trashCategoryID, err) + return nil, fmt.Errorf("error fetching pickup requests: %v", err) } - return &request, nil + + return requests, nil } -func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) { - var request model.RequestPickup - err := r.DB.Where("user_id = ? AND status_pickup = ?", userId, status).First(&request).Error +func (r *requestPickupRepository) GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) { + var items []model.RequestPickupItem + + err := r.DB.Preload("TrashCategory").Where("request_pickup_id = ?", requestPickupId).Find(&items).Error if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, fmt.Errorf("failed to check existing request pickup: %v", err) + return nil, fmt.Errorf("error fetching request pickup items: %v", err) } - return &request, nil + return items, nil } diff --git a/internal/services/address_service.go b/internal/services/address_service.go index 2fa6315..899f699 100644 --- a/internal/services/address_service.go +++ b/internal/services/address_service.go @@ -155,8 +155,8 @@ func (s *addressService) GetAddressByUserID(userID string) ([]dto.AddressRespons Village: addressData["village"].(string), PostalCode: addressData["postalCode"].(string), Detail: addressData["detail"].(string), - Latitude: addressData["latitude"].(string), - Longitude: addressData["longitude"].(string), + Latitude: addressData["latitude"].(float64), + Longitude: addressData["longitude"].(float64), CreatedAt: addressData["createdAt"].(string), UpdatedAt: addressData["updatedAt"].(string), }) @@ -227,8 +227,8 @@ func (s *addressService) GetAddressByID(userID, id string) (*dto.AddressResponse Village: addressData["village"].(string), PostalCode: addressData["postalCode"].(string), Detail: addressData["detail"].(string), - Latitude: addressData["latitude"].(string), - Longitude: addressData["longitude"].(string), + Latitude: addressData["latitude"].(float64), + Longitude: addressData["longitude"].(float64), CreatedAt: addressData["createdAt"].(string), UpdatedAt: addressData["updatedAt"].(string), } diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go index 0fb0f3c..3ce5246 100644 --- a/internal/services/auth/auth_pengepul_service.go +++ b/internal/services/auth/auth_pengepul_service.go @@ -67,9 +67,10 @@ func (s *authPengepulService) checkOTPRequestCooldown(phone string) error { func (s *authPengepulService) sendOTP(phone string) error { otp := generateOTP() - if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - return err - } + fmt.Printf("ur otp is:%s", otp) + // if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { + // return err + // } if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { return err diff --git a/internal/services/collector_service.go b/internal/services/collector_service.go new file mode 100644 index 0000000..5cc255f --- /dev/null +++ b/internal/services/collector_service.go @@ -0,0 +1,108 @@ +package services + +import ( + "fmt" + "rijig/dto" + "rijig/internal/repositories" + "rijig/utils" +) + +type CollectorService interface { + FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) + ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) +} + +type collectorService struct { + repo repositories.CollectorRepository + repoReq repositories.RequestPickupRepository + repoAddress repositories.AddressRepository +} + +func NewCollectorService(repo repositories.CollectorRepository, + repoReq repositories.RequestPickupRepository, + repoAddress repositories.AddressRepository) CollectorService { + return &collectorService{repo: repo, repoReq: repoReq, repoAddress: repoAddress} +} + +func (s *collectorService) FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) { + collectors, err := s.repo.FindActiveCollectors() + if err != nil { + return nil, fmt.Errorf("error fetching active collectors: %v", err) + } + + request, err := s.repoReq.FindRequestPickupByAddressAndStatus(userId, "waiting_collector") + if err != nil { + return nil, fmt.Errorf("gagal mendapatkan data request pickup dengan userid: %v", err) + } + + reqpickaddress, err := s.repoAddress.FindAddressByID(request.AddressId) + if err != nil { + return nil, fmt.Errorf("error fetching address for request pickup %s: %v", request.ID, err) + } + + var nearbyCollectorsResponse []dto.ResponseCollectorDTO + var maxDistance = 10.0 + + for _, collector := range collectors { + + address, err := s.repoAddress.FindAddressByID(collector.AddressId) + if err != nil { + return nil, fmt.Errorf("error fetching address for collector %s: %v", collector.ID, err) + } + + collectorCoord := utils.Coord{Lat: reqpickaddress.Latitude, Lon: reqpickaddress.Longitude} + userCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} + + _, km := utils.Distance(collectorCoord, userCoord) + + if km <= maxDistance { + + nearbyCollectorsResponse = append(nearbyCollectorsResponse, dto.ResponseCollectorDTO{ + ID: collector.ID, + AddressId: collector.User.Name, + Rating: collector.Rating, + }) + } + } + + if len(nearbyCollectorsResponse) == 0 { + return nil, fmt.Errorf("no request pickups found within %v km", maxDistance) + } + + return nearbyCollectorsResponse, nil +} + +func (s *collectorService) ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) { + + request, err := s.repoReq.FindRequestPickupByID(requestId) + if err != nil { + return nil, fmt.Errorf("request pickup not found: %v", err) + } + + if request.StatusPickup != "waiting_collector" { + return nil, fmt.Errorf("pickup request is not in 'waiting_collector' status") + } + + collector, err := s.repo.FindCollectorById(collectorId) + if err != nil { + return nil, fmt.Errorf("collector tidak ditemukan: %v", err) + } + + request.StatusPickup = "confirmed" + request.CollectorID = &collector.ID + + err = s.repoReq.UpdateRequestPickup(requestId, request) + if err != nil { + return nil, fmt.Errorf("failed to update request pickup: %v", err) + } + + confirmedAt, _ := utils.FormatDateToIndonesianFormat(request.ConfirmedByCollectorAt) + + response := dto.ResponseRequestPickup{ + StatusPickup: request.StatusPickup, + CollectorID: *request.CollectorID, + ConfirmedByCollectorAt: confirmedAt, + } + + return &response, nil +} diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go index 86d4276..ecd4514 100644 --- a/internal/services/requestpickup_service.go +++ b/internal/services/requestpickup_service.go @@ -5,18 +5,22 @@ import ( "rijig/dto" "rijig/internal/repositories" "rijig/model" + "rijig/utils" ) type RequestPickupService interface { CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) - GetAllRequestPickups() ([]dto.ResponseRequestPickup, error) - UpdateRequestPickup(id string, request dto.RequestPickup) (*dto.ResponseRequestPickup, error) - DeleteRequestPickup(id string) error + GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) + // GetAllAutomaticRequestPickups(collector_id string) ([]dto.ResponseRequestPickup, error) + // GetAllAutomaticRequestPickup(collectorId string) ([]dto.ResponseRequestPickup, error) + + GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) } type requestPickupService struct { repo repositories.RequestPickupRepository + repoReq repositories.CollectorRepository repoAddress repositories.AddressRepository repoTrash repositories.TrashRepository } @@ -34,12 +38,12 @@ func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, Us return nil, fmt.Errorf("validation errors: %v", errors) } - findAddress, err := s.repoAddress.FindAddressByID(request.AddressID) + _, err := s.repoAddress.FindAddressByID(request.AddressID) if err != nil { return nil, fmt.Errorf("address with ID %s not found", request.AddressID) } - existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_pengepul") + existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector") if err != nil { return nil, fmt.Errorf("error checking for existing request pickup: %v", err) } @@ -49,8 +53,9 @@ func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, Us modelRequest := model.RequestPickup{ UserId: UserId, - AddressId: findAddress.ID, + AddressId: request.AddressID, EvidenceImage: request.EvidenceImage, + RequestMethod: request.RequestMethod, } err = s.repo.CreateRequestPickup(&modelRequest) @@ -58,14 +63,17 @@ func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, Us return nil, fmt.Errorf("failed to create request pickup: %v", err) } + createdAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.UpdatedAt) + response := &dto.ResponseRequestPickup{ ID: modelRequest.ID, UserId: UserId, AddressID: modelRequest.AddressId, EvidenceImage: modelRequest.EvidenceImage, StatusPickup: modelRequest.StatusPickup, - CreatedAt: modelRequest.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: modelRequest.UpdatedAt.Format("2006-01-02 15:04:05"), + CreatedAt: createdAt, + UpdatedAt: updatedAt, } for _, item := range request.RequestItems { @@ -102,80 +110,94 @@ func (s *requestPickupService) GetRequestPickupByID(id string) (*dto.ResponseReq return nil, fmt.Errorf("error fetching request pickup with ID %s: %v", id, err) } + createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) + response := &dto.ResponseRequestPickup{ ID: request.ID, UserId: request.UserId, AddressID: request.AddressId, EvidenceImage: request.EvidenceImage, StatusPickup: request.StatusPickup, - CreatedAt: request.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: request.UpdatedAt.Format("2006-01-02 15:04:05"), + CreatedAt: createdAt, + UpdatedAt: updatedAt, } return response, nil } -func (s *requestPickupService) GetAllRequestPickups() ([]dto.ResponseRequestPickup, error) { +func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) { - requests, err := s.repo.FindAllRequestPickups() + requests, err := s.repo.FindAllRequestPickups(userid) if err != nil { return nil, fmt.Errorf("error fetching all request pickups: %v", err) } var response []dto.ResponseRequestPickup for _, request := range requests { + createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) response = append(response, dto.ResponseRequestPickup{ ID: request.ID, UserId: request.UserId, AddressID: request.AddressId, EvidenceImage: request.EvidenceImage, StatusPickup: request.StatusPickup, - CreatedAt: request.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: request.UpdatedAt.Format("2006-01-02 15:04:05"), + CreatedAt: createdAt, + UpdatedAt: updatedAt, }) } return response, nil } -func (s *requestPickupService) UpdateRequestPickup(id string, request dto.RequestPickup) (*dto.ResponseRequestPickup, error) { +func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { - errors, valid := request.ValidateRequestPickup() - if !valid { - return nil, fmt.Errorf("validation errors: %v", errors) - } - - existingRequest, err := s.repo.FindRequestPickupByID(id) + requests, err := s.repo.GetAutomaticRequestPickupsForCollector(collectorId) if err != nil { - return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) + return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) } - existingRequest.EvidenceImage = request.EvidenceImage + var response []dto.ResponseRequestPickup - err = s.repo.UpdateRequestPickup(id, existingRequest) - if err != nil { - return nil, fmt.Errorf("failed to update request pickup: %v", err) - } + for _, req := range requests { - response := &dto.ResponseRequestPickup{ - ID: existingRequest.ID, - UserId: existingRequest.UserId, - AddressID: existingRequest.AddressId, - EvidenceImage: existingRequest.EvidenceImage, - StatusPickup: existingRequest.StatusPickup, - CreatedAt: existingRequest.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: existingRequest.UpdatedAt.Format("2006-01-02 15:04:05"), + _, distance := utils.Distance( + utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, + utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, + ) + + if distance <= 20 { + + mappedRequest := dto.ResponseRequestPickup{ + ID: req.ID, + UserId: req.UserId, + AddressID: req.AddressId, + EvidenceImage: req.EvidenceImage, + StatusPickup: req.StatusPickup, + CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), + } + + requestItems, err := s.repo.GetRequestPickupItems(req.ID) + if err != nil { + return nil, fmt.Errorf("error fetching request items: %v", err) + } + + var mappedRequestItems []dto.ResponseRequestPickupItem + for _, item := range requestItems { + mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ + ID: item.ID, + TrashCategoryName: item.TrashCategory.Name, + EstimatedAmount: item.EstimatedAmount, + }) + } + + mappedRequest.RequestItems = mappedRequestItems + + response = append(response, mappedRequest) + } } return response, nil } - -func (s *requestPickupService) DeleteRequestPickup(id string) error { - - err := s.repo.DeleteRequestPickup(id) - if err != nil { - return fmt.Errorf("failed to delete request pickup with ID %s: %v", id, err) - } - - return nil -} diff --git a/model/address_model.go b/model/address_model.go index a381737..d5674d0 100644 --- a/model/address_model.go +++ b/model/address_model.go @@ -12,8 +12,8 @@ type Address struct { Village string `gorm:"not null" json:"village"` PostalCode string `gorm:"not null" json:"postalCode"` Detail string `gorm:"not null" json:"detail"` - Latitude string `gorm:"not null" json:"latitude"` - Longitude string `gorm:"not null" json:"longitude"` + Latitude float64 `gorm:"not null" json:"latitude"` + Longitude float64 `gorm:"not null" json:"longitude"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/collector_model.go b/model/collector_model.go new file mode 100644 index 0000000..ed024f2 --- /dev/null +++ b/model/collector_model.go @@ -0,0 +1,11 @@ +package model + +type Collector struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + JobStatus string `gorm:"default:nonactive" json:"jobstatus"` + Rating float32 `gorm:"default:5" json:"rating"` + AddressId string `gorm:"not null" json:"address_id"` + Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` +} diff --git a/model/requestpickup_model.go b/model/requestpickup_model.go index 584cbb2..a8ee3e8 100644 --- a/model/requestpickup_model.go +++ b/model/requestpickup_model.go @@ -5,20 +5,26 @@ import ( ) type RequestPickup struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - UserId string `gorm:"not null" json:"user_id"` - AddressId string `gorm:"not null" json:"address_id"` - RequestItems []RequestPickupItem `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;" json:"request_items"` - EvidenceImage string `json:"evidence_image"` - StatusPickup string `gorm:"default:'waiting_pengepul'" json:"status_pickup"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserId string `gorm:"not null" json:"user_id"` + User User `gorm:"foreignKey:UserId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + AddressId string `gorm:"not null" json:"address_id"` + Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` + RequestItems []RequestPickupItem `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;" json:"request_items"` + EvidenceImage string `json:"evidence_image"` + StatusPickup string `gorm:"default:'waiting_collector'" json:"status_pickup"` + CollectorID *string `gorm:"type:uuid" json:"collector_id,omitempty"` + ConfirmedByCollectorAt time.Time `gorm:"default:current_timestamp" json:"confirmed_by_collector_at,omitempty"` + RequestMethod string `gorm:"not null" json:"request_method"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` } type RequestPickupItem struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - RequestPickupId string `gorm:"not null" json:"request_pickup_id"` - TrashCategoryId string `gorm:"not null" json:"trash_category_id"` - TrashDetailId string `json:"trash_detail_id,omitempty"` - EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RequestPickupId string `gorm:"not null" json:"request_pickup_id"` + RequestPickup RequestPickup `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;"` + TrashCategoryId string `gorm:"not null" json:"trash_category_id"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryId;constraint:OnDelete:CASCADE;"` + EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` } diff --git a/presentation/collector_route.go b/presentation/collector_route.go new file mode 100644 index 0000000..451b3bc --- /dev/null +++ b/presentation/collector_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func CollectorRouter(api fiber.Router) { + repo := repositories.NewCollectorRepository(config.DB) + repoReq := repositories.NewRequestPickupRepository(config.DB) + repoAddress := repositories.NewAddressRepository(config.DB) + colectorService := services.NewCollectorService(repo, repoReq, repoAddress) + collectorHandler := handler.NewCollectorHandler(colectorService) + + collector := api.Group("/collector") + collector.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RolePengepul)) + + collector.Put("confirmrequest/:id", collectorHandler.ConfirmRequestPickup) + +} diff --git a/presentation/requestpickup_route.go b/presentation/requestpickup_route.go index 6c63262..e06e983 100644 --- a/presentation/requestpickup_route.go +++ b/presentation/requestpickup_route.go @@ -15,8 +15,12 @@ func RequestPickupRouter(api fiber.Router) { requestRepo := repositories.NewRequestPickupRepository(config.DB) repoTrash := repositories.NewTrashRepository(config.DB) repoAddress := repositories.NewAddressRepository(config.DB) + // collectorRepo := repositories.NewCollectorRepository(config.DB) requestPickupServices := services.NewRequestPickupService(requestRepo, repoAddress, repoTrash) + // collectorService := services.NewCollectorService(collectorRepo, requestRepo, repoAddress) + // service services.RequestPickupService, + // collectorService services.CollectorService requestPickupHandler := handler.NewRequestPickupHandler(requestPickupServices) @@ -24,6 +28,8 @@ func RequestPickupRouter(api fiber.Router) { requestPickupAPI.Use(middleware.AuthMiddleware) requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) + // requestPickupAPI.Get("/get", middleware.AuthMiddleware, requestPickupHandler.GetAutomaticRequestByUser) + requestPickupAPI.Get("/get-allrequest", requestPickupHandler.GetRequestPickups) // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 8b3a343..9b99496 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -26,6 +26,7 @@ func SetupRoutes(app *fiber.App) { presentation.IdentityCardRouter(api) presentation.CompanyProfileRouter(api) presentation.RequestPickupRouter(api) + presentation.CollectorRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api) diff --git a/utils/havershine.go b/utils/havershine.go new file mode 100644 index 0000000..9de461f --- /dev/null +++ b/utils/havershine.go @@ -0,0 +1,39 @@ +package utils + +import ( + "math" +) + +const ( + earthRadiusMi = 3958 + earthRaidusKm = 6371 +) + +type Coord struct { + Lat float64 + Lon float64 +} + +func degreesToRadians(d float64) float64 { + return d * math.Pi / 180 +} + +func Distance(p, q Coord) (mi, km float64) { + lat1 := degreesToRadians(p.Lat) + lon1 := degreesToRadians(p.Lon) + lat2 := degreesToRadians(q.Lat) + lon2 := degreesToRadians(q.Lon) + + diffLat := lat2 - lat1 + diffLon := lon2 - lon1 + + a := math.Pow(math.Sin(diffLat/2), 2) + math.Cos(lat1)*math.Cos(lat2)* + math.Pow(math.Sin(diffLon/2), 2) + + c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)) + + mi = c * earthRadiusMi + km = c * earthRaidusKm + + return mi, km +} From 2d035074a48882edf2a7b3ed457d785329bbcaaa Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 15 May 2025 09:11:49 +0700 Subject: [PATCH 26/48] fix: fixing and refact code statement and nil value --- dto/address_dto.go | 24 +-- dto/collector_dto.go | 31 +++- dto/requestpickup_dto.go | 17 +- dto/trash_dto.go | 10 +- dto/user_dto.go | 18 +- internal/handler/collector_handler.go | 34 ++++ internal/handler/requestpickup_handler.go | 48 +++--- internal/repositories/collector_repo.go | 58 ++++--- internal/repositories/requestpickup_repo.go | 59 ++++++- internal/services/address_service.go | 2 +- internal/services/collector_service.go | 115 +++++++++---- internal/services/requestpickup_service.go | 178 ++++++++++++++++++-- internal/services/trash_service.go | 2 + model/collector_model.go | 7 +- model/requestpickup_model.go | 19 ++- presentation/collector_route.go | 10 +- presentation/requestpickup_route.go | 12 +- presentation/trash_route.go | 19 ++- router/setup_routes.go.go | 4 +- 19 files changed, 506 insertions(+), 161 deletions(-) diff --git a/dto/address_dto.go b/dto/address_dto.go index b689ad5..b1bdcd6 100644 --- a/dto/address_dto.go +++ b/dto/address_dto.go @@ -3,18 +3,18 @@ package dto import "strings" type AddressResponseDTO struct { - UserID string `json:"user_id"` - ID string `json:"address_id"` - Province string `json:"province"` - Regency string `json:"regency"` - District string `json:"district"` - Village string `json:"village"` - PostalCode string `json:"postalCode"` - Detail string `json:"detail"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + UserID string `json:"user_id,omitempty"` + ID string `json:"address_id,omitempty"` + Province string `json:"province,omitempty"` + Regency string `json:"regency,omitempty"` + District string `json:"district,omitempty"` + Village string `json:"village,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + Detail string `json:"detail,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` } type CreateAddressDTO struct { diff --git a/dto/collector_dto.go b/dto/collector_dto.go index c1f96fb..21e89c5 100644 --- a/dto/collector_dto.go +++ b/dto/collector_dto.go @@ -7,14 +7,31 @@ type RequestCollectorDTO struct { AddressId string `json:"address_id"` } +type SelectCollectorRequest struct { + Collector_id string `json:"collector_id"` +} + +func (r *SelectCollectorRequest) ValidateSelectCollectorRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Collector_id) == "" { + errors["collector_id"] = append(errors["collector_id"], "collector_id harus diisi") + } + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + type ResponseCollectorDTO struct { - ID string `json:"collector_id"` - UserId string `json:"user_id"` - AddressId string `json:"address_id"` - JobStatus string `json:"job_status"` - Rating float32 `json:"rating"` - // CreatedAt string `json:"createdAt"` - // UpdatedAt string `json:"updatedAt"` + ID string `json:"collector_id"` + UserId string `json:"user_id"` + User []UserResponseDTO `json:"user,omitempty"` + AddressId string `json:"address_id"` + Address []AddressResponseDTO `json:"address,omitempty"` + JobStatus *string `json:"job_status,omitempty"` + Rating float32 `json:"rating"` } func (r *RequestCollectorDTO) ValidateRequestColector() (map[string][]string, bool) { diff --git a/dto/requestpickup_dto.go b/dto/requestpickup_dto.go index 643f95c..01e7ad7 100644 --- a/dto/requestpickup_dto.go +++ b/dto/requestpickup_dto.go @@ -6,10 +6,11 @@ import ( ) type RequestPickup struct { - RequestItems []RequestPickupItem `json:"request_items"` - EvidenceImage string `json:"evidence_image"` AddressID string `json:"address_id"` RequestMethod string `json:"request_method"` + EvidenceImage string `json:"evidence_image"` + Notes string `json:"notes"` + RequestItems []RequestPickupItem `json:"request_items"` } type RequestPickupItem struct { @@ -20,10 +21,14 @@ type RequestPickupItem struct { type ResponseRequestPickup struct { ID string `json:"id,omitempty"` UserId string `json:"user_id,omitempty"` + User []UserResponseDTO `json:"user,omitempty"` AddressID string `json:"address_id,omitempty"` + Address []AddressResponseDTO `json:"address,omitempty"` EvidenceImage string `json:"evidence_image,omitempty"` + Notes string `json:"notes,omitempty"` StatusPickup string `json:"status_pickup,omitempty"` CollectorID string `json:"collectorid,omitempty"` + Collector []ResponseCollectorDTO `json:"collector,omitempty"` ConfirmedByCollectorAt string `json:"confirmedat,omitempty"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` @@ -31,10 +36,10 @@ type ResponseRequestPickup struct { } type ResponseRequestPickupItem struct { - ID string `json:"id"` - // TrashCategoryID string `json:"trash_category_id"` - TrashCategoryName string `json:"trash_category_name"` - EstimatedAmount float64 `json:"estimated_amount"` + ID string `json:"id,omitempty"` + TrashCategoryID string `json:"trash_category_id,omitempty"` + TrashCategory []ResponseTrashCategoryDTO `json:"trash_category,omitempty"` + EstimatedAmount float64 `json:"estimated_amount,omitempty"` } func (r *RequestPickup) ValidateRequestPickup() (map[string][]string, bool) { diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 9df527f..092386d 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -8,11 +8,11 @@ type RequestTrashCategoryDTO struct { } type ResponseTrashCategoryDTO struct { - ID string `json:"id"` - Name string `json:"name"` - Icon string `json:"icon"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Icon string `json:"icon,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` Details []ResponseTrashDetailDTO `json:"details,omitempty"` } diff --git a/dto/user_dto.go b/dto/user_dto.go index 9558bd6..3a1164c 100644 --- a/dto/user_dto.go +++ b/dto/user_dto.go @@ -6,16 +6,16 @@ import ( ) type UserResponseDTO struct { - ID string `json:"id"` - Username string `json:"username"` + ID string `json:"id,omitempty"` + Username string `json:"username,omitempty"` Avatar *string `json:"photoprofile,omitempty"` - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` - EmailVerified bool `json:"emailVerified"` - RoleName string `json:"role"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` + Name string `json:"name,omitempty"` + Phone string `json:"phone,omitempty"` + Email string `json:"email,omitempty"` + EmailVerified bool `json:"emailVerified,omitempty"` + RoleName string `json:"role,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` } type RequestUserDTO struct { diff --git a/internal/handler/collector_handler.go b/internal/handler/collector_handler.go index cb2d738..355c8be 100644 --- a/internal/handler/collector_handler.go +++ b/internal/handler/collector_handler.go @@ -1,6 +1,8 @@ package handler import ( + "fmt" + "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -34,3 +36,35 @@ func (h *CollectorHandler) ConfirmRequestPickup(c *fiber.Ctx) error { return utils.SuccessResponse(c, req, "Request pickup confirmed successfully") } + +func (h *CollectorHandler) GetAvaibleCollector(c *fiber.Ctx) error { + + userId := c.Locals("userID").(string) + + requests, err := h.service.FindCollectorsNearby(userId) + if err != nil { + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, requests, "menampilkan data collector terdekat") +} + +func (h *CollectorHandler) ConfirmRequestManualPickup(c *fiber.Ctx) error { + userId := c.Locals("userID").(string) + requestId := c.Params("request_id") + if requestId == "" { + fmt.Println("requestid dibutuhkan") + } + + var request dto.SelectCollectorRequest + if err := c.BodyParser(&request); err != nil { + return fmt.Errorf("error parsing request body: %v", err) + } + + message, err := h.service.ConfirmRequestManualPickup(requestId, userId) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error confirming pickup: %v", err)) + } + + return utils.SuccessResponse(c, message, "berhasil konfirmasi request") +} diff --git a/internal/handler/requestpickup_handler.go b/internal/handler/requestpickup_handler.go index 7558093..0984cd2 100644 --- a/internal/handler/requestpickup_handler.go +++ b/internal/handler/requestpickup_handler.go @@ -53,32 +53,40 @@ func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") } -// func (h *RequestPickupHandler) GetAutomaticRequestByUser(c *fiber.Ctx) error { - -// collectorId, ok := c.Locals("userID").(string) -// if !ok || collectorId == "" { -// return utils.ErrorResponse(c, "Unauthorized: User session not found") -// } - -// requestPickups, err := h.service.GetAllAutomaticRequestPickup(collectorId) -// if err != nil { - -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, requestPickups, "Request pickups fetched successfully") -// } - func (h *RequestPickupHandler) GetRequestPickups(c *fiber.Ctx) error { - // Get userID from Locals + collectorId := c.Locals("userID").(string) - // Call service layer to get the request pickups requests, err := h.service.GetRequestPickupsForCollector(collectorId) if err != nil { return utils.ErrorResponse(c, err.Error()) } - // Return response return utils.SuccessResponse(c, requests, "Automatic request pickups retrieved successfully") -} \ No newline at end of file +} + +func (h *RequestPickupHandler) AssignCollectorToRequest(c *fiber.Ctx) error { + userId, ok := c.Locals("userID").(string) + if !ok || userId == "" { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") + } + + var request dto.SelectCollectorRequest + errors, valid := request.ValidateSelectCollectorRequest() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + if err := c.BodyParser(&request); err != nil { + return fmt.Errorf("error parsing request body: %v", err) + } + + err := h.service.SelectCollectorInRequest(userId, request.Collector_id) + if err != nil { + + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error assigning collector: %v", err)) + } + + return utils.GenericResponse(c, fiber.StatusOK, "berhasil memilih collector") +} + diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go index 0062610..5f0ef53 100644 --- a/internal/repositories/collector_repo.go +++ b/internal/repositories/collector_repo.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "rijig/model" - "rijig/utils" "gorm.io/gorm" ) @@ -13,9 +12,10 @@ import ( type CollectorRepository interface { FindActiveCollectors() ([]model.Collector, error) FindCollectorById(collector_id string) (*model.Collector, error) + FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) CreateCollector(collector *model.Collector) error UpdateCollector(userId string, jobStatus string) (*model.Collector, error) - FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) + // FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) } type collectorRepository struct { @@ -29,7 +29,7 @@ func NewCollectorRepository(db *gorm.DB) CollectorRepository { func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) { var collectors []model.Collector - err := r.DB.Where("job_status = ?", "active").First(&collectors).Error + err := r.DB.Preload("Address").Where("job_status = ?", "active").First(&collectors).Error if err != nil { return nil, fmt.Errorf("failed to fetch active collectors: %v", err) } @@ -38,6 +38,16 @@ func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) } func (r *collectorRepository) FindCollectorById(collector_id string) (*model.Collector, error) { + var collector model.Collector + err := r.DB.Preload("Address").Where("user_id = ?", collector_id).First(&collector).Error + if err != nil { + return nil, fmt.Errorf("error fetching collector: %v", err) + } + fmt.Printf("menampilkan data collector %v", &collector) + return &collector, nil +} + +func (r *collectorRepository) FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) { var collector model.Collector err := r.DB.Where("user_id = ?", collector_id).First(&collector).Error if err != nil { @@ -77,29 +87,29 @@ func (r *collectorRepository) UpdateCollector(userId string, jobStatus string) ( return &existingCollector, nil } -// #====experimen====# -func (r *collectorRepository) FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) { - var requests []model.RequestPickup +// // #====experimen====# +// func (r *collectorRepository) FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) { +// var requests []model.RequestPickup - err := r.DB.Preload("RequestItems"). - Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup). - Find(&requests).Error - if err != nil { - return nil, fmt.Errorf("error fetching request pickups with request_method '%s' and status '%s': %v", requestMethod, statuspickup, err) - } +// err := r.DB.Preload("RequestItems"). +// Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup). +// Find(&requests).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching request pickups with request_method '%s' and status '%s': %v", requestMethod, statuspickup, err) +// } - var nearbyRequests []model.RequestPickup - for _, request := range requests { - address := request.Address +// var nearbyRequests []model.RequestPickup +// for _, request := range requests { +// address := request.Address - requestCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} - collectorCoord := utils.Coord{Lat: collectorLat, Lon: collectorLon} - _, km := utils.Distance(requestCoord, collectorCoord) +// requestCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} +// collectorCoord := utils.Coord{Lat: collectorLat, Lon: collectorLon} +// _, km := utils.Distance(requestCoord, collectorCoord) - if km <= maxDistance { - nearbyRequests = append(nearbyRequests, request) - } - } +// if km <= maxDistance { +// nearbyRequests = append(nearbyRequests, request) +// } +// } - return nearbyRequests, nil -} +// return nearbyRequests, nil +// } diff --git a/internal/repositories/requestpickup_repo.go b/internal/repositories/requestpickup_repo.go index 0eadea7..76fd64e 100644 --- a/internal/repositories/requestpickup_repo.go +++ b/internal/repositories/requestpickup_repo.go @@ -13,9 +13,12 @@ type RequestPickupRepository interface { FindRequestPickupByID(id string) (*model.RequestPickup, error) FindAllRequestPickups(userId string) ([]model.RequestPickup, error) FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) - FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) + FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) + FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) - GetAutomaticRequestPickupsForCollector(collectorId string) ([]model.RequestPickup, error) + GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) + GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) + // SelectCollectorInRequest(userId string, collectorId string) error UpdateRequestPickup(id string, request *model.RequestPickup) error DeleteRequestPickup(id string) error } @@ -78,9 +81,21 @@ func (r *requestPickupRepository) FindAllAutomaticMethodRequest(requestMethod, s return requests, nil } -func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status string) (*model.RequestPickup, error) { +func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) { var request model.RequestPickup - err := r.DB.Where("user_id = ? AND status_pickup = ?", userId, status).First(&request).Error + err := r.DB.Preload("Address").Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to check existing request pickup: %v", err) + } + return &request, nil +} + +func (r *requestPickupRepository) FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) { + var request model.RequestPickup + err := r.DB.Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, nil @@ -99,6 +114,28 @@ func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model. return nil } +// func (r *requestPickupRepository) SelectCollectorInRequest(userId string, collectorId string) error { +// var request model.RequestPickup +// err := r.DB.Model(&model.RequestPickup{}). +// Where("user_id = ? AND status_pickup = ? AND request_method = ? AND collector_id IS NULL", userId, "waiting_collector", "manual"). +// First(&request).Error +// if err != nil { +// if err == gorm.ErrRecordNotFound { +// return fmt.Errorf("no matching request pickup found for user %s", userId) +// } +// return fmt.Errorf("failed to find request pickup: %v", err) +// } + +// err = r.DB.Model(&model.RequestPickup{}). +// Where("id = ?", request.ID). +// Update("collector_id", collectorId). +// Error +// if err != nil { +// return fmt.Errorf("failed to update collector_id: %v", err) +// } +// return nil +// } + func (r *requestPickupRepository) DeleteRequestPickup(id string) error { if err := r.DB.Where("request_pickup_id = ?", id).Delete(&model.RequestPickupItem{}).Error; err != nil { @@ -112,16 +149,24 @@ func (r *requestPickupRepository) DeleteRequestPickup(id string) error { return nil } -func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector(collectorId string) ([]model.RequestPickup, error) { +func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) { var requests []model.RequestPickup - err := r.DB.Preload("Address"). - Where("request_method = ? AND status_pickup = ?", "otomatis", "waiting_collector"). + Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", nil). Find(&requests).Error if err != nil { return nil, fmt.Errorf("error fetching pickup requests: %v", err) } + return requests, nil +} +func (r *requestPickupRepository) GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) { + var requests []model.RequestPickup + err := r.DB.Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", collector_id). + Find(&requests).Error + if err != nil { + return nil, fmt.Errorf("error fetching pickup requests: %v", err) + } return requests, nil } diff --git a/internal/services/address_service.go b/internal/services/address_service.go index 899f699..b6b25c8 100644 --- a/internal/services/address_service.go +++ b/internal/services/address_service.go @@ -304,7 +304,7 @@ func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateA address.Detail = addressDTO.Detail address.Latitude = addressDTO.Latitude address.Longitude = addressDTO.Longitude - address.UpdatedAt = time.Now() + // address.UpdatedAt = time.Now() err = s.AddressRepo.UpdateAddress(address) if err != nil { diff --git a/internal/services/collector_service.go b/internal/services/collector_service.go index 5cc255f..9cf745e 100644 --- a/internal/services/collector_service.go +++ b/internal/services/collector_service.go @@ -5,23 +5,27 @@ import ( "rijig/dto" "rijig/internal/repositories" "rijig/utils" + "time" ) type CollectorService interface { FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) + ConfirmRequestManualPickup(requestId, collectorId string) (any, error) } type collectorService struct { repo repositories.CollectorRepository - repoReq repositories.RequestPickupRepository + repoColl repositories.RequestPickupRepository repoAddress repositories.AddressRepository + repoUser repositories.UserProfilRepository } func NewCollectorService(repo repositories.CollectorRepository, - repoReq repositories.RequestPickupRepository, - repoAddress repositories.AddressRepository) CollectorService { - return &collectorService{repo: repo, repoReq: repoReq, repoAddress: repoAddress} + repoColl repositories.RequestPickupRepository, + repoAddress repositories.AddressRepository, + repoUser repositories.UserProfilRepository) CollectorService { + return &collectorService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoUser: repoUser} } func (s *collectorService) FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) { @@ -30,51 +34,62 @@ func (s *collectorService) FindCollectorsNearby(userId string) ([]dto.ResponseCo return nil, fmt.Errorf("error fetching active collectors: %v", err) } - request, err := s.repoReq.FindRequestPickupByAddressAndStatus(userId, "waiting_collector") - if err != nil { - return nil, fmt.Errorf("gagal mendapatkan data request pickup dengan userid: %v", err) - } - - reqpickaddress, err := s.repoAddress.FindAddressByID(request.AddressId) - if err != nil { - return nil, fmt.Errorf("error fetching address for request pickup %s: %v", request.ID, err) - } - - var nearbyCollectorsResponse []dto.ResponseCollectorDTO - var maxDistance = 10.0 + var avaibleCollectResp []dto.ResponseCollectorDTO for _, collector := range collectors { - address, err := s.repoAddress.FindAddressByID(collector.AddressId) + request, err := s.repoColl.FindRequestPickupByAddressAndStatus(userId, "waiting_collector", "otomatis") if err != nil { - return nil, fmt.Errorf("error fetching address for collector %s: %v", collector.ID, err) + return nil, fmt.Errorf("gagal mendapatkan data request pickup dengan userid: %v", err) } - collectorCoord := utils.Coord{Lat: reqpickaddress.Latitude, Lon: reqpickaddress.Longitude} - userCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} + _, distance := utils.Distance( + utils.Coord{Lat: request.Address.Latitude, Lon: request.Address.Longitude}, + utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, + ) - _, km := utils.Distance(collectorCoord, userCoord) + if distance <= 20 { - if km <= maxDistance { - - nearbyCollectorsResponse = append(nearbyCollectorsResponse, dto.ResponseCollectorDTO{ + mappedRequest := dto.ResponseCollectorDTO{ ID: collector.ID, - AddressId: collector.User.Name, - Rating: collector.Rating, - }) + UserId: collector.UserID, + AddressId: collector.AddressId, + + Rating: collector.Rating, + } + + user, err := s.repoUser.FindByID(collector.UserID) + if err != nil { + return nil, fmt.Errorf("error fetching user data: %v", err) + } + mappedRequest.User = []dto.UserResponseDTO{ + { + Name: user.Name, + Phone: user.Phone, + }, + } + + address, err := s.repoAddress.FindAddressByID(collector.AddressId) + if err != nil { + return nil, fmt.Errorf("error fetching address data: %v", err) + } + mappedRequest.Address = []dto.AddressResponseDTO{ + { + District: address.District, + Village: address.Village, + Detail: address.Detail, + }, + } + + avaibleCollectResp = append(avaibleCollectResp, mappedRequest) } } - if len(nearbyCollectorsResponse) == 0 { - return nil, fmt.Errorf("no request pickups found within %v km", maxDistance) - } - - return nearbyCollectorsResponse, nil + return avaibleCollectResp, nil } func (s *collectorService) ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) { - - request, err := s.repoReq.FindRequestPickupByID(requestId) + request, err := s.repoColl.FindRequestPickupByID(requestId) if err != nil { return nil, fmt.Errorf("request pickup not found: %v", err) } @@ -90,13 +105,14 @@ func (s *collectorService) ConfirmRequestPickup(requestId, collectorId string) ( request.StatusPickup = "confirmed" request.CollectorID = &collector.ID + *request.ConfirmedByCollectorAt = time.Now() - err = s.repoReq.UpdateRequestPickup(requestId, request) + err = s.repoColl.UpdateRequestPickup(requestId, request) if err != nil { return nil, fmt.Errorf("failed to update request pickup: %v", err) } - confirmedAt, _ := utils.FormatDateToIndonesianFormat(request.ConfirmedByCollectorAt) + confirmedAt, _ := utils.FormatDateToIndonesianFormat(*request.ConfirmedByCollectorAt) response := dto.ResponseRequestPickup{ StatusPickup: request.StatusPickup, @@ -106,3 +122,30 @@ func (s *collectorService) ConfirmRequestPickup(requestId, collectorId string) ( return &response, nil } + +func (s *collectorService) ConfirmRequestManualPickup(requestId, collectorId string) (any, error) { + + request, err := s.repoColl.FindRequestPickupByID(requestId) + if err != nil { + return nil, fmt.Errorf("collector tidak ditemukan: %v", err) + } + + coll, err := s.repo.FindCollectorByIdWithoutAddr(collectorId) + if err != nil { + return nil, fmt.Errorf("%v", err) + } + + if coll.ID != *request.CollectorID { + return nil, fmt.Errorf("collectorid tidak sesuai dengan request") + } + + request.StatusPickup = "confirmed" + *request.ConfirmedByCollectorAt = time.Now() + + err = s.repoColl.UpdateRequestPickup(requestId, request) + if err != nil { + return nil, fmt.Errorf("failed to update request pickup: %v", err) + } + + return "berhasil konfirmasi request pickup", nil +} \ No newline at end of file diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go index ecd4514..f79996d 100644 --- a/internal/services/requestpickup_service.go +++ b/internal/services/requestpickup_service.go @@ -12,23 +12,24 @@ type RequestPickupService interface { CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) - // GetAllAutomaticRequestPickups(collector_id string) ([]dto.ResponseRequestPickup, error) - // GetAllAutomaticRequestPickup(collectorId string) ([]dto.ResponseRequestPickup, error) - GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) + SelectCollectorInRequest(userId, collectorId string) error } type requestPickupService struct { repo repositories.RequestPickupRepository - repoReq repositories.CollectorRepository + repoColl repositories.CollectorRepository repoAddress repositories.AddressRepository repoTrash repositories.TrashRepository + repoUser repositories.UserProfilRepository } func NewRequestPickupService(repo repositories.RequestPickupRepository, + repoColl repositories.CollectorRepository, repoAddress repositories.AddressRepository, - repoTrash repositories.TrashRepository) RequestPickupService { - return &requestPickupService{repo: repo, repoAddress: repoAddress, repoTrash: repoTrash} + repoTrash repositories.TrashRepository, + repoUser repositories.UserProfilRepository) RequestPickupService { + return &requestPickupService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoTrash: repoTrash, repoUser: repoUser} } func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) { @@ -43,7 +44,7 @@ func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, Us return nil, fmt.Errorf("address with ID %s not found", request.AddressID) } - existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector") + existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector", "otomatis") if err != nil { return nil, fmt.Errorf("error checking for existing request pickup: %v", err) } @@ -94,9 +95,9 @@ func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, Us } response.RequestItems = append(response.RequestItems, dto.ResponseRequestPickupItem{ - ID: modelItem.ID, - TrashCategoryName: findTrashCategory.Name, - EstimatedAmount: modelItem.EstimatedAmount, + ID: modelItem.ID, + TrashCategory: []dto.ResponseTrashCategoryDTO{{Name: findTrashCategory.Name, Icon: findTrashCategory.Icon}}, + EstimatedAmount: modelItem.EstimatedAmount, }) } @@ -152,8 +153,7 @@ func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.Respon } func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { - - requests, err := s.repo.GetAutomaticRequestPickupsForCollector(collectorId) + requests, err := s.repo.GetAutomaticRequestPickupsForCollector() if err != nil { return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) } @@ -162,8 +162,13 @@ func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) for _, req := range requests { + collector, err := s.repoColl.FindCollectorById(collectorId) + if err != nil { + return nil, fmt.Errorf("error fetching collector data: %v", err) + } + _, distance := utils.Distance( - utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, + utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, ) @@ -179,17 +184,49 @@ func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), } + user, err := s.repoUser.FindByID(req.UserId) + if err != nil { + return nil, fmt.Errorf("error fetching user data: %v", err) + } + mappedRequest.User = []dto.UserResponseDTO{ + { + Name: user.Name, + Phone: user.Phone, + }, + } + + address, err := s.repoAddress.FindAddressByID(req.AddressId) + if err != nil { + return nil, fmt.Errorf("error fetching address data: %v", err) + } + mappedRequest.Address = []dto.AddressResponseDTO{ + { + District: address.District, + Village: address.Village, + Detail: address.Detail, + }, + } + requestItems, err := s.repo.GetRequestPickupItems(req.ID) if err != nil { return nil, fmt.Errorf("error fetching request items: %v", err) } var mappedRequestItems []dto.ResponseRequestPickupItem + for _, item := range requestItems { + trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) + if err != nil { + return nil, fmt.Errorf("error fetching trash category: %v", err) + } + mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ - ID: item.ID, - TrashCategoryName: item.TrashCategory.Name, - EstimatedAmount: item.EstimatedAmount, + ID: item.ID, + TrashCategory: []dto.ResponseTrashCategoryDTO{{ + Name: trashCategory.Name, + Icon: trashCategory.Icon, + }}, + EstimatedAmount: item.EstimatedAmount, }) } @@ -201,3 +238,112 @@ func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) return response, nil } + +func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { + + collector, err := s.repoColl.FindCollectorById(collectorId) + if err != nil { + return nil, fmt.Errorf("error fetching collector data: %v", err) + } + requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) + if err != nil { + return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) + } + + var response []dto.ResponseRequestPickup + + for _, req := range requests { + + createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) + + mappedRequest := dto.ResponseRequestPickup{ + ID: req.ID, + UserId: req.UserId, + AddressID: req.AddressId, + EvidenceImage: req.EvidenceImage, + StatusPickup: req.StatusPickup, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + user, err := s.repoUser.FindByID(req.UserId) + if err != nil { + return nil, fmt.Errorf("error fetching user data: %v", err) + } + mappedRequest.User = []dto.UserResponseDTO{ + { + Name: user.Name, + Phone: user.Phone, + }, + } + + address, err := s.repoAddress.FindAddressByID(req.AddressId) + if err != nil { + return nil, fmt.Errorf("error fetching address data: %v", err) + } + mappedRequest.Address = []dto.AddressResponseDTO{ + { + District: address.District, + Village: address.Village, + Detail: address.Detail, + }, + } + + requestItems, err := s.repo.GetRequestPickupItems(req.ID) + if err != nil { + return nil, fmt.Errorf("error fetching request items: %v", err) + } + + var mappedRequestItems []dto.ResponseRequestPickupItem + + for _, item := range requestItems { + + trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) + if err != nil { + return nil, fmt.Errorf("error fetching trash category: %v", err) + } + + mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ + ID: item.ID, + TrashCategory: []dto.ResponseTrashCategoryDTO{{ + Name: trashCategory.Name, + Icon: trashCategory.Icon, + }}, + EstimatedAmount: item.EstimatedAmount, + }) + } + + mappedRequest.RequestItems = mappedRequestItems + + response = append(response, mappedRequest) + } + + return response, nil +} + +func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { + + request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") + if err != nil { + return fmt.Errorf("request pickup not found: %v", err) + } + + if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { + return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") + } + + collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) + if err != nil { + return fmt.Errorf("collector tidak ditemukan: %v", err) + } + + request.CollectorID = &collector.ID + + err = s.repo.UpdateRequestPickup(request.ID, request) + if err != nil { + return fmt.Errorf("failed to update request pickup: %v", err) + } + + return nil +} diff --git a/internal/services/trash_service.go b/internal/services/trash_service.go index cd66add..bb1cc29 100644 --- a/internal/services/trash_service.go +++ b/internal/services/trash_service.go @@ -244,6 +244,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { var categoriesDTO []dto.ResponseTrashCategoryDTO for _, category := range categories { + // path := os.Getenv("BASE_URL") createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ @@ -255,6 +256,7 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { }) } + cacheData := map[string]interface{}{ "data": categoriesDTO, } diff --git a/model/collector_model.go b/model/collector_model.go index ed024f2..009ed98 100644 --- a/model/collector_model.go +++ b/model/collector_model.go @@ -4,8 +4,13 @@ type Collector struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` UserID string `gorm:"not null" json:"userId"` User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` - JobStatus string `gorm:"default:nonactive" json:"jobstatus"` + JobStatus string `gorm:"default:inactive" json:"jobstatus"` Rating float32 `gorm:"default:5" json:"rating"` AddressId string `gorm:"not null" json:"address_id"` Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` } + +// job_status { +// "active", +// "inactive" +// } diff --git a/model/requestpickup_model.go b/model/requestpickup_model.go index a8ee3e8..13402ca 100644 --- a/model/requestpickup_model.go +++ b/model/requestpickup_model.go @@ -12,9 +12,11 @@ type RequestPickup struct { Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` RequestItems []RequestPickupItem `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;" json:"request_items"` EvidenceImage string `json:"evidence_image"` + Notes string `json:"notes"` StatusPickup string `gorm:"default:'waiting_collector'" json:"status_pickup"` CollectorID *string `gorm:"type:uuid" json:"collector_id,omitempty"` - ConfirmedByCollectorAt time.Time `gorm:"default:current_timestamp" json:"confirmed_by_collector_at,omitempty"` + Collector Collector `gorm:"foreignKey:CollectorID;constraint:OnDelete:CASCADE;" json:"collector"` + ConfirmedByCollectorAt *time.Time `json:"confirmed_by_collector_at,omitempty"` RequestMethod string `gorm:"not null" json:"request_method"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` @@ -25,6 +27,19 @@ type RequestPickupItem struct { RequestPickupId string `gorm:"not null" json:"request_pickup_id"` RequestPickup RequestPickup `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;"` TrashCategoryId string `gorm:"not null" json:"trash_category_id"` - TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryId;constraint:OnDelete:CASCADE;"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryId;constraint:OnDelete:CASCADE;" json:"trash_category"` EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` } + +// request_method { +// "otomatis", +// "manual" +// } + +// status_pickup { +// "waiting_collector", +// "confirmed", +// "collector_picking", +// "completed" +// "canceled" +// } diff --git a/presentation/collector_route.go b/presentation/collector_route.go index 451b3bc..93692b9 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -6,7 +6,8 @@ import ( "rijig/internal/repositories" "rijig/internal/services" "rijig/middleware" - "rijig/utils" + + // "rijig/utils" "github.com/gofiber/fiber/v2" ) @@ -15,12 +16,15 @@ func CollectorRouter(api fiber.Router) { repo := repositories.NewCollectorRepository(config.DB) repoReq := repositories.NewRequestPickupRepository(config.DB) repoAddress := repositories.NewAddressRepository(config.DB) - colectorService := services.NewCollectorService(repo, repoReq, repoAddress) + repoUser := repositories.NewUserProfilRepository(config.DB) + colectorService := services.NewCollectorService(repo, repoReq, repoAddress, repoUser) collectorHandler := handler.NewCollectorHandler(colectorService) collector := api.Group("/collector") - collector.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RolePengepul)) + collector.Use(middleware.AuthMiddleware) collector.Put("confirmrequest/:id", collectorHandler.ConfirmRequestPickup) + collector.Put("confirm-manual/request/:request_id", collectorHandler.ConfirmRequestManualPickup) + collector.Get("/avaible", collectorHandler.GetAvaibleCollector) } diff --git a/presentation/requestpickup_route.go b/presentation/requestpickup_route.go index e06e983..1ef168a 100644 --- a/presentation/requestpickup_route.go +++ b/presentation/requestpickup_route.go @@ -11,13 +11,20 @@ import ( ) func RequestPickupRouter(api fiber.Router) { + // repo repositories.RequestPickupRepository + // repoColl repositories.CollectorRepository + // repoAddress repositories.AddressRepository + // repoTrash repositories.TrashRepository + // repoUser repositories.UserProfilRepository requestRepo := repositories.NewRequestPickupRepository(config.DB) - repoTrash := repositories.NewTrashRepository(config.DB) + repoColl := repositories.NewCollectorRepository(config.DB) repoAddress := repositories.NewAddressRepository(config.DB) + Trashrepo := repositories.NewTrashRepository(config.DB) + repouser := repositories.NewUserProfilRepository(config.DB) // collectorRepo := repositories.NewCollectorRepository(config.DB) - requestPickupServices := services.NewRequestPickupService(requestRepo, repoAddress, repoTrash) + requestPickupServices := services.NewRequestPickupService(requestRepo, repoColl, repoAddress, Trashrepo, repouser) // collectorService := services.NewCollectorService(collectorRepo, requestRepo, repoAddress) // service services.RequestPickupService, // collectorService services.CollectorService @@ -30,6 +37,7 @@ func RequestPickupRouter(api fiber.Router) { requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) // requestPickupAPI.Get("/get", middleware.AuthMiddleware, requestPickupHandler.GetAutomaticRequestByUser) requestPickupAPI.Get("/get-allrequest", requestPickupHandler.GetRequestPickups) + requestPickupAPI.Patch("/select-collector", requestPickupHandler.AssignCollectorToRequest) // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) diff --git a/presentation/trash_route.go b/presentation/trash_route.go index 064bf98..1d738fa 100644 --- a/presentation/trash_route.go +++ b/presentation/trash_route.go @@ -17,16 +17,17 @@ func TrashRouter(api fiber.Router) { trashHandler := handler.NewTrashHandler(trashService) trashAPI := api.Group("/trash") + trashAPI.Use(middleware.AuthMiddleware) - trashAPI.Post("/category", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.CreateCategory) - trashAPI.Post("/category/detail", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.AddDetailToCategory) - trashAPI.Get("/categories", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), trashHandler.GetCategories) - trashAPI.Get("/category/:category_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), trashHandler.GetCategoryByID) - trashAPI.Get("/detail/:detail_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), trashHandler.GetTrashDetailByID) + trashAPI.Post("/category", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.CreateCategory) + trashAPI.Post("/category/detail", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.AddDetailToCategory) + trashAPI.Get("/categories", trashHandler.GetCategories) + trashAPI.Get("/category/:category_id", trashHandler.GetCategoryByID) + trashAPI.Get("/detail/:detail_id", trashHandler.GetTrashDetailByID) - trashAPI.Patch("/category/:category_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateCategory) - trashAPI.Put("/detail/:detail_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateDetail) + trashAPI.Patch("/category/:category_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateCategory) + trashAPI.Put("/detail/:detail_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateDetail) - trashAPI.Delete("/category/:category_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteCategory) - trashAPI.Delete("/detail/:detail_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteDetail) + trashAPI.Delete("/category/:category_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteCategory) + trashAPI.Delete("/detail/:detail_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteDetail) } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 9b99496..2bf1491 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,10 +11,11 @@ import ( ) func SetupRoutes(app *fiber.App) { + apa := app.Group(os.Getenv("BASE_URL")) + apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) - api.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") // || auth router || // // presentation.AuthRouter(api) @@ -41,4 +42,5 @@ func SetupRoutes(app *fiber.App) { presentation.CoverageAreaRouter(api) presentation.StoreRouter(api) presentation.ProductRouter(api) + } From 550f56e27cfe9542c9a698b563d85c5e0205d8d6 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 15 May 2025 11:09:36 +0700 Subject: [PATCH 27/48] fix: fixing response format for about with id --- dto/about_dto.go | 7 +------ internal/repositories/about_repo.go | 12 ++++++++++++ internal/services/about_service.go | 14 ++++++++++++-- presentation/about_route.go | 22 ++++++++++------------ 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/dto/about_dto.go b/dto/about_dto.go index 898e647..c4df688 100644 --- a/dto/about_dto.go +++ b/dto/about_dto.go @@ -7,7 +7,6 @@ import ( type RequestAboutDTO struct { Title string `json:"title"` CoverImage string `json:"cover_image"` - // AboutDetail []RequestAboutDetailDTO `json:"about_detail"` } func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) { @@ -43,11 +42,7 @@ func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool errors := make(map[string][]string) if strings.TrimSpace(r.AboutId) == "" { - errors["about_id"] = append(errors["about_id"], "About ID is required") - } - - if strings.TrimSpace(r.ImageDetail) == "" { - errors["image_detail"] = append(errors["image_detail"], "Image detail is required") + errors["about_id"] = append(errors["about_id"], "about_id is required") } if strings.TrimSpace(r.Description) == "" { diff --git a/internal/repositories/about_repo.go b/internal/repositories/about_repo.go index 3535af9..29224f5 100644 --- a/internal/repositories/about_repo.go +++ b/internal/repositories/about_repo.go @@ -12,6 +12,7 @@ type AboutRepository interface { CreateAboutDetail(aboutDetail *model.AboutDetail) error GetAllAbout() ([]model.About, error) GetAboutByID(id string) (*model.About, error) + GetAboutByIDWithoutPrel(id string) (*model.About, error) GetAboutDetailByID(id string) (*model.AboutDetail, error) UpdateAbout(id string, about *model.About) (*model.About, error) UpdateAboutDetail(id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) @@ -60,6 +61,17 @@ func (r *aboutRepository) GetAboutByID(id string) (*model.About, error) { return &about, nil } +func (r *aboutRepository) GetAboutByIDWithoutPrel(id string) (*model.About, error) { + var about model.About + if err := r.DB.Where("id = ?", id).First(&about).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("about with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch About by ID: %v", err) + } + return &about, nil +} + func (r *aboutRepository) GetAboutDetailByID(id string) (*model.AboutDetail, error) { var aboutDetail model.AboutDetail if err := r.DB.Where("id = ?", id).First(&aboutDetail).Error; err != nil { diff --git a/internal/services/about_service.go b/internal/services/about_service.go index 1d1fcab..6a61a83 100644 --- a/internal/services/about_service.go +++ b/internal/services/about_service.go @@ -272,6 +272,17 @@ func (s *aboutService) GetAboutByID(id string) (*dto.ResponseAboutDTO, error) { return nil, fmt.Errorf("error formatting About response: %v", err) } + var responseDetails []dto.ResponseAboutDetailDTO + for _, detail := range about.AboutDetail { + formattedDetail, err := formatResponseAboutDetailDTO(&detail) + if err != nil { + return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) + } + responseDetails = append(responseDetails, *formattedDetail) + } + + response.AboutDetail = &responseDetails + return response, nil } @@ -317,9 +328,8 @@ func (s *aboutService) CreateAboutDetail(request dto.RequestAboutDetailDTO, cove return nil, fmt.Errorf("validation error: %v", errors) } - _, err := s.aboutRepo.GetAboutByID(request.AboutId) + _, err := s.aboutRepo.GetAboutByIDWithoutPrel(request.AboutId) if err != nil { - return nil, fmt.Errorf("about_id tidak ditemukan: %v", err) } diff --git a/presentation/about_route.go b/presentation/about_route.go index 6616d4e..a803038 100644 --- a/presentation/about_route.go +++ b/presentation/about_route.go @@ -12,26 +12,24 @@ import ( ) func AboutRouter(api fiber.Router) { - aboutRepo := repositories.NewAboutRepository(config.DB) aboutService := services.NewAboutService(aboutRepo) aboutHandler := handler.NewAboutHandler(aboutService) aboutRoutes := api.Group("/about") - aboutRoute := api.Group("/about") - aboutRoutes.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) + aboutRoutes.Use(middleware.AuthMiddleware) - aboutRoute.Get("/", aboutHandler.GetAllAbout) - aboutRoute.Get("/:id", aboutHandler.GetAboutByID) - aboutRoutes.Post("/", aboutHandler.CreateAbout) - aboutRoutes.Put("/:id", aboutHandler.UpdateAbout) - aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) + aboutRoutes.Get("/", aboutHandler.GetAllAbout) + aboutRoutes.Get("/:id", aboutHandler.GetAboutByID) + aboutRoutes.Post("/", aboutHandler.CreateAbout) // admin + aboutRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAbout) + aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) // admin aboutDetailRoutes := api.Group("/about-detail") - aboutDetailRoutes.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) + aboutDetailRoutes.Use(middleware.AuthMiddleware) aboutDetailRoute := api.Group("/about-detail") aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) - aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) - aboutDetailRoutes.Put("/:id", aboutHandler.UpdateAboutDetail) - aboutDetailRoutes.Delete("/:id", aboutHandler.DeleteAboutDetail) + aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin + aboutDetailRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) + aboutDetailRoutes.Delete("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) } From 5b008fd41e876e85e6de4ca310c3618db91c2273 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 15 May 2025 11:28:06 +0700 Subject: [PATCH 28/48] fix&refact: validation response in create about detail --- internal/handler/about_handler.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/handler/about_handler.go b/internal/handler/about_handler.go index ca4f581..ebe895b 100644 --- a/internal/handler/about_handler.go +++ b/internal/handler/about_handler.go @@ -119,6 +119,11 @@ func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { return utils.ErrorResponse(c, "Invalid input data") } + errors, valid := request.ValidateAboutDetail() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + aboutDetailImage, err := c.FormFile("image_detail") if err != nil { log.Printf("Error retrieving image detail from request: %v", err) From fc54cbd1183850c6109d98cb0450264de1ad9b70 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 15 May 2025 16:36:14 +0700 Subject: [PATCH 29/48] fix: fixing image path at about detail --- internal/services/about_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/services/about_service.go b/internal/services/about_service.go index 6a61a83..27a7f17 100644 --- a/internal/services/about_service.go +++ b/internal/services/about_service.go @@ -107,7 +107,7 @@ func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader } func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) { - pathImage := "/uploads/coverabout/coveraboutdetail" + pathImage := "/uploads/coverabout/coveraboutdetail/" coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { From 7fb899ac8b25019398492e544bc49f5946223703 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 16 May 2025 00:13:45 +0700 Subject: [PATCH 30/48] fix: fixing data type and validation also fixing deleting icon in dir --- dto/trash_dto.go | 35 ++++++--- internal/handler/trash_handler.go | 6 ++ internal/services/trash_service.go | 117 +++++++++++++++++------------ model/trash_model.go | 13 ++-- utils/regexp_formatter.go | 19 +++++ 5 files changed, 125 insertions(+), 65 deletions(-) diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 092386d..991ff3d 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -1,19 +1,23 @@ package dto -import "strings" +import ( + "strings" +) type RequestTrashCategoryDTO struct { - Name string `json:"name"` - Icon string `json:"icon"` + Name string `json:"name"` + EstimatedPrice string `json:"estimatedprice"` + Icon string `json:"icon"` } type ResponseTrashCategoryDTO struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Icon string `json:"icon,omitempty"` - CreatedAt string `json:"createdAt,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` - Details []ResponseTrashDetailDTO `json:"details,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Icon string `json:"icon,omitempty"` + EstimatedPrice float64 `json:"estimatedprice"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` + Details []ResponseTrashDetailDTO `json:"details,omitempty"` } type ResponseTrashDetailDTO struct { @@ -37,6 +41,11 @@ func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]str if strings.TrimSpace(r.Name) == "" { errors["name"] = append(errors["name"], "name is required") } + + // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.EstimatedPrice)); !valid { + // errors["estimated_price"] = append(errors["estimated_price"], msg) + // } + if len(errors) > 0 { return errors, false } @@ -50,9 +59,11 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, if strings.TrimSpace(r.Description) == "" { errors["description"] = append(errors["description"], "description is required") } - if r.Price <= 0 { - errors["price"] = append(errors["price"], "price must be greater than 0") - } + + // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.Price)); !valid { + // errors["price"] = append(errors["price"], msg) + // } + if len(errors) > 0 { return errors, false } diff --git a/internal/handler/trash_handler.go b/internal/handler/trash_handler.go index e3cace8..3bec83f 100644 --- a/internal/handler/trash_handler.go +++ b/internal/handler/trash_handler.go @@ -19,10 +19,16 @@ func NewTrashHandler(trashService services.TrashService) *TrashHandler { func (h *TrashHandler) CreateCategory(c *fiber.Ctx) error { var request dto.RequestTrashCategoryDTO + if err := c.BodyParser(&request); err != nil { return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) } + errors, valid := request.ValidateTrashCategoryInput() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + iconTrash, err := c.FormFile("icon") if err != nil { log.Printf("Error retrieving card photo from request: %v", err) diff --git a/internal/services/trash_service.go b/internal/services/trash_service.go index bb1cc29..c4601e9 100644 --- a/internal/services/trash_service.go +++ b/internal/services/trash_service.go @@ -6,6 +6,7 @@ import ( "mime/multipart" "os" "path/filepath" + "strconv" "time" "rijig/dto" @@ -101,9 +102,13 @@ func deleteIconTrashFIle(imagePath string) error { } func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { - errors, valid := request.ValidateTrashCategoryInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) + + parsedPrice, err := strconv.ParseFloat(request.EstimatedPrice, 64) + fmt.Println("Received estimatedprice:", request.EstimatedPrice) + if err != nil { + return nil, fmt.Errorf("gagal memvalidasi harga: %v", err) + } else { + fmt.Printf("hasil parsing%v", parsedPrice) } icontrashPath, err := s.saveIconOfTrash(iconTrash) @@ -113,7 +118,9 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconT category := model.TrashCategory{ Name: request.Name, - Icon: icontrashPath, + + EstimatedPrice: parsedPrice, + Icon: icontrashPath, } if err := s.TrashRepo.CreateCategory(&category); err != nil { @@ -124,11 +131,12 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconT updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: category.ID, + Name: category.Name, + EstimatedPrice: float64(category.EstimatedPrice), + Icon: category.Icon, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } if err := s.CacheCategoryAndDetails(category.ID, categoryResponseDTO, nil, time.Hour*6); err != nil { @@ -142,11 +150,12 @@ func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconT ccreatedAt, _ := utils.FormatDateToIndonesianFormat(c.CreatedAt) cupdatedAt, _ := utils.FormatDateToIndonesianFormat(c.UpdatedAt) categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: c.ID, - Name: c.Name, - Icon: c.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, + ID: c.ID, + Name: c.Name, + EstimatedPrice: float64(c.EstimatedPrice), + Icon: c.Icon, + CreatedAt: ccreatedAt, + UpdatedAt: cupdatedAt, }) } @@ -227,11 +236,12 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { for _, category := range cachedCategories["data"].([]interface{}) { categoryData := category.(map[string]interface{}) categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: categoryData["id"].(string), - Name: categoryData["name"].(string), - Icon: categoryData["icon"].(string), - CreatedAt: categoryData["createdAt"].(string), - UpdatedAt: categoryData["updatedAt"].(string), + ID: categoryData["id"].(string), + Name: categoryData["name"].(string), + EstimatedPrice: categoryData["estimatedprice"].(float64), + Icon: categoryData["icon"].(string), + CreatedAt: categoryData["createdAt"].(string), + UpdatedAt: categoryData["updatedAt"].(string), }) } return categoriesDTO, nil @@ -244,19 +254,19 @@ func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { var categoriesDTO []dto.ResponseTrashCategoryDTO for _, category := range categories { - // path := os.Getenv("BASE_URL") + createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: category.ID, + Name: category.Name, + EstimatedPrice: category.EstimatedPrice, + Icon: category.Icon, + CreatedAt: createdAt, + UpdatedAt: updatedAt, }) } - cacheData := map[string]interface{}{ "data": categoriesDTO, } @@ -274,12 +284,13 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO categoryData := cachedCategory["data"].(map[string]interface{}) details := mapDetails(cachedCategory["details"]) return &dto.ResponseTrashCategoryDTO{ - ID: categoryData["id"].(string), - Name: categoryData["name"].(string), - Icon: categoryData["icon"].(string), - CreatedAt: categoryData["createdAt"].(string), - UpdatedAt: categoryData["updatedAt"].(string), - Details: details, + ID: categoryData["id"].(string), + Name: categoryData["name"].(string), + EstimatedPrice: categoryData["estimatedprice"].(float64), + Icon: categoryData["icon"].(string), + CreatedAt: categoryData["createdAt"].(string), + UpdatedAt: categoryData["updatedAt"].(string), + Details: details, }, nil } @@ -292,11 +303,12 @@ func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoryDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: category.ID, + Name: category.Name, + EstimatedPrice: category.EstimatedPrice, + Icon: category.Icon, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } if category.Details != nil { @@ -405,11 +417,12 @@ func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategor updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + ID: category.ID, + Name: category.Name, + EstimatedPrice: category.EstimatedPrice, + Icon: category.Icon, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } if err := s.CacheCategoryAndDetails(category.ID, categoryResponseDTO, category.Details, time.Hour*6); err != nil { @@ -423,11 +436,12 @@ func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategor ccreatedAt, _ := utils.FormatDateToIndonesianFormat(c.CreatedAt) cupdatedAt, _ := utils.FormatDateToIndonesianFormat(c.UpdatedAt) categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: c.ID, - Name: c.Name, - Icon: c.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, + ID: c.ID, + Name: c.Name, + EstimatedPrice: c.EstimatedPrice, + Icon: c.Icon, + CreatedAt: ccreatedAt, + UpdatedAt: cupdatedAt, }) } @@ -512,6 +526,15 @@ func (s *trashService) DeleteCategory(id string) error { } } + category, err := s.TrashRepo.GetCategoryByID(id) + if err != nil { + return fmt.Errorf("failed to fetch category for deletion: %v", err) + } + + if err := deleteIconTrashFIle(category.Icon); err != nil { + return fmt.Errorf("error deleting icon for category %s: %v", id, err) + } + if err := s.TrashRepo.DeleteCategory(id); err != nil { return fmt.Errorf("failed to delete category: %v", err) } diff --git a/model/trash_model.go b/model/trash_model.go index 7a90d0d..ba44939 100644 --- a/model/trash_model.go +++ b/model/trash_model.go @@ -3,12 +3,13 @@ package model import "time" type TrashCategory struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - Name string `gorm:"not null" json:"name"` - Icon string `json:"icon,omitempty"` - Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` - 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()" json:"id"` + Name string `gorm:"not null" json:"name"` + Icon string `json:"icon,omitempty"` + EstimatedPrice float64 `gorm:"not null" json:"estimated_price"` + Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } type TrashDetail struct { diff --git a/utils/regexp_formatter.go b/utils/regexp_formatter.go index 616675f..3b020ed 100644 --- a/utils/regexp_formatter.go +++ b/utils/regexp_formatter.go @@ -1,7 +1,9 @@ package utils import ( + "fmt" "regexp" + "strconv" "strings" ) @@ -42,3 +44,20 @@ func isSpecialCharacter(char rune) bool { specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" return strings.ContainsRune(specialChars, char) } + +func ValidateFloatPrice(price string) (float64, error) { + + // price = strings.Trim(price, `"`) + // price = strings.TrimSpace(price) + + parsedPrice, err := strconv.ParseFloat(price, 64) + if err != nil { + return 0, fmt.Errorf("harga tidak valid. Format harga harus angka desimal.") + } + + if parsedPrice <= 0 { + return 0, fmt.Errorf("harga harus lebih besar dari 0.") + } + + return parsedPrice, nil +} From 3f911ae321128b4ef651f08bd7923dc0301622f3 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 16 May 2025 01:06:55 +0700 Subject: [PATCH 31/48] feat: logout whatsapp session and remove device --- config/whatsapp.go | 40 ++++++++++++++++++++++++++-- internal/handler/whatsapp_handler.go | 24 +++++++++++++++++ presentation/whatsapp_route.go | 13 +++++++++ router/setup_routes.go.go | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 internal/handler/whatsapp_handler.go create mode 100644 presentation/whatsapp_route.go diff --git a/config/whatsapp.go b/config/whatsapp.go index 18d073c..1ed9dca 100644 --- a/config/whatsapp.go +++ b/config/whatsapp.go @@ -25,7 +25,7 @@ func InitWhatsApp() { dbLog := waLog.Stdout("Database", "DEBUG", true) dsn := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s?sslmode=disable", + "postgres://%s:%s@%s:%s/%s?sslmode=disable", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_HOST"), @@ -80,7 +80,6 @@ func generateQRCode(qrString string) { qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) } - func handleShutdown() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) @@ -109,3 +108,40 @@ func SendWhatsAppMessage(phone, message string) error { log.Printf("WhatsApp message sent successfully to: %s", phone) return nil } + +func LogoutWhatsApp() error { + if WhatsAppClient == nil { + return fmt.Errorf("WhatsApp client is not initialized") + } + + WhatsAppClient.Disconnect() + + err := removeWhatsAppDeviceFromContainer() + if err != nil { + return fmt.Errorf("failed to remove device from container: %v", err) + } + + err = container.Close() + if err != nil { + return fmt.Errorf("failed to close database connection: %v", err) + } + + log.Println("WhatsApp client disconnected and session cleared successfully.") + return nil +} + +func removeWhatsAppDeviceFromContainer() error { + deviceStore, err := container.GetFirstDevice() + if err != nil { + return fmt.Errorf("failed to get WhatsApp device: %v", err) + } + + if deviceStore != nil { + err := deviceStore.Delete() + if err != nil { + return fmt.Errorf("failed to remove device from store: %v", err) + } + } + + return nil +} diff --git a/internal/handler/whatsapp_handler.go b/internal/handler/whatsapp_handler.go new file mode 100644 index 0000000..e875f36 --- /dev/null +++ b/internal/handler/whatsapp_handler.go @@ -0,0 +1,24 @@ +package handler + +import ( + "log" + "rijig/config" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func WhatsAppHandler(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "User is not logged in or invalid session") + } + + err := config.LogoutWhatsApp() + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.ErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Logged out successfully") +} diff --git a/presentation/whatsapp_route.go b/presentation/whatsapp_route.go new file mode 100644 index 0000000..dd9c14e --- /dev/null +++ b/presentation/whatsapp_route.go @@ -0,0 +1,13 @@ +package presentation + +import ( + "rijig/internal/handler" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func WhatsAppRouter(api fiber.Router) { + api.Post("/logout/whastapp", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), handler.WhatsAppHandler) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 2bf1491..ecfad81 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -42,5 +42,6 @@ func SetupRoutes(app *fiber.App) { presentation.CoverageAreaRouter(api) presentation.StoreRouter(api) presentation.ProductRouter(api) + presentation.WhatsAppRouter(api) } From 043a2fe78e7dfc48644d9fa64517b932e5092301 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 17 May 2025 22:30:30 +0700 Subject: [PATCH 32/48] fix:fixing image upload --- internal/services/article_service.go | 150 +++++++++++++-------------- model/trashcart_model.go | 21 ++++ 2 files changed, 94 insertions(+), 77 deletions(-) create mode 100644 model/trashcart_model.go diff --git a/internal/services/article_service.go b/internal/services/article_service.go index b733a8d..a2e10ee 100644 --- a/internal/services/article_service.go +++ b/internal/services/article_service.go @@ -3,6 +3,7 @@ package services import ( "encoding/json" "fmt" + "log" "mime/multipart" "os" "path/filepath" @@ -32,41 +33,77 @@ func NewArticleService(articleRepo repositories.ArticleRepository) ArticleServic return &articleService{ArticleRepo: articleRepo} } -func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { +func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) { + pathImage := "/uploads/articles/" + coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(coverArticleDir); os.IsNotExist(err) { - coverImageDir := "./public" + os.Getenv("BASE_URL") + "/uploads/articles" - if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { - return nil, fmt.Errorf("failed to create directory for cover image: %v", err) + if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for cover article: %v", err) + } } - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} - extension := filepath.Ext(coverImage.Filename) + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverArticle.Filename) if !allowedExtensions[extension] { - return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") } - coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension) - coverImagePath := filepath.Join(coverImageDir, coverImageFileName) + coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension) + coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName) - src, err := coverImage.Open() + src, err := coverArticle.Open() if err != nil { - return nil, fmt.Errorf("failed to open uploaded file: %v", err) + return "", fmt.Errorf("failed to open uploaded file: %v", err) } defer src.Close() - dst, err := os.Create(coverImagePath) + dst, err := os.Create(coverArticlePath) if err != nil { - return nil, fmt.Errorf("failed to create cover image file: %v", err) + return "", fmt.Errorf("failed to create cover article file: %v", err) } defer dst.Close() if _, err := dst.ReadFrom(src); err != nil { - return nil, fmt.Errorf("failed to save cover image: %v", err) + return "", fmt.Errorf("failed to save cover article: %v", err) + } + + iconTrashUrl := fmt.Sprintf("%s%s", pathImage, coverArticleFileName) + + return iconTrashUrl, nil +} + +func deleteCoverArticle(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { + + coverArticlePath, err := s.saveCoverArticle(coverImage) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) } article := model.Article{ Title: request.Title, - CoverImage: coverImagePath, + CoverImage: coverArticlePath, Author: request.Author, Heading: request.Heading, Content: request.Content, @@ -254,21 +291,30 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, return nil, fmt.Errorf("article not found: %v", id) } + if article.CoverImage != "" { + err := deleteCoverArticle(article.CoverImage) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + var coverArticlePath string + if coverImage != nil { + coverArticlePath, err = s.saveCoverArticle(coverImage) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + } + + if coverArticlePath != "" { + article.CoverImage = coverArticlePath + } + article.Title = request.Title article.Heading = request.Heading article.Content = request.Content article.Author = request.Author - var coverImagePath string - if coverImage != nil { - - coverImagePath, err = s.saveCoverImage(coverImage, article.CoverImage) - if err != nil { - return nil, fmt.Errorf("failed to save cover image: %v", err) - } - article.CoverImage = coverImagePath - } - err = s.ArticleRepo.UpdateArticle(id, article) if err != nil { return nil, fmt.Errorf("failed to update article: %v", err) @@ -338,64 +384,14 @@ func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, return articleResponseDTO, nil } -func (s *articleService) saveCoverImage(coverImage *multipart.FileHeader, oldImagePath string) (string, error) { - coverImageDir := "/uploads/articles" - if _, err := os.Stat(coverImageDir); os.IsNotExist(err) { - if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for cover image: %v", err) - } - } - - extension := filepath.Ext(coverImage.Filename) - if extension != ".jpg" && extension != ".jpeg" && extension != ".png" { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension) - coverImagePath := filepath.Join(coverImageDir, coverImageFileName) - - if oldImagePath != "" { - err := os.Remove(oldImagePath) - if err != nil { - fmt.Printf("Failed to delete old cover image: %v\n", err) - } else { - fmt.Printf("Successfully deleted old cover image: %s\n", oldImagePath) - } - } - - src, err := coverImage.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(coverImagePath) - if err != nil { - return "", fmt.Errorf("failed to create cover image file: %v", err) - } - defer dst.Close() - - _, err = dst.ReadFrom(src) - if err != nil { - return "", fmt.Errorf("failed to save cover image: %v", err) - } - - return coverImagePath, nil -} - func (s *articleService) DeleteArticle(id string) error { article, err := s.ArticleRepo.FindArticleByID(id) if err != nil { return fmt.Errorf("failed to find article: %v", id) } - if article.CoverImage != "" { - err := os.Remove(article.CoverImage) - if err != nil { - fmt.Printf("Failed to delete cover image: %v\n", err) - } else { - fmt.Printf("Successfully deleted cover image: %s\n", article.CoverImage) - } + if err := deleteCoverArticle(article.CoverImage); err != nil { + return fmt.Errorf("error waktu menghapus cover image article %s: %v", id, err) } err = s.ArticleRepo.DeleteArticle(id) diff --git a/model/trashcart_model.go b/model/trashcart_model.go new file mode 100644 index 0000000..af23000 --- /dev/null +++ b/model/trashcart_model.go @@ -0,0 +1,21 @@ +package model + +import "time" + +type Cart struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + UserId string `gorm:"not null" json:"userid"` + User User `gorm:"foreignKey:UserId;constraint:OnDelete:CASCADE;" json:"user"` + CartItem []CartItems `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"cartitems"` + TotalAmount float32 `json:"totalamount"` + EstimatedTotalPrice float32 `json:"estimated_totalprice"` +} + +type CartItems struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + TrashId string `json:"trashid"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashId;constraint:OnDelete:CASCADE;" json:"trash"` + Amount float32 `json:"amount"` + CreaatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} From 0467598e58de6663f8654549ecb6ffffec672be8 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 19 May 2025 02:16:24 +0700 Subject: [PATCH 33/48] feat: add feature cart worker and auto commit redis to Db --- cmd/cart_worker.go | 51 +++++ cmd/main.go | 86 ++++++--- config/database.go | 3 + dto/trashcart_dto.go | 50 +++++ go.mod | 1 + go.sum | 2 + internal/handler/trashcart_handler.go | 96 ++++++++++ internal/repositories/trash_repo.go | 10 + internal/repositories/trashcart_repo.go | 100 ++++++++++ internal/services/trashcart_service.go | 240 ++++++++++++++++++++++++ internal/worker/cart_committer.go | 58 ++++++ model/trashcart_model.go | 34 ++-- presentation/trashcart_route.go | 26 +++ router/setup_routes.go.go | 1 + 14 files changed, 719 insertions(+), 39 deletions(-) create mode 100644 cmd/cart_worker.go create mode 100644 dto/trashcart_dto.go create mode 100644 internal/handler/trashcart_handler.go create mode 100644 internal/repositories/trashcart_repo.go create mode 100644 internal/services/trashcart_service.go create mode 100644 internal/worker/cart_committer.go create mode 100644 presentation/trashcart_route.go diff --git a/cmd/cart_worker.go b/cmd/cart_worker.go new file mode 100644 index 0000000..d58aab4 --- /dev/null +++ b/cmd/cart_worker.go @@ -0,0 +1,51 @@ +package main + +// import ( +// "context" +// "log" +// "strings" +// "time" + +// "rijig/config" +// "rijig/internal/services" +// ) + +// // func main() { +// // config.SetupConfig() + +// // } + +// func processCartKeys(ctx context.Context, cartService services.CartService) { +// pattern := "cart:user:*" +// iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() + +// for iter.Next(ctx) { +// key := iter.Val() +// ttl, err := config.RedisClient.TTL(ctx, key).Result() +// if err != nil { +// log.Printf("Failed to get TTL for key %s: %v", key, err) +// continue +// } + +// if ttl <= time.Minute { +// log.Printf("🔄 Auto-committing key: %s", key) +// parts := strings.Split(key, ":") +// if len(parts) != 3 { +// log.Printf("Invalid key format: %s", key) +// continue +// } +// userID := parts[2] + +// err := cartService.CommitCartFromRedis(userID) +// if err != nil { +// log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) +// } else { +// log.Printf("✅ Cart for user %s committed successfully", userID) +// } +// } +// } + +// if err := iter.Err(); err != nil { +// log.Printf("Error iterating keys: %v", err) +// } +// } diff --git a/cmd/main.go b/cmd/main.go index 024c357..64aede6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,8 +1,14 @@ package main import ( + "context" + "log" "rijig/config" + "rijig/internal/repositories" + "rijig/internal/services" "rijig/router" + "strings" + "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -11,37 +17,67 @@ import ( func main() { config.SetupConfig() app := fiber.New() - app.Use(cors.New(cors.Config{ - AllowOrigins: "*", + AllowOrigins: "*", AllowMethods: "GET,POST,PUT,PATCH,DELETE", AllowHeaders: "Content-Type,x-api-key", })) - // app.Use(cors.New(cors.Config{ - // AllowOrigins: "http://localhost:3000", - // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", - // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", - // AllowCredentials: true, - // })) - - // app.Use(func(c *fiber.Ctx) error { - // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - // c.Set("Access-Control-Allow-Credentials", "true") - // return c.Next() - // }) - - // app.Options("*", func(c *fiber.Ctx) error { - // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - // c.Set("Access-Control-Allow-Credentials", "true") - // return c.SendStatus(fiber.StatusNoContent) - // }) - + // Route setup router.SetupRoutes(app) + // Siapkan dependency untuk worker + repoCart := repositories.NewCartRepository() + repoTrash := repositories.NewTrashRepository(config.DB) + cartService := services.NewCartService(repoCart, repoTrash) + ctx := context.Background() + + // ✅ Jalankan worker di background + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + log.Println("🛠️ Cart Worker is running in background...") + for range ticker.C { + processCartKeys(ctx, cartService) + } + }() + + // 🚀 Jalankan server (blocking) config.StartServer(app) } + +func processCartKeys(ctx context.Context, cartService services.CartService) { + pattern := "cart:user:*" + iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() + + for iter.Next(ctx) { + key := iter.Val() + ttl, err := config.RedisClient.TTL(ctx, key).Result() + if err != nil { + log.Printf("Failed to get TTL for key %s: %v", key, err) + continue + } + + if ttl <= time.Minute { + log.Printf("🔄 Auto-committing key: %s", key) + parts := strings.Split(key, ":") + if len(parts) != 3 { + log.Printf("Invalid key format: %s", key) + continue + } + userID := parts[2] + + err := cartService.CommitCartFromRedis(userID) + if err != nil { + log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) + } else { + log.Printf("✅ Cart for user %s committed successfully", userID) + } + } + } + + if err := iter.Err(); err != nil { + log.Printf("Error iterating keys: %v", err) + } +} diff --git a/config/database.go b/config/database.go index e9e920c..95c9859 100644 --- a/config/database.go +++ b/config/database.go @@ -53,6 +53,9 @@ func ConnectDatabase() { // =>requestpickup preparation<= &model.RequestPickup{}, &model.RequestPickupItem{}, + + &model.Cart{}, + &model.CartItem{}, // =>requestpickup preparation<= // =>store preparation<= diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go new file mode 100644 index 0000000..f117e49 --- /dev/null +++ b/dto/trashcart_dto.go @@ -0,0 +1,50 @@ +package dto + +import ( + "strings" + "time" +) + +type ValidationErrors struct { + Errors map[string][]string +} + +func (v ValidationErrors) Error() string { + return "validation error" +} + +type CartResponse struct { + ID string `json:"id"` + UserID string `json:"userid"` + CartItems []CartItemResponse `json:"cartitems"` + TotalAmount float32 `json:"totalamount"` + EstimatedTotalPrice float32 `json:"estimated_totalprice"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CartItemResponse struct { + TrashIcon string `json:"trashicon"` + TrashName string `json:"trashname"` + Amount float32 `json:"amount"` + EstimatedSubTotalPrice float32 `json:"estimated_subtotalprice"` +} + +type RequestCartItems struct { + TrashID string `json:"trashid"` + Amount float32 `json:"amount"` +} + +func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.TrashID) == "" { + errors["trashid"] = append(errors["trashid"], "trashid is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/go.mod b/go.mod index 687dde0..4279056 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect + github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/go.sum b/go.sum index 83b4326..d5b6c0b 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= diff --git a/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go new file mode 100644 index 0000000..5e61d80 --- /dev/null +++ b/internal/handler/trashcart_handler.go @@ -0,0 +1,96 @@ +package handler + +import ( + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CartHandler struct { + CartService services.CartService +} + +func NewCartHandler(service services.CartService) *CartHandler { + return &CartHandler{ + CartService: service, + } +} + +// GET /cart - Get cart by user ID +func (h *CartHandler) GetCart(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "unauthorized or invalid user") + } + + cart, err := h.CartService.GetCartByUserID(userID) + if err != nil { + return utils.InternalServerErrorResponse(c, "failed to retrieve cart") + } + + if cart == nil { + return utils.SuccessResponse(c, nil, "Cart is empty") + } + + return utils.SuccessResponse(c, cart, "User cart data successfully fetched") +} + +// POST /cart - Create new cart +func (h *CartHandler) CreateCart(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "unauthorized or invalid user") + } + + var reqItems []dto.RequestCartItems + if err := c.BodyParser(&reqItems); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"invalid JSON format"}, + }) + } + + // Logic dipindahkan ke service + if err := h.CartService.CreateCartFromDTO(userID, reqItems); err != nil { + if ve, ok := err.(dto.ValidationErrors); ok { + return utils.ValidationErrorResponse(c, ve.Errors) + } + return utils.InternalServerErrorResponse(c, "failed to create cart") + } + + return utils.CreateResponse(c, nil, "Cart created successfully") +} + + +// DELETE /cart/:id - Delete cart by cartID +func (h *CartHandler) DeleteCart(c *fiber.Ctx) error { + cartID := c.Params("id") + if cartID == "" { + return utils.ErrorResponse(c, "Cart ID is required") + } + + if err := h.CartService.DeleteCart(cartID); err != nil { + return utils.InternalServerErrorResponse(c, "failed to delete cart") + } + + return utils.SuccessResponse(c, nil, "Cart deleted successfully") +} + +// POST /cart/commit - Simpan cart dari Redis ke DB +func (h *CartHandler) CommitCart(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.ErrorResponse(c, "unauthorized or invalid user") + } + + err := h.CartService.CommitCartFromRedis(userID) + if err != nil { + if err.Error() == "cart not found in redis" { + return utils.ErrorResponse(c, "Cart tidak ditemukan atau sudah expired") + } + return utils.InternalServerErrorResponse(c, "Gagal menyimpan cart ke database") + } + + return utils.SuccessResponse(c, nil, "Cart berhasil disimpan ke database") +} diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 380716a..dd6bab4 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -15,6 +15,7 @@ type TrashRepository interface { AddDetailToCategory(detail *model.TrashDetail) error GetCategories() ([]model.TrashCategory, error) GetCategoryByID(id string) (*model.TrashCategory, error) + GetTrashCategoryByName(name string) (*model.TrashCategory, error) GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) UpdateCategoryName(id string, newName string) error @@ -63,6 +64,15 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } +func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.DB.Find(&category, "name = ?", name).Error; err != nil { + return nil, fmt.Errorf("category not found: %v", err) + } + return &category, nil +} + func (r *trashRepository) GetTrashDetailByID(id string) (*model.TrashDetail, error) { var detail model.TrashDetail if err := r.DB.First(&detail, "id = ?", id).Error; err != nil { diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go new file mode 100644 index 0000000..5ad88b3 --- /dev/null +++ b/internal/repositories/trashcart_repo.go @@ -0,0 +1,100 @@ +package repositories + +import ( + "errors" + "log" + + "rijig/config" + "rijig/model" + + "gorm.io/gorm" +) + +type CartRepository interface { + Create(cart *model.Cart) error + GetByUserID(userID string) (*model.Cart, error) + Update(cart *model.Cart) error + InsertCartItem(item *model.CartItem) error + UpdateCartItem(item *model.CartItem) error + DeleteCartItemByID(id string) error + Delete(cartID string) error + DeleteByUserID(userID string) error +} + +type cartRepository struct { + db *gorm.DB +} + +func NewCartRepository() CartRepository { + return &cartRepository{ + db: config.DB, + } +} + +func (r *cartRepository) Create(cart *model.Cart) error { + tx := r.db.Begin() + if err := tx.Create(cart).Error; err != nil { + tx.Rollback() + return err + } + return tx.Commit().Error +} + +func (r *cartRepository) GetByUserID(userID string) (*model.Cart, error) { + var cart model.Cart + + err := r.db. + Preload("CartItems.TrashCategory"). + Where("user_id = ?", userID). + First(&cart).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + log.Printf("Error retrieving cart for user %s: %v", userID, err) + return nil, errors.New("failed to retrieve cart") + } + + return &cart, nil +} + +func (r *cartRepository) Update(cart *model.Cart) error { + err := r.db.Save(cart).Error + if err != nil { + log.Printf("Error updating cart %s: %v", cart.ID, err) + return errors.New("failed to update cart") + } + return nil +} + +func (r *cartRepository) InsertCartItem(item *model.CartItem) error { + return r.db.Create(item).Error +} + +func (r *cartRepository) UpdateCartItem(item *model.CartItem) error { + return r.db.Save(item).Error +} + +func (r *cartRepository) DeleteCartItemByID(id string) error { + return r.db.Delete(&model.CartItem{}, "id = ?", id).Error +} + +func (r *cartRepository) Delete(cartID string) error { + result := r.db.Where("id = ?", cartID).Delete(&model.Cart{}) + if result.Error != nil { + log.Printf("Error deleting cart %s: %v", cartID, result.Error) + return errors.New("failed to delete cart") + } + + if result.RowsAffected == 0 { + log.Printf("Cart with ID %s not found for deletion", cartID) + return errors.New("cart not found") + } + + return nil +} + +func (r *cartRepository) DeleteByUserID(userID string) error { + return r.db.Where("user_id = ?", userID).Delete(&model.Cart{}).Error +} diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go new file mode 100644 index 0000000..1562bb7 --- /dev/null +++ b/internal/services/trashcart_service.go @@ -0,0 +1,240 @@ +package services + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "time" + + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "rijig/utils" +) + +type CartService interface { + CreateCartFromDTO(userID string, items []dto.RequestCartItems) error + GetCartByUserID(userID string) (*dto.CartResponse, error) + CommitCartFromRedis(userID string) error + DeleteCart(cartID string) error +} + +type cartService struct { + repo repositories.CartRepository + repoTrash repositories.TrashRepository +} + +func NewCartService(repo repositories.CartRepository, repoTrash repositories.TrashRepository) CartService { + return &cartService{repo: repo, repoTrash: repoTrash} +} + +func redisCartKey(userID string) string { + return fmt.Sprintf("cart:user:%s", userID) +} + +func (s *cartService) CreateCartFromDTO(userID string, items []dto.RequestCartItems) error { + // Validasi semua item + for _, item := range items { + if errMap, valid := item.ValidateRequestCartItem(); !valid { + return dto.ValidationErrors{Errors: errMap} + } + } + + // Ambil cart yang sudah ada dari Redis (jika ada) + var existingCart dto.CartResponse + val, err := utils.GetData(redisCartKey(userID)) + if err == nil && val != "" { + if err := json.Unmarshal([]byte(val), &existingCart); err != nil { + log.Printf("Failed to unmarshal existing cart: %v", err) + } + } + + // Buat map dari existing items untuk mempermudah update + itemMap := make(map[string]dto.CartItemResponse) + for _, item := range existingCart.CartItems { + itemMap[item.TrashName] = item + } + + // Proses input baru + for _, input := range items { + trash, err := s.repoTrash.GetCategoryByID(input.TrashID) + if err != nil { + return fmt.Errorf("failed to retrieve trash category for id %s: %v", input.TrashID, err) + } + + if input.Amount == 0 { + delete(itemMap, trash.Name) // hapus item + continue + } + + subtotal := float32(trash.EstimatedPrice) * input.Amount + + itemMap[trash.Name] = dto.CartItemResponse{ + TrashIcon: trash.Icon, + TrashName: trash.Name, + Amount: input.Amount, + EstimatedSubTotalPrice: subtotal, + } + } + + // Rekonstruksi cart + var finalItems []dto.CartItemResponse + var totalAmount float32 + var totalPrice float32 + for _, item := range itemMap { + finalItems = append(finalItems, item) + totalAmount += item.Amount + totalPrice += item.EstimatedSubTotalPrice + } + + cart := dto.CartResponse{ + ID: existingCart.ID, + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: finalItems, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // Simpan ulang ke Redis dengan TTL 10 menit + return utils.SetData(redisCartKey(userID), cart, 1*time.Minute) +} + + +func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) { + val, err := utils.GetData(redisCartKey(userID)) + if err != nil { + log.Printf("Redis get error: %v", err) + } + if val != "" { + var cached dto.CartResponse + if err := json.Unmarshal([]byte(val), &cached); err == nil { + return &cached, nil + } + } + + cart, err := s.repo.GetByUserID(userID) + if err != nil { + return nil, err + } + if cart == nil { + return nil, nil + } + + var items []dto.CartItemResponse + for _, item := range cart.CartItems { + items = append(items, dto.CartItemResponse{ + TrashIcon: item.TrashCategory.Icon, + TrashName: item.TrashCategory.Name, + Amount: item.Amount, + EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, + }) + } + + response := &dto.CartResponse{ + ID: cart.ID, + UserID: cart.UserID, + TotalAmount: cart.TotalAmount, + EstimatedTotalPrice: cart.EstimatedTotalPrice, + CartItems: items, + CreatedAt: cart.CreatedAt, + UpdatedAt: cart.UpdatedAt, + } + + return response, nil +} + +func (s *cartService) CommitCartFromRedis(userID string) error { + val, err := utils.GetData(redisCartKey(userID)) + if err != nil || val == "" { + return errors.New("no cart found in redis") + } + + var cartDTO dto.CartResponse + if err := json.Unmarshal([]byte(val), &cartDTO); err != nil { + return errors.New("invalid cart data in Redis") + } + + existingCart, err := s.repo.GetByUserID(userID) + if err != nil { + return fmt.Errorf("failed to get cart from db: %v", err) + } + + if existingCart == nil { + // buat cart baru jika belum ada + var items []model.CartItem + for _, item := range cartDTO.CartItems { + trash, err := s.repoTrash.GetTrashCategoryByName(item.TrashName) + if err != nil { + continue + } + + items = append(items, model.CartItem{ + TrashID: trash.ID, + Amount: item.Amount, + SubTotalEstimatedPrice: item.EstimatedSubTotalPrice, + }) + } + + newCart := model.Cart{ + UserID: userID, + TotalAmount: cartDTO.TotalAmount, + EstimatedTotalPrice: cartDTO.EstimatedTotalPrice, + CartItems: items, + } + + return s.repo.Create(&newCart) + } + + // buat map item lama (by trash_name) + existingItemMap := make(map[string]*model.CartItem) + for i := range existingCart.CartItems { + trashName := existingCart.CartItems[i].TrashCategory.Name + existingItemMap[trashName] = &existingCart.CartItems[i] + } + + // proses update/hapus/tambah + for _, newItem := range cartDTO.CartItems { + if newItem.Amount == 0 { + if existing, ok := existingItemMap[newItem.TrashName]; ok { + _ = s.repo.DeleteCartItemByID(existing.ID) + } + continue + } + + trash, err := s.repoTrash.GetTrashCategoryByName(newItem.TrashName) + if err != nil { + continue + } + + if existing, ok := existingItemMap[newItem.TrashName]; ok { + existing.Amount = newItem.Amount + existing.SubTotalEstimatedPrice = newItem.EstimatedSubTotalPrice + _ = s.repo.UpdateCartItem(existing) + } else { + newModelItem := model.CartItem{ + CartID: existingCart.ID, + TrashID: trash.ID, + Amount: newItem.Amount, + SubTotalEstimatedPrice: newItem.EstimatedSubTotalPrice, + } + _ = s.repo.InsertCartItem(&newModelItem) + } + } + + // update cart total amount & price + existingCart.TotalAmount = cartDTO.TotalAmount + existingCart.EstimatedTotalPrice = cartDTO.EstimatedTotalPrice + if err := s.repo.Update(existingCart); err != nil { + return err + } + + return utils.DeleteData(redisCartKey(userID)) +} + + +func (s *cartService) DeleteCart(cartID string) error { + return s.repo.Delete(cartID) +} diff --git a/internal/worker/cart_committer.go b/internal/worker/cart_committer.go new file mode 100644 index 0000000..efe45aa --- /dev/null +++ b/internal/worker/cart_committer.go @@ -0,0 +1,58 @@ +package worker + +import ( + "context" + "encoding/json" + "log" + + "rijig/config" + "rijig/internal/repositories" + "rijig/model" +) + +type CartCommitter struct { + repo repositories.CartRepository +} + +func NewCartCommitter(repo repositories.CartRepository) *CartCommitter { + return &CartCommitter{repo: repo} +} + +func (cc *CartCommitter) RunAutoCommit() { + ctx := context.Background() + pattern := "cart:user:*" + + iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() + for iter.Next(ctx) { + key := iter.Val() + + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + log.Printf("Error fetching key %s: %v", key, err) + continue + } + + var cart model.Cart + if err := json.Unmarshal([]byte(val), &cart); err != nil { + log.Printf("Invalid cart format in key %s: %v", key, err) + continue + } + + // Simpan ke DB + if err := cc.repo.Create(&cart); err != nil { + log.Printf("Failed to commit cart to DB from key %s: %v", key, err) + continue + } + + // Delete from Redis + if err := config.RedisClient.Del(ctx, key).Err(); err != nil { + log.Printf("Failed to delete key %s after commit: %v", key, err) + } else { + log.Printf("Committed and deleted key %s successfully", key) + } + } + + if err := iter.Err(); err != nil { + log.Printf("Redis scan error: %v", err) + } +} diff --git a/model/trashcart_model.go b/model/trashcart_model.go index af23000..97aa8d0 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -1,21 +1,27 @@ package model -import "time" +import ( + "time" +) type Cart struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - UserId string `gorm:"not null" json:"userid"` - User User `gorm:"foreignKey:UserId;constraint:OnDelete:CASCADE;" json:"user"` - CartItem []CartItems `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + UserID string `gorm:"not null" json:"userid"` + User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;" json:"-"` + CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cartitems"` + TotalAmount float32 `json:"totalamount"` + EstimatedTotalPrice float32 `json:"estimated_totalprice"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` } -type CartItems struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - TrashId string `json:"trashid"` - TrashCategory TrashCategory `gorm:"foreignKey:TrashId;constraint:OnDelete:CASCADE;" json:"trash"` - Amount float32 `json:"amount"` - CreaatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +type CartItem struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + CartID string `gorm:"not null" json:"-"` + TrashID string `gorm:"not null" json:"trashid"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashID;constraint:OnDelete:CASCADE;" json:"trash"` + Amount float32 `json:"amount"` + SubTotalEstimatedPrice float32 `json:"subtotalestimatedprice"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` } diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go new file mode 100644 index 0000000..a7313b7 --- /dev/null +++ b/presentation/trashcart_route.go @@ -0,0 +1,26 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func TrashCartRouter(api fiber.Router) { + + cartRepo := repositories.NewCartRepository() + trashRepo := repositories.NewTrashRepository(config.DB) + cartService := services.NewCartService(cartRepo, trashRepo) + cartHandler := handler.NewCartHandler(cartService) + + cart := api.Group("/cart") + cart.Use(middleware.AuthMiddleware) + cart.Post("/", cartHandler.CreateCart) + cart.Get("/", cartHandler.GetCart) + cart.Post("/commit", cartHandler.CommitCart) + cart.Delete("/:id", cartHandler.DeleteCart) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index ecfad81..d23059d 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -28,6 +28,7 @@ func SetupRoutes(app *fiber.App) { presentation.CompanyProfileRouter(api) presentation.RequestPickupRouter(api) presentation.CollectorRouter(api) + presentation.TrashCartRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api) From 3f1e0a96ca14a971eba33527387f5e1d3f8a98b3 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 19 May 2025 05:15:41 +0700 Subject: [PATCH 34/48] fix: fixing login worker and flow logic --- cmd/cart_worker.go | 51 ---- cmd/main.go | 89 ++---- dto/trash_dto.go | 8 - dto/trashcart_dto.go | 23 +- go.mod | 1 - internal/handler/trashcart_handler.go | 130 ++++----- internal/repositories/trash_repo.go | 10 + internal/repositories/trashcart_repo.go | 100 ++----- internal/services/trashcart_redisservices.go | 123 ++++++++ internal/services/trashcart_service.go | 285 +++++++------------ internal/worker/cart_committer.go | 58 ---- internal/worker/cart_worker.go | 76 +++++ middleware/auth_middleware.go | 4 + presentation/trashcart_route.go | 17 +- 14 files changed, 454 insertions(+), 521 deletions(-) delete mode 100644 cmd/cart_worker.go create mode 100644 internal/services/trashcart_redisservices.go delete mode 100644 internal/worker/cart_committer.go create mode 100644 internal/worker/cart_worker.go diff --git a/cmd/cart_worker.go b/cmd/cart_worker.go deleted file mode 100644 index d58aab4..0000000 --- a/cmd/cart_worker.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -// import ( -// "context" -// "log" -// "strings" -// "time" - -// "rijig/config" -// "rijig/internal/services" -// ) - -// // func main() { -// // config.SetupConfig() - -// // } - -// func processCartKeys(ctx context.Context, cartService services.CartService) { -// pattern := "cart:user:*" -// iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - -// for iter.Next(ctx) { -// key := iter.Val() -// ttl, err := config.RedisClient.TTL(ctx, key).Result() -// if err != nil { -// log.Printf("Failed to get TTL for key %s: %v", key, err) -// continue -// } - -// if ttl <= time.Minute { -// log.Printf("🔄 Auto-committing key: %s", key) -// parts := strings.Split(key, ":") -// if len(parts) != 3 { -// log.Printf("Invalid key format: %s", key) -// continue -// } -// userID := parts[2] - -// err := cartService.CommitCartFromRedis(userID) -// if err != nil { -// log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) -// } else { -// log.Printf("✅ Cart for user %s committed successfully", userID) -// } -// } -// } - -// if err := iter.Err(); err != nil { -// log.Printf("Error iterating keys: %v", err) -// } -// } diff --git a/cmd/main.go b/cmd/main.go index 64aede6..2ab9825 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,14 +1,8 @@ package main import ( - "context" - "log" "rijig/config" - "rijig/internal/repositories" - "rijig/internal/services" "rijig/router" - "strings" - "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -17,67 +11,36 @@ import ( func main() { config.SetupConfig() app := fiber.New() + app.Use(cors.New(cors.Config{ - AllowOrigins: "*", + AllowOrigins: "*", AllowMethods: "GET,POST,PUT,PATCH,DELETE", AllowHeaders: "Content-Type,x-api-key", })) - // Route setup + // app.Use(cors.New(cors.Config{ + // AllowOrigins: "http://localhost:3000", + // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", + // AllowCredentials: true, + // })) + + // app.Use(func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.Next() + // }) + + // app.Options("*", func(c *fiber.Ctx) error { + // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") + // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") + // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") + // c.Set("Access-Control-Allow-Credentials", "true") + // return c.SendStatus(fiber.StatusNoContent) + // }) + router.SetupRoutes(app) - - // Siapkan dependency untuk worker - repoCart := repositories.NewCartRepository() - repoTrash := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(repoCart, repoTrash) - ctx := context.Background() - - // ✅ Jalankan worker di background - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - - log.Println("🛠️ Cart Worker is running in background...") - for range ticker.C { - processCartKeys(ctx, cartService) - } - }() - - // 🚀 Jalankan server (blocking) config.StartServer(app) -} - -func processCartKeys(ctx context.Context, cartService services.CartService) { - pattern := "cart:user:*" - iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - - for iter.Next(ctx) { - key := iter.Val() - ttl, err := config.RedisClient.TTL(ctx, key).Result() - if err != nil { - log.Printf("Failed to get TTL for key %s: %v", key, err) - continue - } - - if ttl <= time.Minute { - log.Printf("🔄 Auto-committing key: %s", key) - parts := strings.Split(key, ":") - if len(parts) != 3 { - log.Printf("Invalid key format: %s", key) - continue - } - userID := parts[2] - - err := cartService.CommitCartFromRedis(userID) - if err != nil { - log.Printf("❌ Failed to commit cart for user %s: %v", userID, err) - } else { - log.Printf("✅ Cart for user %s committed successfully", userID) - } - } - } - - if err := iter.Err(); err != nil { - log.Printf("Error iterating keys: %v", err) - } -} +} \ No newline at end of file diff --git a/dto/trash_dto.go b/dto/trash_dto.go index 991ff3d..d1f5fd9 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -42,10 +42,6 @@ func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]str errors["name"] = append(errors["name"], "name is required") } - // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.EstimatedPrice)); !valid { - // errors["estimated_price"] = append(errors["estimated_price"], msg) - // } - if len(errors) > 0 { return errors, false } @@ -60,10 +56,6 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, errors["description"] = append(errors["description"], "description is required") } - // if valid, msg := utils.ValidateFloatPrice(fmt.Sprintf("%f", r.Price)); !valid { - // errors["price"] = append(errors["price"], msg) - // } - if len(errors) > 0 { return errors, false } diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index f117e49..4c9a0d6 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -1,8 +1,8 @@ package dto import ( + "fmt" "strings" - "time" ) type ValidationErrors struct { @@ -19,8 +19,8 @@ type CartResponse struct { CartItems []CartItemResponse `json:"cartitems"` TotalAmount float32 `json:"totalamount"` EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` } type CartItemResponse struct { @@ -48,3 +48,20 @@ func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) return nil, true } + +type BulkRequestCartItems struct { + Items []RequestCartItems `json:"items"` +} + +func (b *BulkRequestCartItems) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + for i, item := range b.Items { + if strings.TrimSpace(item.TrashID) == "" { + errors[fmt.Sprintf("items[%d].trashid", i)] = append(errors[fmt.Sprintf("items[%d].trashid", i)], "trashid is required") + } + } + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/go.mod b/go.mod index 4279056..687dde0 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect - github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go index 5e61d80..7890e77 100644 --- a/internal/handler/trashcart_handler.go +++ b/internal/handler/trashcart_handler.go @@ -9,88 +9,88 @@ import ( ) type CartHandler struct { - CartService services.CartService + Service *services.CartService } -func NewCartHandler(service services.CartService) *CartHandler { - return &CartHandler{ - CartService: service, - } +func NewCartHandler(service *services.CartService) *CartHandler { + return &CartHandler{Service: service} } -// GET /cart - Get cart by user ID -func (h *CartHandler) GetCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") +func (h *CartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { + var body dto.BulkRequestCartItems + if err := c.BodyParser(&body); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"Invalid JSON body"}, + }) } - cart, err := h.CartService.GetCartByUserID(userID) + if errors, ok := body.Validate(); !ok { + return utils.ValidationErrorResponse(c, errors) + } + + userID := c.Locals("userID").(string) + for _, item := range body.Items { + if err := services.AddOrUpdateCartItem(userID, item); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to update one or more items") + } + } + + return utils.SuccessResponse(c, nil, "Cart updated successfully") +} + +func (h *CartHandler) DeleteCartItem(c *fiber.Ctx) error { + trashID := c.Params("trashid") + userID := c.Locals("userID").(string) + + err := services.DeleteCartItem(userID, trashID) if err != nil { - return utils.InternalServerErrorResponse(c, "failed to retrieve cart") + if err.Error() == "no cart found" || err.Error() == "trashid not found" { + return utils.GenericResponse(c, fiber.StatusNotFound, "Trash item not found in cart") + } + return utils.InternalServerErrorResponse(c, "Failed to delete item") } - if cart == nil { - return utils.SuccessResponse(c, nil, "Cart is empty") + return utils.SuccessResponse(c, nil, "Item deleted") +} + +func (h *CartHandler) ClearCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + if err := services.ClearCart(userID); err != nil { + return utils.InternalServerErrorResponse(c, "Failed to clear cart") + } + return utils.SuccessResponse(c, nil, "Cart cleared") +} + +func (h *CartHandler) GetCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + cart, err := h.Service.GetCart(userID) + if err != nil { + return utils.InternalServerErrorResponse(c, "Failed to fetch cart") } return utils.SuccessResponse(c, cart, "User cart data successfully fetched") } -// POST /cart - Create new cart -func (h *CartHandler) CreateCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") - } - - var reqItems []dto.RequestCartItems - if err := c.BodyParser(&reqItems); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"invalid JSON format"}, - }) - } - - // Logic dipindahkan ke service - if err := h.CartService.CreateCartFromDTO(userID, reqItems); err != nil { - if ve, ok := err.(dto.ValidationErrors); ok { - return utils.ValidationErrorResponse(c, ve.Errors) - } - return utils.InternalServerErrorResponse(c, "failed to create cart") - } - - return utils.CreateResponse(c, nil, "Cart created successfully") -} - - -// DELETE /cart/:id - Delete cart by cartID -func (h *CartHandler) DeleteCart(c *fiber.Ctx) error { - cartID := c.Params("id") - if cartID == "" { - return utils.ErrorResponse(c, "Cart ID is required") - } - - if err := h.CartService.DeleteCart(cartID); err != nil { - return utils.InternalServerErrorResponse(c, "failed to delete cart") - } - - return utils.SuccessResponse(c, nil, "Cart deleted successfully") -} - -// POST /cart/commit - Simpan cart dari Redis ke DB func (h *CartHandler) CommitCart(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "unauthorized or invalid user") - } + userID := c.Locals("userID").(string) - err := h.CartService.CommitCartFromRedis(userID) + err := h.Service.CommitCartToDatabase(userID) if err != nil { - if err.Error() == "cart not found in redis" { - return utils.ErrorResponse(c, "Cart tidak ditemukan atau sudah expired") - } - return utils.InternalServerErrorResponse(c, "Gagal menyimpan cart ke database") + return utils.InternalServerErrorResponse(c, "Failed to commit cart to database") } - return utils.SuccessResponse(c, nil, "Cart berhasil disimpan ke database") + return utils.SuccessResponse(c, nil, "Cart committed to database") +} + +// PUT /cart/refresh → refresh TTL Redis +func (h *CartHandler) RefreshCartTTL(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + err := services.RefreshCartTTL(userID) + if err != nil { + return utils.InternalServerErrorResponse(c, "Failed to refresh cart TTL") + } + + return utils.SuccessResponse(c, nil, "Cart TTL refreshed") } diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index dd6bab4..99a5e1d 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -15,6 +15,7 @@ type TrashRepository interface { AddDetailToCategory(detail *model.TrashDetail) error GetCategories() ([]model.TrashCategory, error) GetCategoryByID(id string) (*model.TrashCategory, error) + FindCategoryId(id string) (*model.TrashCategory, error) GetTrashCategoryByName(name string) (*model.TrashCategory, error) GetTrashDetailByID(id string) (*model.TrashDetail, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) @@ -64,6 +65,15 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } +func (r *trashRepository) FindCategoryId(id string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.DB.First(&category, "id = ?", id).Error; err != nil { + return nil, fmt.Errorf("category not found: %v", err) + } + return &category, nil +} + func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) { var category model.TrashCategory diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go index 5ad88b3..59e37ff 100644 --- a/internal/repositories/trashcart_repo.go +++ b/internal/repositories/trashcart_repo.go @@ -1,100 +1,46 @@ package repositories import ( - "errors" - "log" - "rijig/config" "rijig/model" - - "gorm.io/gorm" ) type CartRepository interface { - Create(cart *model.Cart) error - GetByUserID(userID string) (*model.Cart, error) - Update(cart *model.Cart) error - InsertCartItem(item *model.CartItem) error - UpdateCartItem(item *model.CartItem) error - DeleteCartItemByID(id string) error - Delete(cartID string) error - DeleteByUserID(userID string) error + CreateCart(cart *model.Cart) error + GetTrashCategoryByID(id string) (*model.TrashCategory, error) + GetCartByUserID(userID string) (*model.Cart, error) + DeleteCartByUserID(userID string) error } -type cartRepository struct { - db *gorm.DB -} +type cartRepository struct{} func NewCartRepository() CartRepository { - return &cartRepository{ - db: config.DB, - } + return &cartRepository{} } -func (r *cartRepository) Create(cart *model.Cart) error { - tx := r.db.Begin() - if err := tx.Create(cart).Error; err != nil { - tx.Rollback() - return err - } - return tx.Commit().Error +func (r *cartRepository) CreateCart(cart *model.Cart) error { + return config.DB.Create(cart).Error } -func (r *cartRepository) GetByUserID(userID string) (*model.Cart, error) { +func (r *cartRepository) DeleteCartByUserID(userID string) error { + return config.DB.Where("user_id = ?", userID).Delete(&model.Cart{}).Error +} + +func (r *cartRepository) GetTrashCategoryByID(id string) (*model.TrashCategory, error) { + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", id).Error; err != nil { + return nil, err + } + return &trash, nil +} + +func (r *cartRepository) GetCartByUserID(userID string) (*model.Cart, error) { var cart model.Cart - - err := r.db. - Preload("CartItems.TrashCategory"). + err := config.DB.Preload("CartItems.TrashCategory"). Where("user_id = ?", userID). First(&cart).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil - } - log.Printf("Error retrieving cart for user %s: %v", userID, err) - return nil, errors.New("failed to retrieve cart") + return nil, err } - return &cart, nil } - -func (r *cartRepository) Update(cart *model.Cart) error { - err := r.db.Save(cart).Error - if err != nil { - log.Printf("Error updating cart %s: %v", cart.ID, err) - return errors.New("failed to update cart") - } - return nil -} - -func (r *cartRepository) InsertCartItem(item *model.CartItem) error { - return r.db.Create(item).Error -} - -func (r *cartRepository) UpdateCartItem(item *model.CartItem) error { - return r.db.Save(item).Error -} - -func (r *cartRepository) DeleteCartItemByID(id string) error { - return r.db.Delete(&model.CartItem{}, "id = ?", id).Error -} - -func (r *cartRepository) Delete(cartID string) error { - result := r.db.Where("id = ?", cartID).Delete(&model.Cart{}) - if result.Error != nil { - log.Printf("Error deleting cart %s: %v", cartID, result.Error) - return errors.New("failed to delete cart") - } - - if result.RowsAffected == 0 { - log.Printf("Cart with ID %s not found for deletion", cartID) - return errors.New("cart not found") - } - - return nil -} - -func (r *cartRepository) DeleteByUserID(userID string) error { - return r.db.Where("user_id = ?", userID).Delete(&model.Cart{}).Error -} diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go new file mode 100644 index 0000000..1108fc0 --- /dev/null +++ b/internal/services/trashcart_redisservices.go @@ -0,0 +1,123 @@ +package services + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "rijig/config" + "rijig/dto" + + "github.com/go-redis/redis/v8" +) + +const cartTTL = 1 * time.Minute + +func getCartKey(userID string) string { + return fmt.Sprintf("cart:%s", userID) +} + +func GetCartItems(userID string) ([]dto.RequestCartItems, error) { + key := getCartKey(userID) + val, err := config.RedisClient.Get(config.Ctx, key).Result() + if err != nil { + return nil, err + } + + var items []dto.RequestCartItems + err = json.Unmarshal([]byte(val), &items) + if err != nil { + return nil, err + } + return items, nil +} + +func AddOrUpdateCartItem(userID string, newItem dto.RequestCartItems) error { + key := getCartKey(userID) + var cartItems []dto.RequestCartItems + + val, err := config.RedisClient.Get(config.Ctx, key).Result() + if err == nil && val != "" { + json.Unmarshal([]byte(val), &cartItems) + } + + updated := false + for i, item := range cartItems { + if item.TrashID == newItem.TrashID { + if newItem.Amount == 0 { + cartItems = append(cartItems[:i], cartItems[i+1:]...) + } else { + cartItems[i].Amount = newItem.Amount + } + updated = true + break + } + } + + if !updated && newItem.Amount > 0 { + cartItems = append(cartItems, newItem) + } + + return setCartItems(key, cartItems) +} + +func DeleteCartItem(userID, trashID string) error { + key := fmt.Sprintf("cart:%s", userID) + items, err := GetCartItems(userID) + + if err == redis.Nil { + + log.Printf("No cart found in Redis for user: %s", userID) + return fmt.Errorf("no cart found") + } + + if err != nil { + log.Printf("Redis error: %v", err) + return err + } + + index := -1 + for i, item := range items { + if item.TrashID == trashID { + index = i + break + } + } + + if index == -1 { + log.Printf("TrashID %s not found in cart for user %s", trashID, userID) + return fmt.Errorf("trashid not found") + } + + items = append(items[:index], items[index+1:]...) + + if len(items) == 0 { + return config.RedisClient.Del(config.Ctx, key).Err() + } + + return setCartItems(key, items) +} + +func ClearCart(userID string) error { + key := getCartKey(userID) + return config.RedisClient.Del(config.Ctx, key).Err() +} + +func RefreshCartTTL(userID string) error { + key := getCartKey(userID) + return config.RedisClient.Expire(config.Ctx, key, cartTTL).Err() +} + +func setCartItems(key string, items []dto.RequestCartItems) error { + data, err := json.Marshal(items) + if err != nil { + return err + } + + err = config.RedisClient.Set(config.Ctx, key, data, cartTTL).Err() + if err != nil { + log.Printf("Redis SetCart error: %v", err) + } + return err +} diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go index 1562bb7..939ec40 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -1,130 +1,135 @@ package services import ( - "encoding/json" - "errors" - "fmt" "log" "time" "rijig/dto" "rijig/internal/repositories" "rijig/model" - "rijig/utils" + + "github.com/google/uuid" ) -type CartService interface { - CreateCartFromDTO(userID string, items []dto.RequestCartItems) error - GetCartByUserID(userID string) (*dto.CartResponse, error) - CommitCartFromRedis(userID string) error - DeleteCart(cartID string) error +type CartService struct { + Repo repositories.CartRepository } -type cartService struct { - repo repositories.CartRepository - repoTrash repositories.TrashRepository +func NewCartService(repo repositories.CartRepository) *CartService { + return &CartService{Repo: repo} } -func NewCartService(repo repositories.CartRepository, repoTrash repositories.TrashRepository) CartService { - return &cartService{repo: repo, repoTrash: repoTrash} -} +func (s *CartService) CommitCartToDatabase(userID string) error { + items, err := GetCartItems(userID) + if err != nil || len(items) == 0 { + log.Printf("No items to commit for user: %s", userID) + return err + } -func redisCartKey(userID string) string { - return fmt.Sprintf("cart:user:%s", userID) -} + var cartItems []model.CartItem + var totalAmount float32 + var estimatedTotal float32 -func (s *cartService) CreateCartFromDTO(userID string, items []dto.RequestCartItems) error { - // Validasi semua item for _, item := range items { - if errMap, valid := item.ValidateRequestCartItem(); !valid { - return dto.ValidationErrors{Errors: errMap} - } - } - - // Ambil cart yang sudah ada dari Redis (jika ada) - var existingCart dto.CartResponse - val, err := utils.GetData(redisCartKey(userID)) - if err == nil && val != "" { - if err := json.Unmarshal([]byte(val), &existingCart); err != nil { - log.Printf("Failed to unmarshal existing cart: %v", err) - } - } - - // Buat map dari existing items untuk mempermudah update - itemMap := make(map[string]dto.CartItemResponse) - for _, item := range existingCart.CartItems { - itemMap[item.TrashName] = item - } - - // Proses input baru - for _, input := range items { - trash, err := s.repoTrash.GetCategoryByID(input.TrashID) + trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) if err != nil { - return fmt.Errorf("failed to retrieve trash category for id %s: %v", input.TrashID, err) - } - - if input.Amount == 0 { - delete(itemMap, trash.Name) // hapus item + log.Printf("Trash category not found for trashID: %s", item.TrashID) continue } - subtotal := float32(trash.EstimatedPrice) * input.Amount - - itemMap[trash.Name] = dto.CartItemResponse{ - TrashIcon: trash.Icon, - TrashName: trash.Name, - Amount: input.Amount, - EstimatedSubTotalPrice: subtotal, - } - } - - // Rekonstruksi cart - var finalItems []dto.CartItemResponse - var totalAmount float32 - var totalPrice float32 - for _, item := range itemMap { - finalItems = append(finalItems, item) + subTotal := float32(trash.EstimatedPrice) * item.Amount totalAmount += item.Amount - totalPrice += item.EstimatedSubTotalPrice + estimatedTotal += subTotal + + cartItems = append(cartItems, model.CartItem{ + ID: uuid.NewString(), + TrashID: item.TrashID, + Amount: item.Amount, + SubTotalEstimatedPrice: subTotal, + }) } - cart := dto.CartResponse{ - ID: existingCart.ID, + cart := &model.Cart{ + ID: uuid.NewString(), UserID: userID, + CartItems: cartItems, TotalAmount: totalAmount, - EstimatedTotalPrice: totalPrice, - CartItems: finalItems, + EstimatedTotalPrice: estimatedTotal, CreatedAt: time.Now(), UpdatedAt: time.Now(), } - // Simpan ulang ke Redis dengan TTL 10 menit - return utils.SetData(redisCartKey(userID), cart, 1*time.Minute) + if err := s.Repo.DeleteCartByUserID(userID); err != nil { + log.Printf("Failed to delete old cart: %v", err) + } + + if err := s.Repo.CreateCart(cart); err != nil { + log.Printf("Failed to create cart: %v", err) + return err + } + + if err := ClearCart(userID); err != nil { + log.Printf("Failed to clear Redis cart: %v", err) + } + + log.Printf("Cart committed successfully for user: %s", userID) + return nil } - -func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) { - val, err := utils.GetData(redisCartKey(userID)) - if err != nil { - log.Printf("Redis get error: %v", err) +func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { + items, err := GetCartItems(userID) + if err != nil || len(items) == 0 { + return nil, err } - if val != "" { - var cached dto.CartResponse - if err := json.Unmarshal([]byte(val), &cached); err == nil { - return &cached, nil + + var totalAmount float32 + var estimatedTotal float32 + var cartItemDTOs []dto.CartItemResponse + + for _, item := range items { + trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) + if err != nil { + continue } + + subtotal := float32(trash.EstimatedPrice) * item.Amount + totalAmount += item.Amount + estimatedTotal += subtotal + + cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ + TrashIcon: trash.Icon, + TrashName: trash.Name, + Amount: item.Amount, + EstimatedSubTotalPrice: subtotal, + }) } - cart, err := s.repo.GetByUserID(userID) + resp := &dto.CartResponse{ + ID: "N/A", + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: estimatedTotal, + CreatedAt: time.Now().Format(time.RFC3339), + UpdatedAt: time.Now().Format(time.RFC3339), + CartItems: cartItemDTOs, + } + return resp, nil +} + +func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { + + cartRedis, err := s.GetCartFromRedis(userID) + if err == nil && len(cartRedis.CartItems) > 0 { + return cartRedis, nil + } + + cartDB, err := s.Repo.GetCartByUserID(userID) if err != nil { return nil, err } - if cart == nil { - return nil, nil - } var items []dto.CartItemResponse - for _, item := range cart.CartItems { + for _, item := range cartDB.CartItems { items = append(items, dto.CartItemResponse{ TrashIcon: item.TrashCategory.Icon, TrashName: item.TrashCategory.Name, @@ -133,108 +138,14 @@ func (s *cartService) GetCartByUserID(userID string) (*dto.CartResponse, error) }) } - response := &dto.CartResponse{ - ID: cart.ID, - UserID: cart.UserID, - TotalAmount: cart.TotalAmount, - EstimatedTotalPrice: cart.EstimatedTotalPrice, + resp := &dto.CartResponse{ + ID: cartDB.ID, + UserID: cartDB.UserID, + TotalAmount: cartDB.TotalAmount, + EstimatedTotalPrice: cartDB.EstimatedTotalPrice, + CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), + UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), CartItems: items, - CreatedAt: cart.CreatedAt, - UpdatedAt: cart.UpdatedAt, } - - return response, nil -} - -func (s *cartService) CommitCartFromRedis(userID string) error { - val, err := utils.GetData(redisCartKey(userID)) - if err != nil || val == "" { - return errors.New("no cart found in redis") - } - - var cartDTO dto.CartResponse - if err := json.Unmarshal([]byte(val), &cartDTO); err != nil { - return errors.New("invalid cart data in Redis") - } - - existingCart, err := s.repo.GetByUserID(userID) - if err != nil { - return fmt.Errorf("failed to get cart from db: %v", err) - } - - if existingCart == nil { - // buat cart baru jika belum ada - var items []model.CartItem - for _, item := range cartDTO.CartItems { - trash, err := s.repoTrash.GetTrashCategoryByName(item.TrashName) - if err != nil { - continue - } - - items = append(items, model.CartItem{ - TrashID: trash.ID, - Amount: item.Amount, - SubTotalEstimatedPrice: item.EstimatedSubTotalPrice, - }) - } - - newCart := model.Cart{ - UserID: userID, - TotalAmount: cartDTO.TotalAmount, - EstimatedTotalPrice: cartDTO.EstimatedTotalPrice, - CartItems: items, - } - - return s.repo.Create(&newCart) - } - - // buat map item lama (by trash_name) - existingItemMap := make(map[string]*model.CartItem) - for i := range existingCart.CartItems { - trashName := existingCart.CartItems[i].TrashCategory.Name - existingItemMap[trashName] = &existingCart.CartItems[i] - } - - // proses update/hapus/tambah - for _, newItem := range cartDTO.CartItems { - if newItem.Amount == 0 { - if existing, ok := existingItemMap[newItem.TrashName]; ok { - _ = s.repo.DeleteCartItemByID(existing.ID) - } - continue - } - - trash, err := s.repoTrash.GetTrashCategoryByName(newItem.TrashName) - if err != nil { - continue - } - - if existing, ok := existingItemMap[newItem.TrashName]; ok { - existing.Amount = newItem.Amount - existing.SubTotalEstimatedPrice = newItem.EstimatedSubTotalPrice - _ = s.repo.UpdateCartItem(existing) - } else { - newModelItem := model.CartItem{ - CartID: existingCart.ID, - TrashID: trash.ID, - Amount: newItem.Amount, - SubTotalEstimatedPrice: newItem.EstimatedSubTotalPrice, - } - _ = s.repo.InsertCartItem(&newModelItem) - } - } - - // update cart total amount & price - existingCart.TotalAmount = cartDTO.TotalAmount - existingCart.EstimatedTotalPrice = cartDTO.EstimatedTotalPrice - if err := s.repo.Update(existingCart); err != nil { - return err - } - - return utils.DeleteData(redisCartKey(userID)) -} - - -func (s *cartService) DeleteCart(cartID string) error { - return s.repo.Delete(cartID) + return resp, nil } diff --git a/internal/worker/cart_committer.go b/internal/worker/cart_committer.go deleted file mode 100644 index efe45aa..0000000 --- a/internal/worker/cart_committer.go +++ /dev/null @@ -1,58 +0,0 @@ -package worker - -import ( - "context" - "encoding/json" - "log" - - "rijig/config" - "rijig/internal/repositories" - "rijig/model" -) - -type CartCommitter struct { - repo repositories.CartRepository -} - -func NewCartCommitter(repo repositories.CartRepository) *CartCommitter { - return &CartCommitter{repo: repo} -} - -func (cc *CartCommitter) RunAutoCommit() { - ctx := context.Background() - pattern := "cart:user:*" - - iter := config.RedisClient.Scan(ctx, 0, pattern, 0).Iterator() - for iter.Next(ctx) { - key := iter.Val() - - val, err := config.RedisClient.Get(ctx, key).Result() - if err != nil { - log.Printf("Error fetching key %s: %v", key, err) - continue - } - - var cart model.Cart - if err := json.Unmarshal([]byte(val), &cart); err != nil { - log.Printf("Invalid cart format in key %s: %v", key, err) - continue - } - - // Simpan ke DB - if err := cc.repo.Create(&cart); err != nil { - log.Printf("Failed to commit cart to DB from key %s: %v", key, err) - continue - } - - // Delete from Redis - if err := config.RedisClient.Del(ctx, key).Err(); err != nil { - log.Printf("Failed to delete key %s after commit: %v", key, err) - } else { - log.Printf("Committed and deleted key %s successfully", key) - } - } - - if err := iter.Err(); err != nil { - log.Printf("Redis scan error: %v", err) - } -} diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go new file mode 100644 index 0000000..ce208ab --- /dev/null +++ b/internal/worker/cart_worker.go @@ -0,0 +1,76 @@ +package worker + +import ( + "log" + "strings" + "time" + + "rijig/config" + "rijig/internal/services" +) + +const ( + lockPrefix = "lock:cart:" + lockExpiration = 30 * time.Second + commitThreshold = 20 * time.Second + scanPattern = "cart:*" +) + +func StartCartCommitWorker(service *services.CartService) { + + go func() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + log.Println("🛠️ Cart Worker is running in background...") + for range ticker.C { + processCarts(service) + } + }() + +} + +func processCarts(service *services.CartService) { + iter := config.RedisClient.Scan(config.Ctx, 0, scanPattern, 0).Iterator() + for iter.Next(config.Ctx) { + key := iter.Val() + + ttl, err := config.RedisClient.TTL(config.Ctx, key).Result() + if err != nil { + log.Printf("❌ Error getting TTL for %s: %v", key, err) + continue + } + + if ttl > 0 && ttl < commitThreshold { + userID := extractUserIDFromKey(key) + if userID == "" { + continue + } + + lockKey := lockPrefix + userID + acquired, err := config.RedisClient.SetNX(config.Ctx, lockKey, "locked", lockExpiration).Result() + if err != nil || !acquired { + continue + } + + log.Printf("🔄 Auto-committing cart for user %s (TTL: %v)", userID, ttl) + if err := service.CommitCartToDatabase(userID); err != nil { + log.Printf("❌ Failed to commit cart for %s: %v", userID, err) + } else { + log.Printf("✅ Cart committed for user %s", userID) + } + + } + } + + if err := iter.Err(); err != nil { + log.Printf("❌ Error iterating Redis keys: %v", err) + } +} + +func extractUserIDFromKey(key string) string { + if strings.HasPrefix(key, "cart:") { + return strings.TrimPrefix(key, "cart:") + } + return "" +} diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go index 400798b..9549e25 100644 --- a/middleware/auth_middleware.go +++ b/middleware/auth_middleware.go @@ -35,6 +35,10 @@ func AuthMiddleware(c *fiber.Ctx) error { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") } + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Unexpected signing method") + } + userID := claims["sub"].(string) deviceID := claims["device_id"].(string) diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go index a7313b7..277010d 100644 --- a/presentation/trashcart_route.go +++ b/presentation/trashcart_route.go @@ -1,26 +1,27 @@ package presentation import ( - "rijig/config" "rijig/internal/handler" "rijig/internal/repositories" "rijig/internal/services" + "rijig/internal/worker" "rijig/middleware" "github.com/gofiber/fiber/v2" ) func TrashCartRouter(api fiber.Router) { - cartRepo := repositories.NewCartRepository() - trashRepo := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(cartRepo, trashRepo) + cartService := services.NewCartService(cartRepo) cartHandler := handler.NewCartHandler(cartService) - cart := api.Group("/cart") - cart.Use(middleware.AuthMiddleware) - cart.Post("/", cartHandler.CreateCart) + worker.StartCartCommitWorker(cartService) + + cart := api.Group("/cart", middleware.AuthMiddleware) + cart.Put("/refresh", cartHandler.RefreshCartTTL) + cart.Post("/", cartHandler.AddOrUpdateCartItem) cart.Get("/", cartHandler.GetCart) cart.Post("/commit", cartHandler.CommitCart) - cart.Delete("/:id", cartHandler.DeleteCart) + cart.Delete("/", cartHandler.ClearCart) + cart.Delete("/:trashid", cartHandler.DeleteCartItem) } From 6df861f7764b962c89827ee4408011c0f62f1165 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 21 May 2025 13:00:41 +0700 Subject: [PATCH 35/48] refact: recator response --- dto/trashcart_dto.go | 2 ++ internal/services/trashcart_redisservices.go | 1 + internal/services/trashcart_service.go | 3 +++ 3 files changed, 6 insertions(+) diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index 4c9a0d6..e955600 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -24,6 +24,8 @@ type CartResponse struct { } type CartItemResponse struct { + ItemId string `json:"item_id"` + TrashId string `json:"trashid"` TrashIcon string `json:"trashicon"` TrashName string `json:"trashname"` Amount float32 `json:"amount"` diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go index 1108fc0..a19b149 100644 --- a/internal/services/trashcart_redisservices.go +++ b/internal/services/trashcart_redisservices.go @@ -30,6 +30,7 @@ func GetCartItems(userID string) ([]dto.RequestCartItems, error) { if err != nil { return nil, err } + return items, nil } diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go index 939ec40..04ebfd4 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -97,6 +97,7 @@ func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) estimatedTotal += subtotal cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ + TrashId: trash.ID, TrashIcon: trash.Icon, TrashName: trash.Name, Amount: item.Amount, @@ -131,6 +132,8 @@ func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { var items []dto.CartItemResponse for _, item := range cartDB.CartItems { items = append(items, dto.CartItemResponse{ + ItemId: item.ID, + TrashId: item.TrashID, TrashIcon: item.TrashCategory.Icon, TrashName: item.TrashCategory.Name, Amount: item.Amount, From 226d188ece469905fcd0bdeb3fa5bba60334dfa5 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 22 May 2025 00:03:16 +0700 Subject: [PATCH 36/48] refact: stress refact collector and make average price for trashcategory --- config/database.go | 1 + dto/collector_dto.go | 78 ++++- dto/trashcart_dto.go | 8 +- internal/handler/collector_handler.go | 252 +++++++++++++-- internal/handler/requestpickup_handler.go | 134 ++++---- internal/repositories/collector_repo.go | 218 ++++++++----- internal/repositories/trash_repo.go | 20 ++ internal/services/collector_service.go | 297 ++++++++++------- internal/services/requestpickup_service.go | 324 +++++++++---------- internal/services/trashcart_redisservices.go | 8 +- internal/services/trashcart_service.go | 10 +- model/collector_model.go | 31 +- model/trashcart_model.go | 13 +- presentation/collector_route.go | 43 ++- presentation/requestpickup_route.go | 73 ++--- router/setup_routes.go.go | 2 +- 16 files changed, 967 insertions(+), 545 deletions(-) diff --git a/config/database.go b/config/database.go index 95c9859..d19ebbd 100644 --- a/config/database.go +++ b/config/database.go @@ -43,6 +43,7 @@ func ConnectDatabase() { // =>user preparation<= &model.User{}, &model.Collector{}, + &model.AvaibleTrashByCollector{}, &model.Role{}, &model.UserPin{}, &model.Address{}, diff --git a/dto/collector_dto.go b/dto/collector_dto.go index 21e89c5..a48033a 100644 --- a/dto/collector_dto.go +++ b/dto/collector_dto.go @@ -1,10 +1,22 @@ package dto -import "strings" +import ( + "fmt" + "strings" +) type RequestCollectorDTO struct { - UserId string `json:"user_id"` - AddressId string `json:"address_id"` + AddressId string `json:"address_id"` + AvaibleTrashbyCollector []RequestAvaibleTrashbyCollector `json:"avaible_trash"` +} + +type RequestAvaibleTrashbyCollector struct { + TrashId string `json:"trash_id"` + TrashPrice float32 `json:"trash_price"` +} + +type RequestAddAvaibleTrash struct { + AvaibleTrash []RequestAvaibleTrashbyCollector `json:"avaible_trash"` } type SelectCollectorRequest struct { @@ -24,25 +36,65 @@ func (r *SelectCollectorRequest) ValidateSelectCollectorRequest() (map[string][] return nil, true } -type ResponseCollectorDTO struct { - ID string `json:"collector_id"` - UserId string `json:"user_id"` - User []UserResponseDTO `json:"user,omitempty"` - AddressId string `json:"address_id"` - Address []AddressResponseDTO `json:"address,omitempty"` - JobStatus *string `json:"job_status,omitempty"` - Rating float32 `json:"rating"` +func (r *RequestAddAvaibleTrash) ValidateRequestAddAvaibleTrash() (map[string][]string, bool) { + errors := make(map[string][]string) + + if len(r.AvaibleTrash) == 0 { + errors["avaible_trash"] = append(errors["avaible_trash"], "tidak boleh kosong") + } + + for i, trash := range r.AvaibleTrash { + if strings.TrimSpace(trash.TrashId) == "" { + errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)], "trash_id tidak boleh kosong") + } + if trash.TrashPrice <= 0 { + errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)], "trash_price harus lebih dari 0") + } + } + + if len(errors) > 0 { + return errors, false + } + return nil, true } -func (r *RequestCollectorDTO) ValidateRequestColector() (map[string][]string, bool) { +type ResponseCollectorDTO struct { + ID string `json:"collector_id"` + UserId string `json:"user_id"` + User *UserResponseDTO `json:"user,omitempty"` + AddressId string `json:"address_id"` + Address *AddressResponseDTO `json:"address,omitempty"` + JobStatus *string `json:"job_status,omitempty"` + Rating float32 `json:"rating"` + AvaibleTrashbyCollector []ResponseAvaibleTrashByCollector `json:"avaible_trash"` +} + +type ResponseAvaibleTrashByCollector struct { + ID string `json:"id"` + TrashId string `json:"trash_id"` + TrashName string `json:"trash_name"` + TrashIcon string `json:"trash_icon"` + TrashPrice float32 `json:"trash_price"` +} + +func (r *RequestCollectorDTO) ValidateRequestCollector() (map[string][]string, bool) { errors := make(map[string][]string) if strings.TrimSpace(r.AddressId) == "" { errors["address_id"] = append(errors["address_id"], "address_id harus diisi") } + + for i, trash := range r.AvaibleTrashbyCollector { + if strings.TrimSpace(trash.TrashId) == "" { + errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)], "trash_id tidak boleh kosong") + } + if trash.TrashPrice <= 0 { + errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)], "trash_price harus lebih dari 0") + } + } + if len(errors) > 0 { return errors, false } - return nil, true } diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index e955600..3f4a541 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -33,14 +33,14 @@ type CartItemResponse struct { } type RequestCartItems struct { - TrashID string `json:"trashid"` - Amount float32 `json:"amount"` + TrashCategoryID string `json:"trashid"` + Amount float32 `json:"amount"` } func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.TrashID) == "" { + if strings.TrimSpace(r.TrashCategoryID) == "" { errors["trashid"] = append(errors["trashid"], "trashid is required") } @@ -58,7 +58,7 @@ type BulkRequestCartItems struct { func (b *BulkRequestCartItems) Validate() (map[string][]string, bool) { errors := make(map[string][]string) for i, item := range b.Items { - if strings.TrimSpace(item.TrashID) == "" { + if strings.TrimSpace(item.TrashCategoryID) == "" { errors[fmt.Sprintf("items[%d].trashid", i)] = append(errors[fmt.Sprintf("items[%d].trashid", i)], "trashid is required") } } diff --git a/internal/handler/collector_handler.go b/internal/handler/collector_handler.go index 355c8be..aad6c5d 100644 --- a/internal/handler/collector_handler.go +++ b/internal/handler/collector_handler.go @@ -1,7 +1,7 @@ package handler import ( - "fmt" + "context" "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -9,62 +9,244 @@ import ( "github.com/gofiber/fiber/v2" ) -type CollectorHandler struct { +type CollectorHandler interface { + CreateCollector(c *fiber.Ctx) error + AddTrashToCollector(c *fiber.Ctx) error + GetCollectorByID(c *fiber.Ctx) error + GetCollectorByUserID(c *fiber.Ctx) error + UpdateCollector(c *fiber.Ctx) error + UpdateJobStatus(c *fiber.Ctx) error + UpdateTrash(c *fiber.Ctx) error + DeleteTrash(c *fiber.Ctx) error +} +type collectorHandler struct { service services.CollectorService } -func NewCollectorHandler(service services.CollectorService) *CollectorHandler { - return &CollectorHandler{service} +func NewCollectorHandler(service services.CollectorService) CollectorHandler { + return &collectorHandler{service: service} } -func (h *CollectorHandler) ConfirmRequestPickup(c *fiber.Ctx) error { +// func (h *CollectorHandler) ConfirmRequestPickup(c *fiber.Ctx) error { - collectorId, ok := c.Locals("userID").(string) - if !ok || collectorId == "" { +// collectorId, ok := c.Locals("userID").(string) +// if !ok || collectorId == "" { +// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") +// } + +// requestPickupId := c.Params("id") +// if requestPickupId == "" { +// return utils.ErrorResponse(c, "RequestPickup ID is required") +// } + +// req, err := h.service.ConfirmRequestPickup(requestPickupId, collectorId) +// if err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } + +// return utils.SuccessResponse(c, req, "Request pickup confirmed successfully") +// } + +// func (h *CollectorHandler) GetAvaibleCollector(c *fiber.Ctx) error { + +// userId := c.Locals("userID").(string) + +// requests, err := h.service.FindCollectorsNearby(userId) +// if err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } + +// return utils.SuccessResponse(c, requests, "menampilkan data collector terdekat") +// } + +// func (h *CollectorHandler) ConfirmRequestManualPickup(c *fiber.Ctx) error { +// userId := c.Locals("userID").(string) +// requestId := c.Params("request_id") +// if requestId == "" { +// fmt.Println("requestid dibutuhkan") +// } + +// var request dto.SelectCollectorRequest +// if err := c.BodyParser(&request); err != nil { +// return fmt.Errorf("error parsing request body: %v", err) +// } + +// message, err := h.service.ConfirmRequestManualPickup(requestId, userId) +// if err != nil { +// return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error confirming pickup: %v", err)) +// } + +// return utils.SuccessResponse(c, message, "berhasil konfirmasi request") +// } + +func (h *collectorHandler) CreateCollector(c *fiber.Ctx) error { + var req dto.RequestCollectorDTO + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if errs, valid := req.ValidateRequestCollector(); !valid { + return utils.ValidationErrorResponse(c, errs) + } + + userID := c.Locals("userID").(string) + err := h.service.CreateCollector(context.Background(), userID, req) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.CreateResponse(c, nil, "Collector berhasil dibuat") +} + +// POST /collectors/:id/trash +func (h *collectorHandler) AddTrashToCollector(c *fiber.Ctx) error { + collectorID := c.Params("id") + var req dto.RequestAddAvaibleTrash + + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if errs, valid := req.ValidateRequestAddAvaibleTrash(); !valid { + return utils.ValidationErrorResponse(c, errs) + } + + err := h.service.AddTrashToCollector(context.Background(), collectorID, req) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Trash berhasil ditambahkan") +} + +// GET /collectors/:id +func (h *collectorHandler) GetCollectorByID(c *fiber.Ctx) error { + collectorID := c.Params("id") + result, err := h.service.GetCollectorByID(context.Background(), collectorID) + if err != nil { + return utils.ErrorResponse(c, "Collector tidak ditemukan") + } + return utils.SuccessResponse(c, result, "Data collector berhasil diambil") +} +func (h *collectorHandler) GetCollectorByUserID(c *fiber.Ctx) error { + // collectorID := c.Params("id") + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") } - requestPickupId := c.Params("id") - if requestPickupId == "" { - return utils.ErrorResponse(c, "RequestPickup ID is required") - } - - req, err := h.service.ConfirmRequestPickup(requestPickupId, collectorId) + result, err := h.service.GetCollectorByUserID(context.Background(), userID) if err != nil { - return utils.ErrorResponse(c, err.Error()) + return utils.ErrorResponse(c, "Collector tidak ditemukan") } - - return utils.SuccessResponse(c, req, "Request pickup confirmed successfully") + return utils.SuccessResponse(c, result, "Data collector berhasil diambil") } -func (h *CollectorHandler) GetAvaibleCollector(c *fiber.Ctx) error { - - userId := c.Locals("userID").(string) - - requests, err := h.service.FindCollectorsNearby(userId) - if err != nil { - return utils.ErrorResponse(c, err.Error()) +// PATCH /collectors/:id +func (h *collectorHandler) UpdateCollector(c *fiber.Ctx) error { + collectorID := c.Params("id") + var req struct { + JobStatus *string `json:"job_status"` + Rating float32 `json:"rating"` + AddressID string `json:"address_id"` } - return utils.SuccessResponse(c, requests, "menampilkan data collector terdekat") + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if req.AddressID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "address_id": {"tidak boleh kosong"}, + }) + } + + err := h.service.UpdateCollector(context.Background(), collectorID, req.JobStatus, req.Rating, req.AddressID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Collector berhasil diperbarui") } -func (h *CollectorHandler) ConfirmRequestManualPickup(c *fiber.Ctx) error { - userId := c.Locals("userID").(string) - requestId := c.Params("request_id") - if requestId == "" { - fmt.Println("requestid dibutuhkan") +func (h *collectorHandler) UpdateJobStatus(c *fiber.Ctx) error { + collectorID := c.Params("id") + var req struct { + JobStatus string `json:"job_status"` } - var request dto.SelectCollectorRequest - if err := c.BodyParser(&request); err != nil { - return fmt.Errorf("error parsing request body: %v", err) + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) } - message, err := h.service.ConfirmRequestManualPickup(requestId, userId) + if req.JobStatus != "active" && req.JobStatus != "inactive" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "job_status": {"harus bernilai 'active' atau 'inactive'"}, + }) + } + + err := h.service.UpdateCollector(c.Context(), collectorID, &req.JobStatus, 0, "") if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error confirming pickup: %v", err)) + return utils.InternalServerErrorResponse(c, err.Error()) } - return utils.SuccessResponse(c, message, "berhasil konfirmasi request") + return utils.SuccessResponse(c, nil, "Status collector berhasil diperbarui") +} + + +// PATCH /collectors/:id/trash +func (h *collectorHandler) UpdateTrash(c *fiber.Ctx) error { + collectorID := c.Params("id") + var req []dto.RequestAvaibleTrashbyCollector + + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + for i, t := range req { + if t.TrashId == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "trash_id": {t.TrashId, "trash_id tidak boleh kosong pada item ke " + string(rune(i))}, + }) + } + if t.TrashPrice <= 0 { + return utils.ValidationErrorResponse(c, map[string][]string{ + "trash_price": {"trash_price harus lebih dari 0 pada item ke " + string(rune(i))}, + }) + } + } + + err := h.service.UpdateAvaibleTrashByCollector(context.Background(), collectorID, req) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Trash berhasil diperbarui") +} + +// DELETE /collectors/trash/:id +func (h *collectorHandler) DeleteTrash(c *fiber.Ctx) error { + trashID := c.Params("id") + if trashID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "trash_id": {"tidak boleh kosong"}, + }) + } + + err := h.service.DeleteAvaibleTrash(context.Background(), trashID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Trash berhasil dihapus") } diff --git a/internal/handler/requestpickup_handler.go b/internal/handler/requestpickup_handler.go index 0984cd2..4ae4d72 100644 --- a/internal/handler/requestpickup_handler.go +++ b/internal/handler/requestpickup_handler.go @@ -1,92 +1,92 @@ package handler -import ( - "fmt" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" +// import ( +// "fmt" +// "rijig/dto" +// "rijig/internal/services" +// "rijig/utils" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -type RequestPickupHandler struct { - service services.RequestPickupService -} +// type RequestPickupHandler struct { +// service services.RequestPickupService +// } -func NewRequestPickupHandler(service services.RequestPickupService) *RequestPickupHandler { - return &RequestPickupHandler{service: service} -} +// func NewRequestPickupHandler(service services.RequestPickupService) *RequestPickupHandler { +// return &RequestPickupHandler{service: service} +// } -func (h *RequestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } +// func (h *RequestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { +// userID, ok := c.Locals("userID").(string) +// if !ok || userID == "" { +// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") +// } - var request dto.RequestPickup +// var request dto.RequestPickup - if err := c.BodyParser(&request); err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") - } +// if err := c.BodyParser(&request); err != nil { +// return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") +// } - errors, valid := request.ValidateRequestPickup() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } +// errors, valid := request.ValidateRequestPickup() +// if !valid { +// return utils.ValidationErrorResponse(c, errors) +// } - response, err := h.service.CreateRequestPickup(request, userID) - if err != nil { - return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating request pickup: %v", err)) - } +// response, err := h.service.CreateRequestPickup(request, userID) +// if err != nil { +// return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating request pickup: %v", err)) +// } - return utils.SuccessResponse(c, response, "Request pickup created successfully") -} +// return utils.SuccessResponse(c, response, "Request pickup created successfully") +// } -func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { - id := c.Params("id") +// func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { +// id := c.Params("id") - response, err := h.service.GetRequestPickupByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) - } +// response, err := h.service.GetRequestPickupByID(id) +// if err != nil { +// return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) +// } - return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") -} +// return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") +// } -func (h *RequestPickupHandler) GetRequestPickups(c *fiber.Ctx) error { +// func (h *RequestPickupHandler) GetRequestPickups(c *fiber.Ctx) error { - collectorId := c.Locals("userID").(string) +// collectorId := c.Locals("userID").(string) - requests, err := h.service.GetRequestPickupsForCollector(collectorId) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } +// requests, err := h.service.GetRequestPickupsForCollector(collectorId) +// if err != nil { +// return utils.ErrorResponse(c, err.Error()) +// } - return utils.SuccessResponse(c, requests, "Automatic request pickups retrieved successfully") -} +// return utils.SuccessResponse(c, requests, "Automatic request pickups retrieved successfully") +// } -func (h *RequestPickupHandler) AssignCollectorToRequest(c *fiber.Ctx) error { - userId, ok := c.Locals("userID").(string) - if !ok || userId == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } +// func (h *RequestPickupHandler) AssignCollectorToRequest(c *fiber.Ctx) error { +// userId, ok := c.Locals("userID").(string) +// if !ok || userId == "" { +// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") +// } - var request dto.SelectCollectorRequest - errors, valid := request.ValidateSelectCollectorRequest() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } +// var request dto.SelectCollectorRequest +// errors, valid := request.ValidateSelectCollectorRequest() +// if !valid { +// return utils.ValidationErrorResponse(c, errors) +// } - if err := c.BodyParser(&request); err != nil { - return fmt.Errorf("error parsing request body: %v", err) - } +// if err := c.BodyParser(&request); err != nil { +// return fmt.Errorf("error parsing request body: %v", err) +// } - err := h.service.SelectCollectorInRequest(userId, request.Collector_id) - if err != nil { +// err := h.service.SelectCollectorInRequest(userId, request.Collector_id) +// if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error assigning collector: %v", err)) - } +// return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error assigning collector: %v", err)) +// } - return utils.GenericResponse(c, fiber.StatusOK, "berhasil memilih collector") -} +// return utils.GenericResponse(c, fiber.StatusOK, "berhasil memilih collector") +// } diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go index 5f0ef53..9340893 100644 --- a/internal/repositories/collector_repo.go +++ b/internal/repositories/collector_repo.go @@ -1,115 +1,177 @@ package repositories import ( + "context" "errors" - "fmt" - "log" - "rijig/model" - "gorm.io/gorm" + // "fmt" + + // "log" + "rijig/config" + "rijig/model" + // "gorm.io/gorm" ) type CollectorRepository interface { - FindActiveCollectors() ([]model.Collector, error) - FindCollectorById(collector_id string) (*model.Collector, error) - FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) - CreateCollector(collector *model.Collector) error - UpdateCollector(userId string, jobStatus string) (*model.Collector, error) - // FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) + // FindActiveCollectors() ([]model.Collector, error) + // FindCollectorById(collector_id string) (*model.Collector, error) + // FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) + // CreateCollector(collector *model.Collector) error + // UpdateCollector(userId string, jobStatus string) (*model.Collector, error) + + CreateCollector(ctx context.Context, collector *model.Collector) error + AddAvaibleTrash(ctx context.Context, trashItems []model.AvaibleTrashByCollector) error + GetCollectorByID(ctx context.Context, collectorID string) (*model.Collector, error) + GetCollectorByUserID(ctx context.Context, userID string) (*model.Collector, error) + GetTrashItemByID(ctx context.Context, id string) (*model.AvaibleTrashByCollector, error) + UpdateCollector(ctx context.Context, collector *model.Collector, updates map[string]interface{}) error + UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []model.AvaibleTrashByCollector) error + DeleteAvaibleTrash(ctx context.Context, trashID string) error } type collectorRepository struct { - DB *gorm.DB + // DB *gorm.DB } -func NewCollectorRepository(db *gorm.DB) CollectorRepository { - return &collectorRepository{DB: db} +// func NewCollectorRepository(db *gorm.DB) CollectorRepository { +// return &collectorRepository{DB: db} +// } +func NewCollectorRepository() CollectorRepository { + return &collectorRepository{} } -func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) { - var collectors []model.Collector +// func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) { +// var collectors []model.Collector - err := r.DB.Preload("Address").Where("job_status = ?", "active").First(&collectors).Error - if err != nil { - return nil, fmt.Errorf("failed to fetch active collectors: %v", err) +// err := r.DB.Preload("Address").Where("job_status = ?", "active").First(&collectors).Error +// if err != nil { +// return nil, fmt.Errorf("failed to fetch active collectors: %v", err) +// } + +// return collectors, nil +// } + +// func (r *collectorRepository) FindCollectorById(collector_id string) (*model.Collector, error) { +// var collector model.Collector +// err := r.DB.Preload("Address").Where("user_id = ?", collector_id).First(&collector).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching collector: %v", err) +// } +// fmt.Printf("menampilkan data collector %v", &collector) +// return &collector, nil +// } + +// func (r *collectorRepository) FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) { +// var collector model.Collector +// err := r.DB.Where("user_id = ?", collector_id).First(&collector).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching collector: %v", err) +// } +// fmt.Printf("menampilkan data collector %v", &collector) +// return &collector, nil +// } + +// func (r *collectorRepository) CreateCollector(collector *model.Collector) error { +// if err := r.DB.Create(collector).Error; err != nil { +// return fmt.Errorf("failed to create collector: %v", err) +// } +// return nil +// } + +// func (r *collectorRepository) UpdateCollector(userId string, jobStatus string) (*model.Collector, error) { +// var existingCollector model.Collector + +// if err := r.DB.Where("user_id = ?", userId).First(&existingCollector).Error; err != nil { +// if errors.Is(err, gorm.ErrRecordNotFound) { +// return nil, fmt.Errorf("collector dengan user_id %s tidak ditemukan", userId) +// } +// log.Printf("Gagal mencari collector: %v", err) +// return nil, fmt.Errorf("gagal fetching collector: %w", err) +// } + +// if jobStatus != "active" && jobStatus != "nonactive" { +// return nil, fmt.Errorf("invalid job status: %v", jobStatus) +// } + +// if err := r.DB.Model(&existingCollector).Update("jobstatus", jobStatus).Error; err != nil { +// log.Printf("Gagal mengupdate data collector: %v", err) +// return nil, fmt.Errorf("gagal mengupdate job status untuk collector: %w", err) +// } + +// return &existingCollector, nil +// } + +func (r *collectorRepository) CreateCollector(ctx context.Context, collector *model.Collector) error { + return config.DB.WithContext(ctx).Create(collector).Error +} + +func (r *collectorRepository) AddAvaibleTrash(ctx context.Context, trashItems []model.AvaibleTrashByCollector) error { + if len(trashItems) == 0 { + return nil } - - return collectors, nil + return config.DB.WithContext(ctx).Create(&trashItems).Error } -func (r *collectorRepository) FindCollectorById(collector_id string) (*model.Collector, error) { +func (r *collectorRepository) GetCollectorByID(ctx context.Context, collectorID string) (*model.Collector, error) { var collector model.Collector - err := r.DB.Preload("Address").Where("user_id = ?", collector_id).First(&collector).Error + err := config.DB.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("AvaibleTrashByCollector.TrashCategory"). + First(&collector, "id = ?", collectorID).Error + if err != nil { - return nil, fmt.Errorf("error fetching collector: %v", err) + return nil, err } - fmt.Printf("menampilkan data collector %v", &collector) return &collector, nil } -func (r *collectorRepository) FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) { +func (r *collectorRepository) GetCollectorByUserID(ctx context.Context, userID string) (*model.Collector, error) { var collector model.Collector - err := r.DB.Where("user_id = ?", collector_id).First(&collector).Error + err := config.DB.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("AvaibleTrashByCollector.TrashCategory"). + First(&collector, "user_id = ?", userID).Error + if err != nil { - return nil, fmt.Errorf("error fetching collector: %v", err) + return nil, err } - fmt.Printf("menampilkan data collector %v", &collector) return &collector, nil } -func (r *collectorRepository) CreateCollector(collector *model.Collector) error { - if err := r.DB.Create(collector).Error; err != nil { - return fmt.Errorf("failed to create collector: %v", err) +func (r *collectorRepository) GetTrashItemByID(ctx context.Context, id string) (*model.AvaibleTrashByCollector, error) { + var item model.AvaibleTrashByCollector + if err := config.DB.WithContext(ctx).First(&item, "id = ?", id).Error; err != nil { + return nil, err + } + return &item, nil +} + +func (r *collectorRepository) UpdateCollector(ctx context.Context, collector *model.Collector, updates map[string]interface{}) error { + return config.DB.WithContext(ctx). + Model(&model.Collector{}). + Where("id = ?", collector.ID). + Updates(updates).Error +} + +func (r *collectorRepository) UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []model.AvaibleTrashByCollector) error { + for _, trash := range updatedTrash { + err := config.DB.WithContext(ctx). + Model(&model.AvaibleTrashByCollector{}). + Where("collector_id = ? AND trash_category_id = ?", collectorID, trash.TrashCategoryID). + Update("price", trash.Price).Error + if err != nil { + return err + } } return nil } -func (r *collectorRepository) UpdateCollector(userId string, jobStatus string) (*model.Collector, error) { - var existingCollector model.Collector - - if err := r.DB.Where("user_id = ?", userId).First(&existingCollector).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("collector dengan user_id %s tidak ditemukan", userId) - } - log.Printf("Gagal mencari collector: %v", err) - return nil, fmt.Errorf("gagal fetching collector: %w", err) +func (r *collectorRepository) DeleteAvaibleTrash(ctx context.Context, trashID string) error { + if trashID == "" { + return errors.New("trash_id cannot be empty") } - - if jobStatus != "active" && jobStatus != "nonactive" { - return nil, fmt.Errorf("invalid job status: %v", jobStatus) - } - - if err := r.DB.Model(&existingCollector).Update("jobstatus", jobStatus).Error; err != nil { - log.Printf("Gagal mengupdate data collector: %v", err) - return nil, fmt.Errorf("gagal mengupdate job status untuk collector: %w", err) - } - - return &existingCollector, nil + return config.DB.WithContext(ctx). + Delete(&model.AvaibleTrashByCollector{}, "id = ?", trashID).Error } - -// // #====experimen====# -// func (r *collectorRepository) FindAllAutomaticMethodRequestWithDistance(requestMethod, statuspickup string, collectorLat, collectorLon float64, maxDistance float64) ([]model.RequestPickup, error) { -// var requests []model.RequestPickup - -// err := r.DB.Preload("RequestItems"). -// Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup). -// Find(&requests).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching request pickups with request_method '%s' and status '%s': %v", requestMethod, statuspickup, err) -// } - -// var nearbyRequests []model.RequestPickup -// for _, request := range requests { -// address := request.Address - -// requestCoord := utils.Coord{Lat: address.Latitude, Lon: address.Longitude} -// collectorCoord := utils.Coord{Lat: collectorLat, Lon: collectorLon} -// _, km := utils.Distance(requestCoord, collectorCoord) - -// if km <= maxDistance { -// nearbyRequests = append(nearbyRequests, request) -// } -// } - -// return nearbyRequests, nil -// } diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 99a5e1d..4b88d35 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -1,10 +1,12 @@ package repositories import ( + "context" "errors" "fmt" "log" + "rijig/config" "rijig/model" "gorm.io/gorm" @@ -22,6 +24,7 @@ type TrashRepository interface { UpdateCategoryName(id string, newName string) error UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) UpdateTrashDetail(id string, description string, price float64) error + UpdateEstimatedPrice(ctx context.Context, trashCategoryID string) error DeleteCategory(id string) error DeleteTrashDetail(id string) error } @@ -131,6 +134,23 @@ func (r *trashRepository) UpdateTrashDetail(id string, description string, price return nil } +func (r *trashRepository) UpdateEstimatedPrice(ctx context.Context, trashCategoryID string) error { + var avg float64 + + err := config.DB.WithContext(ctx). + Model(&model.AvaibleTrashByCollector{}). + Where("trash_category_id = ?", trashCategoryID). + Select("AVG(price)").Scan(&avg).Error + if err != nil { + return err + } + + return config.DB.WithContext(ctx). + Model(&model.TrashCategory{}). + Where("id = ?", trashCategoryID). + Update("estimated_price", avg).Error +} + func (r *trashRepository) DeleteCategory(id string) error { if err := r.DB.Delete(&model.TrashCategory{}, "id = ?", id).Error; err != nil { return fmt.Errorf("failed to delete category: %v", err) diff --git a/internal/services/collector_service.go b/internal/services/collector_service.go index 9cf745e..0984d31 100644 --- a/internal/services/collector_service.go +++ b/internal/services/collector_service.go @@ -1,151 +1,224 @@ package services import ( - "fmt" + "context" + "errors" + "rijig/dto" "rijig/internal/repositories" - "rijig/utils" - "time" + "rijig/model" ) type CollectorService interface { - FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) - ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) - ConfirmRequestManualPickup(requestId, collectorId string) (any, error) + CreateCollector(ctx context.Context, userID string, req dto.RequestCollectorDTO) error + AddTrashToCollector(ctx context.Context, collectorID string, req dto.RequestAddAvaibleTrash) error + GetCollectorByID(ctx context.Context, collectorID string) (*dto.ResponseCollectorDTO, error) + GetCollectorByUserID(ctx context.Context, userID string) (*dto.ResponseCollectorDTO, error) + UpdateCollector(ctx context.Context, collectorID string, jobStatus *string, rating float32, addressID string) error + UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []dto.RequestAvaibleTrashbyCollector) error + DeleteAvaibleTrash(ctx context.Context, trashID string) error } type collectorService struct { - repo repositories.CollectorRepository - repoColl repositories.RequestPickupRepository - repoAddress repositories.AddressRepository - repoUser repositories.UserProfilRepository + repo repositories.CollectorRepository + trashRepo repositories.TrashRepository } -func NewCollectorService(repo repositories.CollectorRepository, - repoColl repositories.RequestPickupRepository, - repoAddress repositories.AddressRepository, - repoUser repositories.UserProfilRepository) CollectorService { - return &collectorService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoUser: repoUser} +func NewCollectorService(repo repositories.CollectorRepository, trashRepo repositories.TrashRepository, + +) CollectorService { + + return &collectorService{repo: repo, trashRepo: trashRepo} } -func (s *collectorService) FindCollectorsNearby(userId string) ([]dto.ResponseCollectorDTO, error) { - collectors, err := s.repo.FindActiveCollectors() - if err != nil { - return nil, fmt.Errorf("error fetching active collectors: %v", err) +func (s *collectorService) CreateCollector(ctx context.Context, userID string, req dto.RequestCollectorDTO) error { + collector := &model.Collector{ + UserID: userID, + AddressID: req.AddressId, + JobStatus: "inactive", + Rating: 5, } - var avaibleCollectResp []dto.ResponseCollectorDTO - - for _, collector := range collectors { - - request, err := s.repoColl.FindRequestPickupByAddressAndStatus(userId, "waiting_collector", "otomatis") - if err != nil { - return nil, fmt.Errorf("gagal mendapatkan data request pickup dengan userid: %v", err) - } - - _, distance := utils.Distance( - utils.Coord{Lat: request.Address.Latitude, Lon: request.Address.Longitude}, - utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, - ) - - if distance <= 20 { - - mappedRequest := dto.ResponseCollectorDTO{ - ID: collector.ID, - UserId: collector.UserID, - AddressId: collector.AddressId, - - Rating: collector.Rating, - } - - user, err := s.repoUser.FindByID(collector.UserID) - if err != nil { - return nil, fmt.Errorf("error fetching user data: %v", err) - } - mappedRequest.User = []dto.UserResponseDTO{ - { - Name: user.Name, - Phone: user.Phone, - }, - } - - address, err := s.repoAddress.FindAddressByID(collector.AddressId) - if err != nil { - return nil, fmt.Errorf("error fetching address data: %v", err) - } - mappedRequest.Address = []dto.AddressResponseDTO{ - { - District: address.District, - Village: address.Village, - Detail: address.Detail, - }, - } - - avaibleCollectResp = append(avaibleCollectResp, mappedRequest) - } + if err := s.repo.CreateCollector(ctx, collector); err != nil { + return err } - return avaibleCollectResp, nil + var trashItems []model.AvaibleTrashByCollector + for _, item := range req.AvaibleTrashbyCollector { + trashItems = append(trashItems, model.AvaibleTrashByCollector{ + CollectorID: collector.ID, + TrashCategoryID: item.TrashId, + Price: item.TrashPrice, + }) + } + + if err := s.repo.AddAvaibleTrash(ctx, trashItems); err != nil { + return err + } + + for _, t := range trashItems { + _ = s.trashRepo.UpdateEstimatedPrice(ctx, t.TrashCategoryID) + } + + return nil } -func (s *collectorService) ConfirmRequestPickup(requestId, collectorId string) (*dto.ResponseRequestPickup, error) { - request, err := s.repoColl.FindRequestPickupByID(requestId) - if err != nil { - return nil, fmt.Errorf("request pickup not found: %v", err) +func (s *collectorService) AddTrashToCollector(ctx context.Context, collectorID string, req dto.RequestAddAvaibleTrash) error { + var trashItems []model.AvaibleTrashByCollector + for _, item := range req.AvaibleTrash { + trashItems = append(trashItems, model.AvaibleTrashByCollector{ + CollectorID: collectorID, + TrashCategoryID: item.TrashId, + Price: item.TrashPrice, + }) + } + if err := s.repo.AddAvaibleTrash(ctx, trashItems); err != nil { + return err } - if request.StatusPickup != "waiting_collector" { - return nil, fmt.Errorf("pickup request is not in 'waiting_collector' status") + for _, t := range trashItems { + _ = s.trashRepo.UpdateEstimatedPrice(ctx, t.TrashCategoryID) } - collector, err := s.repo.FindCollectorById(collectorId) - if err != nil { - return nil, fmt.Errorf("collector tidak ditemukan: %v", err) - } - - request.StatusPickup = "confirmed" - request.CollectorID = &collector.ID - *request.ConfirmedByCollectorAt = time.Now() - - err = s.repoColl.UpdateRequestPickup(requestId, request) - if err != nil { - return nil, fmt.Errorf("failed to update request pickup: %v", err) - } - - confirmedAt, _ := utils.FormatDateToIndonesianFormat(*request.ConfirmedByCollectorAt) - - response := dto.ResponseRequestPickup{ - StatusPickup: request.StatusPickup, - CollectorID: *request.CollectorID, - ConfirmedByCollectorAt: confirmedAt, - } - - return &response, nil + return nil } -func (s *collectorService) ConfirmRequestManualPickup(requestId, collectorId string) (any, error) { - - request, err := s.repoColl.FindRequestPickupByID(requestId) +func (s *collectorService) GetCollectorByID(ctx context.Context, collectorID string) (*dto.ResponseCollectorDTO, error) { + collector, err := s.repo.GetCollectorByID(ctx, collectorID) if err != nil { - return nil, fmt.Errorf("collector tidak ditemukan: %v", err) + return nil, err } - coll, err := s.repo.FindCollectorByIdWithoutAddr(collectorId) + response := &dto.ResponseCollectorDTO{ + ID: collector.ID, + UserId: collector.UserID, + AddressId: collector.AddressID, + JobStatus: &collector.JobStatus, + Rating: collector.Rating, + User: &dto.UserResponseDTO{ + ID: collector.User.ID, + Name: collector.User.Name, + Phone: collector.User.Phone, + }, + Address: &dto.AddressResponseDTO{ + Province: collector.Address.Province, + District: collector.Address.District, + Regency: collector.Address.Regency, + Village: collector.Address.Village, + PostalCode: collector.Address.PostalCode, + Latitude: collector.Address.Latitude, + Longitude: collector.Address.Longitude, + }, + } + + for _, item := range collector.AvaibleTrashByCollector { + response.AvaibleTrashbyCollector = append(response.AvaibleTrashbyCollector, dto.ResponseAvaibleTrashByCollector{ + ID: item.ID, + TrashId: item.TrashCategory.ID, + TrashName: item.TrashCategory.Name, + TrashIcon: item.TrashCategory.Icon, + TrashPrice: item.Price, + }) + } + + return response, nil +} + +func (s *collectorService) GetCollectorByUserID(ctx context.Context, userID string) (*dto.ResponseCollectorDTO, error) { + collector, err := s.repo.GetCollectorByUserID(ctx, userID) if err != nil { - return nil, fmt.Errorf("%v", err) + return nil, err } - if coll.ID != *request.CollectorID { - return nil, fmt.Errorf("collectorid tidak sesuai dengan request") + response := &dto.ResponseCollectorDTO{ + ID: collector.ID, + UserId: collector.UserID, + AddressId: collector.AddressID, + JobStatus: &collector.JobStatus, + Rating: collector.Rating, + User: &dto.UserResponseDTO{ + ID: collector.User.ID, + Name: collector.User.Name, + Phone: collector.User.Phone, + }, + Address: &dto.AddressResponseDTO{ + Province: collector.Address.Province, + District: collector.Address.District, + Regency: collector.Address.Regency, + Village: collector.Address.Village, + PostalCode: collector.Address.PostalCode, + Latitude: collector.Address.Latitude, + Longitude: collector.Address.Longitude, + }, } - request.StatusPickup = "confirmed" - *request.ConfirmedByCollectorAt = time.Now() + for _, item := range collector.AvaibleTrashByCollector { + response.AvaibleTrashbyCollector = append(response.AvaibleTrashbyCollector, dto.ResponseAvaibleTrashByCollector{ + ID: item.ID, + TrashId: item.TrashCategory.ID, + TrashName: item.TrashCategory.Name, + TrashIcon: item.TrashCategory.Icon, + TrashPrice: item.Price, + }) + } - err = s.repoColl.UpdateRequestPickup(requestId, request) + return response, nil +} + +func (s *collectorService) UpdateCollector(ctx context.Context, collectorID string, jobStatus *string, rating float32, addressID string) error { + updates := make(map[string]interface{}) + + if jobStatus != nil { + updates["job_status"] = *jobStatus + } + if rating > 0 { + updates["rating"] = rating + } + if addressID != "" { + updates["address_id"] = addressID + } + + if len(updates) == 0 { + return errors.New("tidak ada data yang diubah") + } + + return s.repo.UpdateCollector(ctx, &model.Collector{ID: collectorID}, updates) +} + +func (s *collectorService) UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []dto.RequestAvaibleTrashbyCollector) error { + var updated []model.AvaibleTrashByCollector + for _, item := range updatedTrash { + updated = append(updated, model.AvaibleTrashByCollector{ + CollectorID: collectorID, + TrashCategoryID: item.TrashId, + Price: item.TrashPrice, + }) + } + + if err := s.repo.UpdateAvaibleTrashByCollector(ctx, collectorID, updated); err != nil { + return err + } + + for _, item := range updated { + _ = s.trashRepo.UpdateEstimatedPrice(ctx, item.TrashCategoryID) + } + + return nil +} + +func (s *collectorService) DeleteAvaibleTrash(ctx context.Context, trashID string) error { + if trashID == "" { + return errors.New("trash_id tidak boleh kosong") + } + + item, err := s.repo.GetTrashItemByID(ctx, trashID) if err != nil { - return nil, fmt.Errorf("failed to update request pickup: %v", err) + return err } - return "berhasil konfirmasi request pickup", nil -} \ No newline at end of file + if err := s.repo.DeleteAvaibleTrash(ctx, trashID); err != nil { + return err + } + + return s.trashRepo.UpdateEstimatedPrice(ctx, item.TrashCategoryID) +} diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go index f79996d..8656d06 100644 --- a/internal/services/requestpickup_service.go +++ b/internal/services/requestpickup_service.go @@ -9,11 +9,11 @@ import ( ) type RequestPickupService interface { - CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) - GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) - GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) - GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) - SelectCollectorInRequest(userId, collectorId string) error + // CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) + // GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) + // GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) + // GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) + // SelectCollectorInRequest(userId, collectorId string) error } type requestPickupService struct { @@ -152,198 +152,198 @@ func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.Respon return response, nil } -func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { - requests, err := s.repo.GetAutomaticRequestPickupsForCollector() - if err != nil { - return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) - } +// func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { +// requests, err := s.repo.GetAutomaticRequestPickupsForCollector() +// if err != nil { +// return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) +// } - var response []dto.ResponseRequestPickup +// var response []dto.ResponseRequestPickup - for _, req := range requests { +// for _, req := range requests { - collector, err := s.repoColl.FindCollectorById(collectorId) - if err != nil { - return nil, fmt.Errorf("error fetching collector data: %v", err) - } +// collector, err := s.repoColl.FindCollectorById(collectorId) +// if err != nil { +// return nil, fmt.Errorf("error fetching collector data: %v", err) +// } - _, distance := utils.Distance( - utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, - utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, - ) +// _, distance := utils.Distance( +// utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, +// utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, +// ) - if distance <= 20 { +// if distance <= 20 { - mappedRequest := dto.ResponseRequestPickup{ - ID: req.ID, - UserId: req.UserId, - AddressID: req.AddressId, - EvidenceImage: req.EvidenceImage, - StatusPickup: req.StatusPickup, - CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), - UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), - } +// mappedRequest := dto.ResponseRequestPickup{ +// ID: req.ID, +// UserId: req.UserId, +// AddressID: req.AddressId, +// EvidenceImage: req.EvidenceImage, +// StatusPickup: req.StatusPickup, +// CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), +// UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), +// } - user, err := s.repoUser.FindByID(req.UserId) - if err != nil { - return nil, fmt.Errorf("error fetching user data: %v", err) - } - mappedRequest.User = []dto.UserResponseDTO{ - { - Name: user.Name, - Phone: user.Phone, - }, - } +// user, err := s.repoUser.FindByID(req.UserId) +// if err != nil { +// return nil, fmt.Errorf("error fetching user data: %v", err) +// } +// mappedRequest.User = []dto.UserResponseDTO{ +// { +// Name: user.Name, +// Phone: user.Phone, +// }, +// } - address, err := s.repoAddress.FindAddressByID(req.AddressId) - if err != nil { - return nil, fmt.Errorf("error fetching address data: %v", err) - } - mappedRequest.Address = []dto.AddressResponseDTO{ - { - District: address.District, - Village: address.Village, - Detail: address.Detail, - }, - } +// address, err := s.repoAddress.FindAddressByID(req.AddressId) +// if err != nil { +// return nil, fmt.Errorf("error fetching address data: %v", err) +// } +// mappedRequest.Address = []dto.AddressResponseDTO{ +// { +// District: address.District, +// Village: address.Village, +// Detail: address.Detail, +// }, +// } - requestItems, err := s.repo.GetRequestPickupItems(req.ID) - if err != nil { - return nil, fmt.Errorf("error fetching request items: %v", err) - } +// requestItems, err := s.repo.GetRequestPickupItems(req.ID) +// if err != nil { +// return nil, fmt.Errorf("error fetching request items: %v", err) +// } - var mappedRequestItems []dto.ResponseRequestPickupItem +// var mappedRequestItems []dto.ResponseRequestPickupItem - for _, item := range requestItems { - trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) - if err != nil { - return nil, fmt.Errorf("error fetching trash category: %v", err) - } +// for _, item := range requestItems { +// trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) +// if err != nil { +// return nil, fmt.Errorf("error fetching trash category: %v", err) +// } - mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ - ID: item.ID, - TrashCategory: []dto.ResponseTrashCategoryDTO{{ - Name: trashCategory.Name, - Icon: trashCategory.Icon, - }}, - EstimatedAmount: item.EstimatedAmount, - }) - } +// mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ +// ID: item.ID, +// TrashCategory: []dto.ResponseTrashCategoryDTO{{ +// Name: trashCategory.Name, +// Icon: trashCategory.Icon, +// }}, +// EstimatedAmount: item.EstimatedAmount, +// }) +// } - mappedRequest.RequestItems = mappedRequestItems +// mappedRequest.RequestItems = mappedRequestItems - response = append(response, mappedRequest) - } - } +// response = append(response, mappedRequest) +// } +// } - return response, nil -} +// return response, nil +// } -func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { +// func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { - collector, err := s.repoColl.FindCollectorById(collectorId) - if err != nil { - return nil, fmt.Errorf("error fetching collector data: %v", err) - } - requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) - if err != nil { - return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) - } +// collector, err := s.repoColl.FindCollectorById(collectorId) +// if err != nil { +// return nil, fmt.Errorf("error fetching collector data: %v", err) +// } +// requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) +// if err != nil { +// return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) +// } - var response []dto.ResponseRequestPickup +// var response []dto.ResponseRequestPickup - for _, req := range requests { +// for _, req := range requests { - createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) +// createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) +// updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) - mappedRequest := dto.ResponseRequestPickup{ - ID: req.ID, - UserId: req.UserId, - AddressID: req.AddressId, - EvidenceImage: req.EvidenceImage, - StatusPickup: req.StatusPickup, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } +// mappedRequest := dto.ResponseRequestPickup{ +// ID: req.ID, +// UserId: req.UserId, +// AddressID: req.AddressId, +// EvidenceImage: req.EvidenceImage, +// StatusPickup: req.StatusPickup, +// CreatedAt: createdAt, +// UpdatedAt: updatedAt, +// } - user, err := s.repoUser.FindByID(req.UserId) - if err != nil { - return nil, fmt.Errorf("error fetching user data: %v", err) - } - mappedRequest.User = []dto.UserResponseDTO{ - { - Name: user.Name, - Phone: user.Phone, - }, - } +// user, err := s.repoUser.FindByID(req.UserId) +// if err != nil { +// return nil, fmt.Errorf("error fetching user data: %v", err) +// } +// mappedRequest.User = []dto.UserResponseDTO{ +// { +// Name: user.Name, +// Phone: user.Phone, +// }, +// } - address, err := s.repoAddress.FindAddressByID(req.AddressId) - if err != nil { - return nil, fmt.Errorf("error fetching address data: %v", err) - } - mappedRequest.Address = []dto.AddressResponseDTO{ - { - District: address.District, - Village: address.Village, - Detail: address.Detail, - }, - } +// address, err := s.repoAddress.FindAddressByID(req.AddressId) +// if err != nil { +// return nil, fmt.Errorf("error fetching address data: %v", err) +// } +// mappedRequest.Address = []dto.AddressResponseDTO{ +// { +// District: address.District, +// Village: address.Village, +// Detail: address.Detail, +// }, +// } - requestItems, err := s.repo.GetRequestPickupItems(req.ID) - if err != nil { - return nil, fmt.Errorf("error fetching request items: %v", err) - } +// requestItems, err := s.repo.GetRequestPickupItems(req.ID) +// if err != nil { +// return nil, fmt.Errorf("error fetching request items: %v", err) +// } - var mappedRequestItems []dto.ResponseRequestPickupItem +// var mappedRequestItems []dto.ResponseRequestPickupItem - for _, item := range requestItems { +// for _, item := range requestItems { - trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) - if err != nil { - return nil, fmt.Errorf("error fetching trash category: %v", err) - } +// trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) +// if err != nil { +// return nil, fmt.Errorf("error fetching trash category: %v", err) +// } - mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ - ID: item.ID, - TrashCategory: []dto.ResponseTrashCategoryDTO{{ - Name: trashCategory.Name, - Icon: trashCategory.Icon, - }}, - EstimatedAmount: item.EstimatedAmount, - }) - } +// mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ +// ID: item.ID, +// TrashCategory: []dto.ResponseTrashCategoryDTO{{ +// Name: trashCategory.Name, +// Icon: trashCategory.Icon, +// }}, +// EstimatedAmount: item.EstimatedAmount, +// }) +// } - mappedRequest.RequestItems = mappedRequestItems +// mappedRequest.RequestItems = mappedRequestItems - response = append(response, mappedRequest) - } +// response = append(response, mappedRequest) +// } - return response, nil -} +// return response, nil +// } -func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { +// func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { - request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") - if err != nil { - return fmt.Errorf("request pickup not found: %v", err) - } +// request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") +// if err != nil { +// return fmt.Errorf("request pickup not found: %v", err) +// } - if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { - return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") - } +// if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { +// return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") +// } - collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) - if err != nil { - return fmt.Errorf("collector tidak ditemukan: %v", err) - } +// collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) +// if err != nil { +// return fmt.Errorf("collector tidak ditemukan: %v", err) +// } - request.CollectorID = &collector.ID +// request.CollectorID = &collector.ID - err = s.repo.UpdateRequestPickup(request.ID, request) - if err != nil { - return fmt.Errorf("failed to update request pickup: %v", err) - } +// err = s.repo.UpdateRequestPickup(request.ID, request) +// if err != nil { +// return fmt.Errorf("failed to update request pickup: %v", err) +// } - return nil -} +// return nil +// } diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go index a19b149..12b1a12 100644 --- a/internal/services/trashcart_redisservices.go +++ b/internal/services/trashcart_redisservices.go @@ -30,7 +30,7 @@ func GetCartItems(userID string) ([]dto.RequestCartItems, error) { if err != nil { return nil, err } - + return items, nil } @@ -45,7 +45,7 @@ func AddOrUpdateCartItem(userID string, newItem dto.RequestCartItems) error { updated := false for i, item := range cartItems { - if item.TrashID == newItem.TrashID { + if item.TrashCategoryID == newItem.TrashCategoryID { if newItem.Amount == 0 { cartItems = append(cartItems[:i], cartItems[i+1:]...) } else { @@ -80,14 +80,14 @@ func DeleteCartItem(userID, trashID string) error { index := -1 for i, item := range items { - if item.TrashID == trashID { + if item.TrashCategoryID == trashID { index = i break } } if index == -1 { - log.Printf("TrashID %s not found in cart for user %s", trashID, userID) + log.Printf("TrashCategoryID %s not found in cart for user %s", trashID, userID) return fmt.Errorf("trashid not found") } diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go index 04ebfd4..b8442a6 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -31,9 +31,9 @@ func (s *CartService) CommitCartToDatabase(userID string) error { var estimatedTotal float32 for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) + trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) if err != nil { - log.Printf("Trash category not found for trashID: %s", item.TrashID) + log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) continue } @@ -43,7 +43,7 @@ func (s *CartService) CommitCartToDatabase(userID string) error { cartItems = append(cartItems, model.CartItem{ ID: uuid.NewString(), - TrashID: item.TrashID, + TrashCategoryID: item.TrashCategoryID, Amount: item.Amount, SubTotalEstimatedPrice: subTotal, }) @@ -87,7 +87,7 @@ func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) var cartItemDTOs []dto.CartItemResponse for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashID) + trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) if err != nil { continue } @@ -133,7 +133,7 @@ func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { for _, item := range cartDB.CartItems { items = append(items, dto.CartItemResponse{ ItemId: item.ID, - TrashId: item.TrashID, + TrashId: item.TrashCategoryID, TrashIcon: item.TrashCategory.Icon, TrashName: item.TrashCategory.Name, Amount: item.Amount, diff --git a/model/collector_model.go b/model/collector_model.go index 009ed98..4000cad 100644 --- a/model/collector_model.go +++ b/model/collector_model.go @@ -1,16 +1,25 @@ package model +import "time" + type Collector struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - UserID string `gorm:"not null" json:"userId"` - User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` - JobStatus string `gorm:"default:inactive" json:"jobstatus"` - Rating float32 `gorm:"default:5" json:"rating"` - AddressId string `gorm:"not null" json:"address_id"` - Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + UserID string `gorm:"not null" json:"user_id"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-"` + JobStatus string `gorm:"default:inactive" json:"jobstatus"` + Rating float32 `gorm:"default:5" json:"rating"` + AddressID string `gorm:"not null" json:"address_id"` + Address Address `gorm:"foreignKey:AddressID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` + AvaibleTrashByCollector []AvaibleTrashByCollector `gorm:"foreignKey:CollectorID;constraint:OnDelete:CASCADE;" json:"avaible_trash"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` } -// job_status { -// "active", -// "inactive" -// } +type AvaibleTrashByCollector struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + CollectorID string `gorm:"not null" json:"collector_id"` + Collector *Collector `gorm:"foreignKey:CollectorID;constraint:OnDelete:CASCADE;" json:"-"` + TrashCategoryID string `gorm:"not null" json:"trash_id"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"trash_category"` + Price float32 `json:"price"` +} diff --git a/model/trashcart_model.go b/model/trashcart_model.go index 97aa8d0..bfe1467 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -11,17 +11,18 @@ type Cart struct { CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cartitems"` TotalAmount float32 `json:"totalamount"` EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` } type CartItem struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` CartID string `gorm:"not null" json:"-"` - TrashID string `gorm:"not null" json:"trashid"` - TrashCategory TrashCategory `gorm:"foreignKey:TrashID;constraint:OnDelete:CASCADE;" json:"trash"` + Cart *Cart `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"-"` + TrashCategoryID string `gorm:"not null" json:"trash_id"` + TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"trash_category"` Amount float32 `json:"amount"` SubTotalEstimatedPrice float32 `json:"subtotalestimatedprice"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"createdAt"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updatedAt"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` } diff --git a/presentation/collector_route.go b/presentation/collector_route.go index 93692b9..06f200a 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -13,18 +13,39 @@ import ( ) func CollectorRouter(api fiber.Router) { - repo := repositories.NewCollectorRepository(config.DB) - repoReq := repositories.NewRequestPickupRepository(config.DB) - repoAddress := repositories.NewAddressRepository(config.DB) - repoUser := repositories.NewUserProfilRepository(config.DB) - colectorService := services.NewCollectorService(repo, repoReq, repoAddress, repoUser) - collectorHandler := handler.NewCollectorHandler(colectorService) + // repo := repositories.NewCollectorRepository(config.DB) + // repoReq := repositories.NewRequestPickupRepository(config.DB) + // repoAddress := repositories.NewAddressRepository(config.DB) + // repoUser := repositories.NewUserProfilRepository(config.DB) + // colectorService := services.NewCollectorService(repo, repoReq, repoAddress, repoUser) + // collectorHandler := handler.NewCollectorHandler(colectorService) - collector := api.Group("/collector") - collector.Use(middleware.AuthMiddleware) + // collector := api.Group("/collector") + // collector.Use(middleware.AuthMiddleware) - collector.Put("confirmrequest/:id", collectorHandler.ConfirmRequestPickup) - collector.Put("confirm-manual/request/:request_id", collectorHandler.ConfirmRequestManualPickup) - collector.Get("/avaible", collectorHandler.GetAvaibleCollector) + // collector.Put("confirmrequest/:id", collectorHandler.ConfirmRequestPickup) + // collector.Put("confirm-manual/request/:request_id", collectorHandler.ConfirmRequestManualPickup) + // collector.Get("/avaible", collectorHandler.GetAvaibleCollector) + // Middleware Auth dan Role + + // Inisialisasi repository dan service + collectorRepo := repositories.NewCollectorRepository() + trashRepo := repositories.NewTrashRepository(config.DB) + collectorService := services.NewCollectorService(collectorRepo, trashRepo) + collectorHandler := handler.NewCollectorHandler(collectorService) + + // Group collector + collectors := api.Group("/collectors") + collectors.Use(middleware.AuthMiddleware) + + // === Collector routes === + collectors.Post("/", collectorHandler.CreateCollector) // Create collector + collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) // Add trash to collector + collectors.Get("/:id", collectorHandler.GetCollectorByID) // Get collector by ID + collectors.Get("/", collectorHandler.GetCollectorByUserID) // Get collector by ID + collectors.Patch("/:id", collectorHandler.UpdateCollector) // Update collector fields + collectors.Patch("/:id/trash", collectorHandler.UpdateTrash) + collectors.Patch("/:id/job-status", collectorHandler.UpdateJobStatus) + collectors.Delete("/trash/:id", collectorHandler.DeleteTrash) } diff --git a/presentation/requestpickup_route.go b/presentation/requestpickup_route.go index 1ef168a..029826a 100644 --- a/presentation/requestpickup_route.go +++ b/presentation/requestpickup_route.go @@ -1,45 +1,46 @@ package presentation -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" +// import ( +// "rijig/config" +// "rijig/internal/handler" +// "rijig/internal/repositories" +// "rijig/internal/services" +// "rijig/middleware" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -func RequestPickupRouter(api fiber.Router) { - // repo repositories.RequestPickupRepository - // repoColl repositories.CollectorRepository - // repoAddress repositories.AddressRepository - // repoTrash repositories.TrashRepository - // repoUser repositories.UserProfilRepository +// func RequestPickupRouter(api fiber.Router) { +// // repo repositories.RequestPickupRepository +// // repoColl repositories.CollectorRepository +// // repoAddress repositories.AddressRepository +// // repoTrash repositories.TrashRepository +// // repoUser repositories.UserProfilRepository - requestRepo := repositories.NewRequestPickupRepository(config.DB) - repoColl := repositories.NewCollectorRepository(config.DB) - repoAddress := repositories.NewAddressRepository(config.DB) - Trashrepo := repositories.NewTrashRepository(config.DB) - repouser := repositories.NewUserProfilRepository(config.DB) - // collectorRepo := repositories.NewCollectorRepository(config.DB) +// requestRepo := repositories.NewRequestPickupRepository(config.DB) +// // repoColl := repositories.NewCollectorRepository(config.DB) +// repoColl := repositories.NewCollectorRepository() +// repoAddress := repositories.NewAddressRepository(config.DB) +// Trashrepo := repositories.NewTrashRepository(config.DB) +// repouser := repositories.NewUserProfilRepository(config.DB) +// // collectorRepo := repositories.NewCollectorRepository(config.DB) - requestPickupServices := services.NewRequestPickupService(requestRepo, repoColl, repoAddress, Trashrepo, repouser) - // collectorService := services.NewCollectorService(collectorRepo, requestRepo, repoAddress) - // service services.RequestPickupService, - // collectorService services.CollectorService +// requestPickupServices := services.NewRequestPickupService(requestRepo, repoColl, repoAddress, Trashrepo, repouser) +// // collectorService := services.NewCollectorService(collectorRepo, requestRepo, repoAddress) +// // service services.RequestPickupService, +// // collectorService services.CollectorService - requestPickupHandler := handler.NewRequestPickupHandler(requestPickupServices) +// requestPickupHandler := handler.NewRequestPickupHandler(requestPickupServices) - requestPickupAPI := api.Group("/requestpickup") - requestPickupAPI.Use(middleware.AuthMiddleware) +// requestPickupAPI := api.Group("/requestpickup") +// requestPickupAPI.Use(middleware.AuthMiddleware) - requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) - // requestPickupAPI.Get("/get", middleware.AuthMiddleware, requestPickupHandler.GetAutomaticRequestByUser) - requestPickupAPI.Get("/get-allrequest", requestPickupHandler.GetRequestPickups) - requestPickupAPI.Patch("/select-collector", requestPickupHandler.AssignCollectorToRequest) - // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) - // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) - // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) - // requestPickupAPI.Delete("/:id", requestPickupHandler.DeleteRequestPickup) -} +// requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) +// // requestPickupAPI.Get("/get", middleware.AuthMiddleware, requestPickupHandler.GetAutomaticRequestByUser) +// requestPickupAPI.Get("/get-allrequest", requestPickupHandler.GetRequestPickups) +// requestPickupAPI.Patch("/select-collector", requestPickupHandler.AssignCollectorToRequest) +// // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) +// // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) +// // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) +// // requestPickupAPI.Delete("/:id", requestPickupHandler.DeleteRequestPickup) +// } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index d23059d..4c7e163 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -26,7 +26,7 @@ func SetupRoutes(app *fiber.App) { // || auth router || // presentation.IdentityCardRouter(api) presentation.CompanyProfileRouter(api) - presentation.RequestPickupRouter(api) + // presentation.RequestPickupRouter(api) presentation.CollectorRouter(api) presentation.TrashCartRouter(api) From b7a1d10898d121ebccd054cc8c64dd4b9f9f00f6 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 22 May 2025 01:42:14 +0700 Subject: [PATCH 37/48] fix: fixing cart feature behavior and auto commit optimizing --- cmd/main.go | 18 +- dto/trashcart_dto.go | 72 +++--- go.mod | 1 + go.sum | 2 + internal/handler/cart_handler.go | 114 +++++++++ internal/handler/trashcart_handler.go | 96 ------- internal/services/cart_redis.go | 104 ++++++++ internal/services/cart_service.go | 35 +++ internal/services/trashcart_redisservices.go | 124 --------- internal/services/trashcart_service.go | 254 +++++++++---------- internal/worker/cart_worker.go | 141 ++++++---- model/trashcart_model.go | 18 +- presentation/cart_router.go | 24 ++ presentation/trashcart_route.go | 27 -- 14 files changed, 550 insertions(+), 480 deletions(-) create mode 100644 internal/handler/cart_handler.go delete mode 100644 internal/handler/trashcart_handler.go create mode 100644 internal/services/cart_redis.go create mode 100644 internal/services/cart_service.go delete mode 100644 internal/services/trashcart_redisservices.go create mode 100644 presentation/cart_router.go delete mode 100644 presentation/trashcart_route.go diff --git a/cmd/main.go b/cmd/main.go index 2ab9825..e99d0be 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,19 +1,33 @@ package main import ( + "log" + "os" "rijig/config" + "rijig/internal/worker" "rijig/router" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/robfig/cron" ) func main() { config.SetupConfig() + logFile, _ := os.OpenFile("logs/cart_commit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + log.SetOutput(logFile) + + go func() { + c := cron.New() + c.AddFunc("@every 1m", func() { + _ = worker.CommitExpiredCartsToDB() + }) + c.Start() + }() app := fiber.New() app.Use(cors.New(cors.Config{ - AllowOrigins: "*", + AllowOrigins: "*", AllowMethods: "GET,POST,PUT,PATCH,DELETE", AllowHeaders: "Content-Type,x-api-key", })) @@ -43,4 +57,4 @@ func main() { router.SetupRoutes(app) config.StartServer(app) -} \ No newline at end of file +} diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index 3f4a541..fe124fa 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -5,65 +5,53 @@ import ( "strings" ) -type ValidationErrors struct { - Errors map[string][]string +type RequestCartItemDTO struct { + TrashID string `json:"trash_id"` + Amount float32 `json:"amount"` } -func (v ValidationErrors) Error() string { - return "validation error" +type RequestCartDTO struct { + CartItems []RequestCartItemDTO `json:"cart_items"` } -type CartResponse struct { - ID string `json:"id"` - UserID string `json:"userid"` - CartItems []CartItemResponse `json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type CartItemResponse struct { - ItemId string `json:"item_id"` - TrashId string `json:"trashid"` - TrashIcon string `json:"trashicon"` - TrashName string `json:"trashname"` +type ResponseCartItemDTO struct { + ID string `json:"id"` + TrashID string `json:"trash_id"` + TrashName string `json:"trash_name"` + TrashIcon string `json:"trash_icon"` Amount float32 `json:"amount"` - EstimatedSubTotalPrice float32 `json:"estimated_subtotalprice"` + SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` } -type RequestCartItems struct { - TrashCategoryID string `json:"trashid"` - Amount float32 `json:"amount"` +type ResponseCartDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TotalAmount float32 `json:"total_amount"` + EstimatedTotalPrice float32 `json:"estimated_total_price"` + CartItems []ResponseCartItemDTO `json:"cart_items"` } -func (r *RequestCartItems) ValidateRequestCartItem() (map[string][]string, bool) { +// ==== VALIDATION ==== + +func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { errors := make(map[string][]string) - if strings.TrimSpace(r.TrashCategoryID) == "" { - errors["trashid"] = append(errors["trashid"], "trashid is required") + if len(r.CartItems) == 0 { + errors["cart_items"] = append(errors["cart_items"], "minimal satu item harus dimasukkan") } - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type BulkRequestCartItems struct { - Items []RequestCartItems `json:"items"` -} - -func (b *BulkRequestCartItems) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - for i, item := range b.Items { - if strings.TrimSpace(item.TrashCategoryID) == "" { - errors[fmt.Sprintf("items[%d].trashid", i)] = append(errors[fmt.Sprintf("items[%d].trashid", i)], "trashid is required") + for i, item := range r.CartItems { + if strings.TrimSpace(item.TrashID) == "" { + errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong") + } + if item.Amount <= 0 { + errors[fmt.Sprintf("cart_items[%d].amount", i)] = append(errors[fmt.Sprintf("cart_items[%d].amount", i)], "amount harus lebih dari 0") } } + if len(errors) > 0 { return errors, false } + return nil, true } diff --git a/go.mod b/go.mod index 687dde0..93ed566 100644 --- a/go.mod +++ b/go.mod @@ -37,6 +37,7 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect + github.com/robfig/cron v1.2.0 github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/go.sum b/go.sum index d5b6c0b..a238c19 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= diff --git a/internal/handler/cart_handler.go b/internal/handler/cart_handler.go new file mode 100644 index 0000000..ab98fd7 --- /dev/null +++ b/internal/handler/cart_handler.go @@ -0,0 +1,114 @@ +package handler + +import ( + "context" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CartHandler interface { + GetCart(c *fiber.Ctx) error + AddOrUpdateCartItem(c *fiber.Ctx) error + AddMultipleCartItems(c *fiber.Ctx) error + DeleteCartItem(c *fiber.Ctx) error + ClearCart(c *fiber.Ctx) error +} + +type cartHandler struct { + service services.CartService +} + +func NewCartHandler(service services.CartService) CartHandler { + return &cartHandler{service: service} +} + +// GET /cart +func (h *cartHandler) GetCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + cart, err := h.service.GetCart(context.Background(), userID) + if err != nil { + return utils.ErrorResponse(c, "Cart belum dibuat atau sudah kadaluarsa") + } + + return utils.SuccessResponse(c, cart, "Data cart berhasil diambil") +} + +// POST /cart/item +func (h *cartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + var item dto.RequestCartItemDTO + if err := c.BodyParser(&item); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"format tidak valid"}}) + } + + if item.TrashID == "" || item.Amount <= 0 { + return utils.ValidationErrorResponse(c, map[string][]string{ + "trash_id": {"harus diisi"}, + "amount": {"harus lebih dari 0"}, + }) + } + + if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Item berhasil ditambahkan/diupdate di cart") +} + +// POST /cart/items +func (h *cartHandler) AddMultipleCartItems(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + var payload dto.RequestCartDTO + if err := c.BodyParser(&payload); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format tidak valid"}, + }) + } + + if errs, ok := payload.ValidateRequestCartDTO(); !ok { + return utils.ValidationErrorResponse(c, errs) + } + + for _, item := range payload.CartItems { + if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + } + + return utils.SuccessResponse(c, nil, "Semua item berhasil ditambahkan/diupdate ke cart") +} + + +// DELETE /cart/item/:trashID +func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + trashID := c.Params("trashID") + + if trashID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{"trash_id": {"tidak boleh kosong"}}) + } + + err := h.service.DeleteItem(context.Background(), userID, trashID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Item berhasil dihapus dari cart") +} + +// DELETE /cart +func (h *cartHandler) ClearCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + if err := h.service.ClearCart(context.Background(), userID); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Seluruh cart berhasil dihapus") +} diff --git a/internal/handler/trashcart_handler.go b/internal/handler/trashcart_handler.go deleted file mode 100644 index 7890e77..0000000 --- a/internal/handler/trashcart_handler.go +++ /dev/null @@ -1,96 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CartHandler struct { - Service *services.CartService -} - -func NewCartHandler(service *services.CartService) *CartHandler { - return &CartHandler{Service: service} -} - -func (h *CartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { - var body dto.BulkRequestCartItems - if err := c.BodyParser(&body); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"Invalid JSON body"}, - }) - } - - if errors, ok := body.Validate(); !ok { - return utils.ValidationErrorResponse(c, errors) - } - - userID := c.Locals("userID").(string) - for _, item := range body.Items { - if err := services.AddOrUpdateCartItem(userID, item); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to update one or more items") - } - } - - return utils.SuccessResponse(c, nil, "Cart updated successfully") -} - -func (h *CartHandler) DeleteCartItem(c *fiber.Ctx) error { - trashID := c.Params("trashid") - userID := c.Locals("userID").(string) - - err := services.DeleteCartItem(userID, trashID) - if err != nil { - if err.Error() == "no cart found" || err.Error() == "trashid not found" { - return utils.GenericResponse(c, fiber.StatusNotFound, "Trash item not found in cart") - } - return utils.InternalServerErrorResponse(c, "Failed to delete item") - } - - return utils.SuccessResponse(c, nil, "Item deleted") -} - -func (h *CartHandler) ClearCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - if err := services.ClearCart(userID); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to clear cart") - } - return utils.SuccessResponse(c, nil, "Cart cleared") -} - -func (h *CartHandler) GetCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - cart, err := h.Service.GetCart(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to fetch cart") - } - - return utils.SuccessResponse(c, cart, "User cart data successfully fetched") -} - -func (h *CartHandler) CommitCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - err := h.Service.CommitCartToDatabase(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to commit cart to database") - } - - return utils.SuccessResponse(c, nil, "Cart committed to database") -} - -// PUT /cart/refresh → refresh TTL Redis -func (h *CartHandler) RefreshCartTTL(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - err := services.RefreshCartTTL(userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Failed to refresh cart TTL") - } - - return utils.SuccessResponse(c, nil, "Cart TTL refreshed") -} diff --git a/internal/services/cart_redis.go b/internal/services/cart_redis.go new file mode 100644 index 0000000..aff0ce9 --- /dev/null +++ b/internal/services/cart_redis.go @@ -0,0 +1,104 @@ +package services + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "rijig/config" + "rijig/dto" +) + +var cartTTL = 30 * time.Minute + +func getCartKey(userID string) string { + return fmt.Sprintf("cart:user:%s", userID) +} + +func SetCartToRedis(ctx context.Context, userID string, cart dto.RequestCartDTO) error { + key := getCartKey(userID) + + data, err := json.Marshal(cart) + if err != nil { + return fmt.Errorf("failed to marshal cart: %w", err) + } + + err = config.RedisClient.Set(ctx, key, data, cartTTL).Err() + if err != nil { + return fmt.Errorf("failed to save cart to redis: %w", err) + } + + return nil +} + +func GetCartFromRedis(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { + key := getCartKey(userID) + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + return nil, err + } + + var cart dto.RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + return nil, fmt.Errorf("failed to unmarshal cart data: %w", err) + } + + return &cart, nil +} + +func DeleteCartFromRedis(ctx context.Context, userID string) error { + key := getCartKey(userID) + return config.RedisClient.Del(ctx, key).Err() +} + +func GetCartTTL(ctx context.Context, userID string) (time.Duration, error) { + key := getCartKey(userID) + return config.RedisClient.TTL(ctx, key).Result() +} + +func UpdateOrAddCartItemToRedis(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { + cart, err := GetCartFromRedis(ctx, userID) + if err != nil { + + cart = &dto.RequestCartDTO{ + CartItems: []dto.RequestCartItemDTO{item}, + } + return SetCartToRedis(ctx, userID, *cart) + } + + updated := false + for i, ci := range cart.CartItems { + if ci.TrashID == item.TrashID { + cart.CartItems[i].Amount = item.Amount + updated = true + break + } + } + if !updated { + cart.CartItems = append(cart.CartItems, item) + } + + return SetCartToRedis(ctx, userID, *cart) +} + +func RemoveCartItemFromRedis(ctx context.Context, userID, trashID string) error { + cart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + + updatedItems := make([]dto.RequestCartItemDTO, 0) + for _, ci := range cart.CartItems { + if ci.TrashID != trashID { + updatedItems = append(updatedItems, ci) + } + } + + if len(updatedItems) == 0 { + return DeleteCartFromRedis(ctx, userID) + } + + cart.CartItems = updatedItems + return SetCartToRedis(ctx, userID, *cart) +} diff --git a/internal/services/cart_service.go b/internal/services/cart_service.go new file mode 100644 index 0000000..d98cd8d --- /dev/null +++ b/internal/services/cart_service.go @@ -0,0 +1,35 @@ +package services + +import ( + "rijig/dto" + "context" +) + +type CartService interface { + GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) + AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error + DeleteItem(ctx context.Context, userID string, trashID string) error + ClearCart(ctx context.Context, userID string) error +} + +type cartService struct{} + +func NewCartService() CartService { + return &cartService{} +} + +func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { + return GetCartFromRedis(ctx, userID) +} + +func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { + return UpdateOrAddCartItemToRedis(ctx, userID, item) +} + +func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error { + return RemoveCartItemFromRedis(ctx, userID, trashID) +} + +func (s *cartService) ClearCart(ctx context.Context, userID string) error { + return DeleteCartFromRedis(ctx, userID) +} \ No newline at end of file diff --git a/internal/services/trashcart_redisservices.go b/internal/services/trashcart_redisservices.go deleted file mode 100644 index 12b1a12..0000000 --- a/internal/services/trashcart_redisservices.go +++ /dev/null @@ -1,124 +0,0 @@ -package services - -import ( - "encoding/json" - "fmt" - "log" - "time" - - "rijig/config" - "rijig/dto" - - "github.com/go-redis/redis/v8" -) - -const cartTTL = 1 * time.Minute - -func getCartKey(userID string) string { - return fmt.Sprintf("cart:%s", userID) -} - -func GetCartItems(userID string) ([]dto.RequestCartItems, error) { - key := getCartKey(userID) - val, err := config.RedisClient.Get(config.Ctx, key).Result() - if err != nil { - return nil, err - } - - var items []dto.RequestCartItems - err = json.Unmarshal([]byte(val), &items) - if err != nil { - return nil, err - } - - return items, nil -} - -func AddOrUpdateCartItem(userID string, newItem dto.RequestCartItems) error { - key := getCartKey(userID) - var cartItems []dto.RequestCartItems - - val, err := config.RedisClient.Get(config.Ctx, key).Result() - if err == nil && val != "" { - json.Unmarshal([]byte(val), &cartItems) - } - - updated := false - for i, item := range cartItems { - if item.TrashCategoryID == newItem.TrashCategoryID { - if newItem.Amount == 0 { - cartItems = append(cartItems[:i], cartItems[i+1:]...) - } else { - cartItems[i].Amount = newItem.Amount - } - updated = true - break - } - } - - if !updated && newItem.Amount > 0 { - cartItems = append(cartItems, newItem) - } - - return setCartItems(key, cartItems) -} - -func DeleteCartItem(userID, trashID string) error { - key := fmt.Sprintf("cart:%s", userID) - items, err := GetCartItems(userID) - - if err == redis.Nil { - - log.Printf("No cart found in Redis for user: %s", userID) - return fmt.Errorf("no cart found") - } - - if err != nil { - log.Printf("Redis error: %v", err) - return err - } - - index := -1 - for i, item := range items { - if item.TrashCategoryID == trashID { - index = i - break - } - } - - if index == -1 { - log.Printf("TrashCategoryID %s not found in cart for user %s", trashID, userID) - return fmt.Errorf("trashid not found") - } - - items = append(items[:index], items[index+1:]...) - - if len(items) == 0 { - return config.RedisClient.Del(config.Ctx, key).Err() - } - - return setCartItems(key, items) -} - -func ClearCart(userID string) error { - key := getCartKey(userID) - return config.RedisClient.Del(config.Ctx, key).Err() -} - -func RefreshCartTTL(userID string) error { - key := getCartKey(userID) - return config.RedisClient.Expire(config.Ctx, key, cartTTL).Err() -} - -func setCartItems(key string, items []dto.RequestCartItems) error { - data, err := json.Marshal(items) - if err != nil { - return err - } - - err = config.RedisClient.Set(config.Ctx, key, data, cartTTL).Err() - if err != nil { - log.Printf("Redis SetCart error: %v", err) - } - return err -} diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go index b8442a6..a231c47 100644 --- a/internal/services/trashcart_service.go +++ b/internal/services/trashcart_service.go @@ -1,154 +1,154 @@ package services -import ( - "log" - "time" +// import ( +// "log" +// "time" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" +// "rijig/dto" +// "rijig/internal/repositories" +// "rijig/model" - "github.com/google/uuid" -) +// "github.com/google/uuid" +// ) -type CartService struct { - Repo repositories.CartRepository -} +// type CartService struct { +// Repo repositories.CartRepository +// } -func NewCartService(repo repositories.CartRepository) *CartService { - return &CartService{Repo: repo} -} +// func NewCartService(repo repositories.CartRepository) *CartService { +// return &CartService{Repo: repo} +// } -func (s *CartService) CommitCartToDatabase(userID string) error { - items, err := GetCartItems(userID) - if err != nil || len(items) == 0 { - log.Printf("No items to commit for user: %s", userID) - return err - } +// func (s *CartService) CommitCartToDatabase(userID string) error { +// items, err := GetCartItems(userID) +// if err != nil || len(items) == 0 { +// log.Printf("No items to commit for user: %s", userID) +// return err +// } - var cartItems []model.CartItem - var totalAmount float32 - var estimatedTotal float32 +// var cartItems []model.CartItem +// var totalAmount float32 +// var estimatedTotal float32 - for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) - if err != nil { - log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) - continue - } +// for _, item := range items { +// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) +// if err != nil { +// log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) +// continue +// } - subTotal := float32(trash.EstimatedPrice) * item.Amount - totalAmount += item.Amount - estimatedTotal += subTotal +// subTotal := float32(trash.EstimatedPrice) * item.Amount +// totalAmount += item.Amount +// estimatedTotal += subTotal - cartItems = append(cartItems, model.CartItem{ - ID: uuid.NewString(), - TrashCategoryID: item.TrashCategoryID, - Amount: item.Amount, - SubTotalEstimatedPrice: subTotal, - }) - } +// cartItems = append(cartItems, model.CartItem{ +// ID: uuid.NewString(), +// TrashCategoryID: item.TrashCategoryID, +// Amount: item.Amount, +// SubTotalEstimatedPrice: subTotal, +// }) +// } - cart := &model.Cart{ - ID: uuid.NewString(), - UserID: userID, - CartItems: cartItems, - TotalAmount: totalAmount, - EstimatedTotalPrice: estimatedTotal, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } +// cart := &model.Cart{ +// ID: uuid.NewString(), +// UserID: userID, +// CartItems: cartItems, +// TotalAmount: totalAmount, +// EstimatedTotalPrice: estimatedTotal, +// CreatedAt: time.Now(), +// UpdatedAt: time.Now(), +// } - if err := s.Repo.DeleteCartByUserID(userID); err != nil { - log.Printf("Failed to delete old cart: %v", err) - } +// if err := s.Repo.DeleteCartByUserID(userID); err != nil { +// log.Printf("Failed to delete old cart: %v", err) +// } - if err := s.Repo.CreateCart(cart); err != nil { - log.Printf("Failed to create cart: %v", err) - return err - } +// if err := s.Repo.CreateCart(cart); err != nil { +// log.Printf("Failed to create cart: %v", err) +// return err +// } - if err := ClearCart(userID); err != nil { - log.Printf("Failed to clear Redis cart: %v", err) - } +// if err := ClearCart(userID); err != nil { +// log.Printf("Failed to clear Redis cart: %v", err) +// } - log.Printf("Cart committed successfully for user: %s", userID) - return nil -} +// log.Printf("Cart committed successfully for user: %s", userID) +// return nil +// } -func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { - items, err := GetCartItems(userID) - if err != nil || len(items) == 0 { - return nil, err - } +// func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { +// items, err := GetCartItems(userID) +// if err != nil || len(items) == 0 { +// return nil, err +// } - var totalAmount float32 - var estimatedTotal float32 - var cartItemDTOs []dto.CartItemResponse +// var totalAmount float32 +// var estimatedTotal float32 +// var cartItemDTOs []dto.CartItemResponse - for _, item := range items { - trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) - if err != nil { - continue - } +// for _, item := range items { +// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) +// if err != nil { +// continue +// } - subtotal := float32(trash.EstimatedPrice) * item.Amount - totalAmount += item.Amount - estimatedTotal += subtotal +// subtotal := float32(trash.EstimatedPrice) * item.Amount +// totalAmount += item.Amount +// estimatedTotal += subtotal - cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ - TrashId: trash.ID, - TrashIcon: trash.Icon, - TrashName: trash.Name, - Amount: item.Amount, - EstimatedSubTotalPrice: subtotal, - }) - } +// cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ +// TrashId: trash.ID, +// TrashIcon: trash.Icon, +// TrashName: trash.Name, +// Amount: item.Amount, +// EstimatedSubTotalPrice: subtotal, +// }) +// } - resp := &dto.CartResponse{ - ID: "N/A", - UserID: userID, - TotalAmount: totalAmount, - EstimatedTotalPrice: estimatedTotal, - CreatedAt: time.Now().Format(time.RFC3339), - UpdatedAt: time.Now().Format(time.RFC3339), - CartItems: cartItemDTOs, - } - return resp, nil -} +// resp := &dto.CartResponse{ +// ID: "N/A", +// UserID: userID, +// TotalAmount: totalAmount, +// EstimatedTotalPrice: estimatedTotal, +// CreatedAt: time.Now().Format(time.RFC3339), +// UpdatedAt: time.Now().Format(time.RFC3339), +// CartItems: cartItemDTOs, +// } +// return resp, nil +// } -func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { +// func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { - cartRedis, err := s.GetCartFromRedis(userID) - if err == nil && len(cartRedis.CartItems) > 0 { - return cartRedis, nil - } +// cartRedis, err := s.GetCartFromRedis(userID) +// if err == nil && len(cartRedis.CartItems) > 0 { +// return cartRedis, nil +// } - cartDB, err := s.Repo.GetCartByUserID(userID) - if err != nil { - return nil, err - } +// cartDB, err := s.Repo.GetCartByUserID(userID) +// if err != nil { +// return nil, err +// } - var items []dto.CartItemResponse - for _, item := range cartDB.CartItems { - items = append(items, dto.CartItemResponse{ - ItemId: item.ID, - TrashId: item.TrashCategoryID, - TrashIcon: item.TrashCategory.Icon, - TrashName: item.TrashCategory.Name, - Amount: item.Amount, - EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, - }) - } +// var items []dto.CartItemResponse +// for _, item := range cartDB.CartItems { +// items = append(items, dto.CartItemResponse{ +// ItemId: item.ID, +// TrashId: item.TrashCategoryID, +// TrashIcon: item.TrashCategory.Icon, +// TrashName: item.TrashCategory.Name, +// Amount: item.Amount, +// EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, +// }) +// } - resp := &dto.CartResponse{ - ID: cartDB.ID, - UserID: cartDB.UserID, - TotalAmount: cartDB.TotalAmount, - EstimatedTotalPrice: cartDB.EstimatedTotalPrice, - CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), - UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), - CartItems: items, - } - return resp, nil -} +// resp := &dto.CartResponse{ +// ID: cartDB.ID, +// UserID: cartDB.UserID, +// TotalAmount: cartDB.TotalAmount, +// EstimatedTotalPrice: cartDB.EstimatedTotalPrice, +// CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), +// UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), +// CartItems: items, +// } +// return resp, nil +// } diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go index ce208ab..07449fb 100644 --- a/internal/worker/cart_worker.go +++ b/internal/worker/cart_worker.go @@ -1,76 +1,111 @@ package worker import ( - "log" + "context" + "encoding/json" + "fmt" "strings" "time" "rijig/config" - "rijig/internal/services" + "rijig/dto" + "rijig/model" ) -const ( - lockPrefix = "lock:cart:" - lockExpiration = 30 * time.Second - commitThreshold = 20 * time.Second - scanPattern = "cart:*" -) +func CommitExpiredCartsToDB() error { + ctx := context.Background() -func StartCartCommitWorker(service *services.CartService) { + keys, err := config.RedisClient.Keys(ctx, "cart:user:*").Result() + if err != nil { + return fmt.Errorf("error fetching cart keys: %w", err) + } - go func() { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - log.Println("🛠️ Cart Worker is running in background...") - for range ticker.C { - processCarts(service) - } - }() - -} - -func processCarts(service *services.CartService) { - iter := config.RedisClient.Scan(config.Ctx, 0, scanPattern, 0).Iterator() - for iter.Next(config.Ctx) { - key := iter.Val() - - ttl, err := config.RedisClient.TTL(config.Ctx, key).Result() - if err != nil { - log.Printf("❌ Error getting TTL for %s: %v", key, err) + for _, key := range keys { + ttl, err := config.RedisClient.TTL(ctx, key).Result() + if err != nil || ttl > 30*time.Second { continue } - if ttl > 0 && ttl < commitThreshold { - userID := extractUserIDFromKey(key) - if userID == "" { - continue - } - - lockKey := lockPrefix + userID - acquired, err := config.RedisClient.SetNX(config.Ctx, lockKey, "locked", lockExpiration).Result() - if err != nil || !acquired { - continue - } - - log.Printf("🔄 Auto-committing cart for user %s (TTL: %v)", userID, ttl) - if err := service.CommitCartToDatabase(userID); err != nil { - log.Printf("❌ Failed to commit cart for %s: %v", userID, err) - } else { - log.Printf("✅ Cart committed for user %s", userID) - } - + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + continue } + + var cart dto.RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + continue + } + + userID := extractUserIDFromKey(key) + + cartID := SaveCartToDB(ctx, userID, &cart) + + _ = config.RedisClient.Del(ctx, key).Err() + + fmt.Printf( + "[AUTO-COMMIT] UserID: %s | CartID: %s | TotalItem: %d | EstimatedTotalPrice: %.2f | Committed at: %s\n", + userID, cartID, len(cart.CartItems), calculateTotalEstimated(&cart), time.Now().Format(time.RFC3339), + ) + } - if err := iter.Err(); err != nil { - log.Printf("❌ Error iterating Redis keys: %v", err) - } + return nil } func extractUserIDFromKey(key string) string { - if strings.HasPrefix(key, "cart:") { - return strings.TrimPrefix(key, "cart:") + + parts := strings.Split(key, ":") + if len(parts) == 3 { + return parts[2] } return "" } + +func SaveCartToDB(ctx context.Context, userID string, cart *dto.RequestCartDTO) string { + totalAmount := float32(0) + totalPrice := float32(0) + + var cartItems []model.CartItem + for _, item := range cart.CartItems { + + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { + continue + } + + subtotal := trash.EstimatedPrice * float64(item.Amount) + totalAmount += item.Amount + totalPrice += float32(subtotal) + + cartItems = append(cartItems, model.CartItem{ + TrashCategoryID: item.TrashID, + Amount: item.Amount, + SubTotalEstimatedPrice: float32(subtotal), + }) + } + + newCart := model.Cart{ + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: cartItems, + } + + if err := config.DB.WithContext(ctx).Create(&newCart).Error; err != nil { + fmt.Printf("Error committing cart: %v\n", err) + } + + return newCart.ID +} + +func calculateTotalEstimated(cart *dto.RequestCartDTO) float32 { + var total float32 + for _, item := range cart.CartItems { + var trash model.TrashCategory + if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { + continue + } + total += item.Amount * float32(trash.EstimatedPrice) + } + return total +} diff --git a/model/trashcart_model.go b/model/trashcart_model.go index bfe1467..49d530a 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -6,13 +6,13 @@ import ( type Cart struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - UserID string `gorm:"not null" json:"userid"` + UserID string `gorm:"not null" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;" json:"-"` - CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cartitems"` - TotalAmount float32 `json:"totalamount"` - EstimatedTotalPrice float32 `json:"estimated_totalprice"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cart_items"` + TotalAmount float32 `json:"total_amount"` + EstimatedTotalPrice float32 `json:"estimated_total_price"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } type CartItem struct { @@ -22,7 +22,7 @@ type CartItem struct { TrashCategoryID string `gorm:"not null" json:"trash_id"` TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"trash_category"` Amount float32 `json:"amount"` - SubTotalEstimatedPrice float32 `json:"subtotalestimatedprice"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } diff --git a/presentation/cart_router.go b/presentation/cart_router.go new file mode 100644 index 0000000..30c6725 --- /dev/null +++ b/presentation/cart_router.go @@ -0,0 +1,24 @@ +package presentation + +import ( + "rijig/internal/handler" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func TrashCartRouter(api fiber.Router) { + cartService := services.NewCartService() + cartHandler := handler.NewCartHandler(cartService) + + cart := api.Group("/cart") + cart.Use(middleware.AuthMiddleware) + + cart.Get("/", cartHandler.GetCart) + cart.Post("/item", cartHandler.AddOrUpdateCartItem) + cart.Post("/items", cartHandler.AddMultipleCartItems) + cart.Delete("/item/:trashID", cartHandler.DeleteCartItem) + cart.Delete("/", cartHandler.ClearCart) + +} diff --git a/presentation/trashcart_route.go b/presentation/trashcart_route.go deleted file mode 100644 index 277010d..0000000 --- a/presentation/trashcart_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation - -import ( - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/internal/worker" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func TrashCartRouter(api fiber.Router) { - cartRepo := repositories.NewCartRepository() - cartService := services.NewCartService(cartRepo) - cartHandler := handler.NewCartHandler(cartService) - - worker.StartCartCommitWorker(cartService) - - cart := api.Group("/cart", middleware.AuthMiddleware) - cart.Put("/refresh", cartHandler.RefreshCartTTL) - cart.Post("/", cartHandler.AddOrUpdateCartItem) - cart.Get("/", cartHandler.GetCart) - cart.Post("/commit", cartHandler.CommitCart) - cart.Delete("/", cartHandler.ClearCart) - cart.Delete("/:trashid", cartHandler.DeleteCartItem) -} From ab6f282235ce65075658dbc776d4962faa31ac20 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 22 May 2025 12:36:27 +0700 Subject: [PATCH 38/48] feat: add all feature are maybe needed in pickup bussiness --- config/database.go | 2 + dto/collector_dto.go | 11 + dto/rating_dto.go | 25 + dto/request_pickup_dto.go | 85 +++ internal/handler/pickup_history_handler.go | 37 ++ internal/handler/pickup_matching_handler.go | 49 ++ internal/handler/rating_handler.go | 66 ++ internal/handler/request_pickup_handler.go | 150 +++++ internal/repositories/collector_repo.go | 110 ++-- internal/repositories/pickup_history_repo.go | 34 + internal/repositories/rating_repo.go | 48 ++ internal/repositories/request_pickup_repo.go | 143 +++++ internal/repositories/requestpickup_repo.go | 324 +++++----- internal/repositories/trash_repo.go | 10 + internal/services/pickup_history_service.go | 36 ++ internal/services/pickup_maching_service.go | 146 +++++ internal/services/rating_service.go | 43 ++ internal/services/request_pickup_service.go | 139 +++++ internal/services/requestpickup_service.go | 618 +++++++++---------- model/pickup_history_model.go | 12 + model/rating_model.go | 13 + model/requestpickup_model.go | 28 +- presentation/collector_route.go | 13 +- presentation/pickup_matching_route.go | 25 + presentation/rating_route.go | 24 + presentation/request_pickup_route.go | 33 + router/setup_routes.go.go | 5 +- 27 files changed, 1667 insertions(+), 562 deletions(-) create mode 100644 dto/rating_dto.go create mode 100644 dto/request_pickup_dto.go create mode 100644 internal/handler/pickup_history_handler.go create mode 100644 internal/handler/pickup_matching_handler.go create mode 100644 internal/handler/rating_handler.go create mode 100644 internal/handler/request_pickup_handler.go create mode 100644 internal/repositories/pickup_history_repo.go create mode 100644 internal/repositories/rating_repo.go create mode 100644 internal/repositories/request_pickup_repo.go create mode 100644 internal/services/pickup_history_service.go create mode 100644 internal/services/pickup_maching_service.go create mode 100644 internal/services/rating_service.go create mode 100644 internal/services/request_pickup_service.go create mode 100644 model/pickup_history_model.go create mode 100644 model/rating_model.go create mode 100644 presentation/pickup_matching_route.go create mode 100644 presentation/rating_route.go create mode 100644 presentation/request_pickup_route.go diff --git a/config/database.go b/config/database.go index d19ebbd..04707b3 100644 --- a/config/database.go +++ b/config/database.go @@ -54,6 +54,8 @@ func ConnectDatabase() { // =>requestpickup preparation<= &model.RequestPickup{}, &model.RequestPickupItem{}, + &model.PickupStatusHistory{}, + &model.PickupRating{}, &model.Cart{}, &model.CartItem{}, diff --git a/dto/collector_dto.go b/dto/collector_dto.go index a48033a..0a523d7 100644 --- a/dto/collector_dto.go +++ b/dto/collector_dto.go @@ -5,6 +5,17 @@ import ( "strings" ) +type NearbyCollectorDTO struct { + CollectorID string `json:"collector_id"` + Name string `json:"name"` + Phone string `json:"phone"` + Rating float32 `json:"rating"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + DistanceKm float64 `json:"distance_km"` + MatchedTrash []string `json:"matched_trash_ids"` +} + type RequestCollectorDTO struct { AddressId string `json:"address_id"` AvaibleTrashbyCollector []RequestAvaibleTrashbyCollector `json:"avaible_trash"` diff --git a/dto/rating_dto.go b/dto/rating_dto.go new file mode 100644 index 0000000..62b680d --- /dev/null +++ b/dto/rating_dto.go @@ -0,0 +1,25 @@ +package dto + +import "strings" + +type CreatePickupRatingDTO struct { + Rating float32 `json:"rating"` + Feedback string `json:"feedback"` +} + +func (r *CreatePickupRatingDTO) ValidateCreatePickupRatingDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.Rating < 1.0 || r.Rating > 5.0 { + errors["rating"] = append(errors["rating"], "Rating harus antara 1.0 sampai 5.0") + } + + if len(strings.TrimSpace(r.Feedback)) > 255 { + errors["feedback"] = append(errors["feedback"], "Feedback tidak boleh lebih dari 255 karakter") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/dto/request_pickup_dto.go b/dto/request_pickup_dto.go new file mode 100644 index 0000000..480ed9e --- /dev/null +++ b/dto/request_pickup_dto.go @@ -0,0 +1,85 @@ +package dto + +import ( + "strings" +) + +// type NearbyCollectorDTO struct { +// CollectorID string `json:"collector_id"` +// Name string `json:"name"` +// Phone string `json:"phone"` +// Rating float32 `json:"rating"` +// Latitude float64 `json:"latitude"` +// Longitude float64 `json:"longitude"` +// DistanceKm float64 `json:"distance_km"` +// MatchedTrash []string `json:"matched_trash_ids"` +// } + +type SelectCollectorDTO struct { + CollectorID string `json:"collector_id"` +} + +type UpdateRequestPickupItemDTO struct { + ItemID string `json:"item_id"` + Amount float64 `json:"actual_amount"` +} + +type UpdatePickupItemsRequest struct { + Items []UpdateRequestPickupItemDTO `json:"items"` +} + +func (r *SelectCollectorDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CollectorID) == "" { + errors["collector_id"] = append(errors["collector_id"], "collector_id tidak boleh kosong") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type AssignedPickupDTO struct { + PickupID string `json:"pickup_id"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Notes string `json:"notes"` + MatchedTrash []string `json:"matched_trash"` +} + +type PickupRequestForCollectorDTO struct { + PickupID string `json:"pickup_id"` + UserID string `json:"user_id"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + DistanceKm float64 `json:"distance_km"` + MatchedTrash []string `json:"matched_trash"` +} + +type RequestPickupDTO struct { + AddressID string `json:"address_id"` + RequestMethod string `json:"request_method"` // "manual" atau "otomatis" + Notes string `json:"notes,omitempty"` +} + +func (r *RequestPickupDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.AddressID) == "" { + errors["address_id"] = append(errors["address_id"], "alamat harus dipilih") + } + + method := strings.ToLower(strings.TrimSpace(r.RequestMethod)) + if method != "manual" && method != "otomatis" { + errors["request_method"] = append(errors["request_method"], "harus manual atau otomatis") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/internal/handler/pickup_history_handler.go b/internal/handler/pickup_history_handler.go new file mode 100644 index 0000000..2525f44 --- /dev/null +++ b/internal/handler/pickup_history_handler.go @@ -0,0 +1,37 @@ +package handler + +import ( + "context" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type PickupStatusHistoryHandler interface { + GetStatusHistory(c *fiber.Ctx) error +} + +type pickupStatusHistoryHandler struct { + service services.PickupStatusHistoryService +} + +func NewPickupStatusHistoryHandler(service services.PickupStatusHistoryService) PickupStatusHistoryHandler { + return &pickupStatusHistoryHandler{service: service} +} + +func (h *pickupStatusHistoryHandler) GetStatusHistory(c *fiber.Ctx) error { + pickupID := c.Params("id") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID tidak boleh kosong"}, + }) + } + + histories, err := h.service.GetStatusHistory(context.Background(), pickupID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, histories, "Riwayat status pickup berhasil diambil") +} diff --git a/internal/handler/pickup_matching_handler.go b/internal/handler/pickup_matching_handler.go new file mode 100644 index 0000000..ecb39d9 --- /dev/null +++ b/internal/handler/pickup_matching_handler.go @@ -0,0 +1,49 @@ +package handler + +import ( + "context" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type PickupMatchingHandler interface { + GetNearbyCollectorsForPickup(c *fiber.Ctx) error + GetAvailablePickupForCollector(c *fiber.Ctx) error +} + +type pickupMatchingHandler struct { + service services.PickupMatchingService +} + +func NewPickupMatchingHandler(service services.PickupMatchingService) PickupMatchingHandler { + return &pickupMatchingHandler{service: service} +} + +func (h *pickupMatchingHandler) GetNearbyCollectorsForPickup(c *fiber.Ctx) error { + pickupID := c.Params("pickupID") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID harus disertakan"}, + }) + } + + collectors, err := h.service.FindNearbyCollectorsForPickup(context.Background(), pickupID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, collectors, "Data collector terdekat berhasil diambil") +} + +func (h *pickupMatchingHandler) GetAvailablePickupForCollector(c *fiber.Ctx) error { + collectorID := c.Locals("userID").(string) + + pickups, err := h.service.FindAvailableRequestsForCollector(context.Background(), collectorID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, pickups, "Data request pickup otomatis berhasil diambil") +} diff --git a/internal/handler/rating_handler.go b/internal/handler/rating_handler.go new file mode 100644 index 0000000..6f64916 --- /dev/null +++ b/internal/handler/rating_handler.go @@ -0,0 +1,66 @@ +package handler + +import ( + "context" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type PickupRatingHandler interface { + CreateRating(c *fiber.Ctx) error + GetRatingsByCollector(c *fiber.Ctx) error + GetAverageRating(c *fiber.Ctx) error +} + +type pickupRatingHandler struct { + service services.PickupRatingService +} + +func NewPickupRatingHandler(service services.PickupRatingService) PickupRatingHandler { + return &pickupRatingHandler{service: service} +} + +func (h *pickupRatingHandler) CreateRating(c *fiber.Ctx) error { + pickupID := c.Params("id") + userID := c.Locals("userID").(string) + collectorID := c.Query("collector_id") + + var req dto.CreatePickupRatingDTO + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"Format JSON tidak valid"}, + }) + } + + if errs, ok := req.ValidateCreatePickupRatingDTO(); !ok { + return utils.ValidationErrorResponse(c, errs) + } + + err := h.service.CreateRating(context.Background(), userID, pickupID, collectorID, req) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Rating berhasil dikirim") +} + +func (h *pickupRatingHandler) GetRatingsByCollector(c *fiber.Ctx) error { + collectorID := c.Params("id") + ratings, err := h.service.GetRatingsByCollector(context.Background(), collectorID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + return utils.SuccessResponse(c, ratings, "Daftar rating collector berhasil diambil") +} + +func (h *pickupRatingHandler) GetAverageRating(c *fiber.Ctx) error { + collectorID := c.Params("id") + avg, err := h.service.GetAverageRating(context.Background(), collectorID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + return utils.SuccessResponse(c, fiber.Map{"average_rating": avg}, "Rata-rata rating collector") +} diff --git a/internal/handler/request_pickup_handler.go b/internal/handler/request_pickup_handler.go new file mode 100644 index 0000000..f5c2826 --- /dev/null +++ b/internal/handler/request_pickup_handler.go @@ -0,0 +1,150 @@ +package handler + +import ( + "context" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + "time" + + "github.com/gofiber/fiber/v2" +) + +type RequestPickupHandler interface { + CreateRequestPickup(c *fiber.Ctx) error + SelectCollector(c *fiber.Ctx) error + GetAssignedPickup(c *fiber.Ctx) error + ConfirmPickup(c *fiber.Ctx) error + UpdatePickupStatus(c *fiber.Ctx) error + UpdatePickupItemActualAmount(c *fiber.Ctx) error +} + +type requestPickupHandler struct { + service services.RequestPickupService +} + +func NewRequestPickupHandler(service services.RequestPickupService) RequestPickupHandler { + return &requestPickupHandler{service: service} +} + +func (h *requestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + var req dto.RequestPickupDTO + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if errs, ok := req.Validate(); !ok { + return utils.ValidationErrorResponse(c, errs) + } + + if err := h.service.ConvertCartToRequestPickup(context.Background(), userID, req); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Request pickup berhasil dibuat") +} + +func (h *requestPickupHandler) SelectCollector(c *fiber.Ctx) error { + pickupID := c.Params("id") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID harus disertakan"}, + }) + } + + var req dto.SelectCollectorDTO + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if errs, ok := req.Validate(); !ok { + return utils.ValidationErrorResponse(c, errs) + } + + if err := h.service.AssignCollectorToRequest(context.Background(), pickupID, req); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Collector berhasil dipilih untuk pickup") +} + +func (h *requestPickupHandler) GetAssignedPickup(c *fiber.Ctx) error { + collectorID := c.Locals("userID").(string) + result, err := h.service.FindRequestsAssignedToCollector(context.Background(), collectorID) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + return utils.SuccessResponse(c, result, "Data pickup yang ditugaskan berhasil diambil") +} + +func (h *requestPickupHandler) ConfirmPickup(c *fiber.Ctx) error { + pickupID := c.Params("id") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID wajib diisi"}, + }) + } + + err := h.service.ConfirmPickupByCollector(context.Background(), pickupID, time.Now()) + if err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + return utils.SuccessResponse(c, nil, "Pickup berhasil dikonfirmasi oleh collector") +} + +func (h *requestPickupHandler) UpdatePickupStatus(c *fiber.Ctx) error { + pickupID := c.Params("id") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID tidak boleh kosong"}, + }) + } + + if err := h.service.UpdatePickupStatusToPickingUp(context.Background(), pickupID); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Status pickup berhasil diperbarui menjadi 'collector_are_picking_up'") +} + +func (h *requestPickupHandler) UpdatePickupItemActualAmount(c *fiber.Ctx) error { + pickupID := c.Params("id") + if pickupID == "" { + return utils.ValidationErrorResponse(c, map[string][]string{ + "pickup_id": {"pickup ID tidak boleh kosong"}, + }) + } + + var req dto.UpdatePickupItemsRequest + if err := c.BodyParser(&req); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{ + "body": {"format JSON tidak valid"}, + }) + } + + if len(req.Items) == 0 { + return utils.ValidationErrorResponse(c, map[string][]string{ + "items": {"daftar item tidak boleh kosong"}, + }) + } + + for _, item := range req.Items { + if item.ItemID == "" || item.Amount <= 0 { + return utils.ValidationErrorResponse(c, map[string][]string{ + "item": {"item_id harus valid dan amount > 0"}, + }) + } + } + + if err := h.service.UpdateActualPickupItems(context.Background(), pickupID, req.Items); err != nil { + return utils.InternalServerErrorResponse(c, err.Error()) + } + + return utils.SuccessResponse(c, nil, "Berat aktual dan harga berhasil diperbarui") +} \ No newline at end of file diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go index 9340893..aa41c71 100644 --- a/internal/repositories/collector_repo.go +++ b/internal/repositories/collector_repo.go @@ -4,21 +4,11 @@ import ( "context" "errors" - // "fmt" - - // "log" "rijig/config" "rijig/model" - // "gorm.io/gorm" ) type CollectorRepository interface { - // FindActiveCollectors() ([]model.Collector, error) - // FindCollectorById(collector_id string) (*model.Collector, error) - // FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) - // CreateCollector(collector *model.Collector) error - // UpdateCollector(userId string, jobStatus string) (*model.Collector, error) - CreateCollector(ctx context.Context, collector *model.Collector) error AddAvaibleTrash(ctx context.Context, trashItems []model.AvaibleTrashByCollector) error GetCollectorByID(ctx context.Context, collectorID string) (*model.Collector, error) @@ -27,80 +17,18 @@ type CollectorRepository interface { UpdateCollector(ctx context.Context, collector *model.Collector, updates map[string]interface{}) error UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []model.AvaibleTrashByCollector) error DeleteAvaibleTrash(ctx context.Context, trashID string) error + + GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) + GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) } type collectorRepository struct { - // DB *gorm.DB } -// func NewCollectorRepository(db *gorm.DB) CollectorRepository { -// return &collectorRepository{DB: db} -// } func NewCollectorRepository() CollectorRepository { return &collectorRepository{} } -// func (r *collectorRepository) FindActiveCollectors() ([]model.Collector, error) { -// var collectors []model.Collector - -// err := r.DB.Preload("Address").Where("job_status = ?", "active").First(&collectors).Error -// if err != nil { -// return nil, fmt.Errorf("failed to fetch active collectors: %v", err) -// } - -// return collectors, nil -// } - -// func (r *collectorRepository) FindCollectorById(collector_id string) (*model.Collector, error) { -// var collector model.Collector -// err := r.DB.Preload("Address").Where("user_id = ?", collector_id).First(&collector).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching collector: %v", err) -// } -// fmt.Printf("menampilkan data collector %v", &collector) -// return &collector, nil -// } - -// func (r *collectorRepository) FindCollectorByIdWithoutAddr(collector_id string) (*model.Collector, error) { -// var collector model.Collector -// err := r.DB.Where("user_id = ?", collector_id).First(&collector).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching collector: %v", err) -// } -// fmt.Printf("menampilkan data collector %v", &collector) -// return &collector, nil -// } - -// func (r *collectorRepository) CreateCollector(collector *model.Collector) error { -// if err := r.DB.Create(collector).Error; err != nil { -// return fmt.Errorf("failed to create collector: %v", err) -// } -// return nil -// } - -// func (r *collectorRepository) UpdateCollector(userId string, jobStatus string) (*model.Collector, error) { -// var existingCollector model.Collector - -// if err := r.DB.Where("user_id = ?", userId).First(&existingCollector).Error; err != nil { -// if errors.Is(err, gorm.ErrRecordNotFound) { -// return nil, fmt.Errorf("collector dengan user_id %s tidak ditemukan", userId) -// } -// log.Printf("Gagal mencari collector: %v", err) -// return nil, fmt.Errorf("gagal fetching collector: %w", err) -// } - -// if jobStatus != "active" && jobStatus != "nonactive" { -// return nil, fmt.Errorf("invalid job status: %v", jobStatus) -// } - -// if err := r.DB.Model(&existingCollector).Update("jobstatus", jobStatus).Error; err != nil { -// log.Printf("Gagal mengupdate data collector: %v", err) -// return nil, fmt.Errorf("gagal mengupdate job status untuk collector: %w", err) -// } - -// return &existingCollector, nil -// } - func (r *collectorRepository) CreateCollector(ctx context.Context, collector *model.Collector) error { return config.DB.WithContext(ctx).Create(collector).Error } @@ -175,3 +103,35 @@ func (r *collectorRepository) DeleteAvaibleTrash(ctx context.Context, trashID st return config.DB.WithContext(ctx). Delete(&model.AvaibleTrashByCollector{}, "id = ?", trashID).Error } + + +// +func (r *collectorRepository) GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) { + var collectors []model.Collector + err := config.DB.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("AvaibleTrashbyCollector.TrashCategory"). + Where("job_status = ?", "active"). + Find(&collectors).Error + + if err != nil { + return nil, err + } + + return collectors, nil +} + +func (r *collectorRepository) GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) { + var collector model.Collector + err := config.DB.WithContext(ctx). + Preload("Address"). + Preload("AvaibleTrashbyCollector"). + Where("id = ?", collectorID). + First(&collector).Error + + if err != nil { + return nil, err + } + return &collector, nil +} diff --git a/internal/repositories/pickup_history_repo.go b/internal/repositories/pickup_history_repo.go new file mode 100644 index 0000000..536ecf7 --- /dev/null +++ b/internal/repositories/pickup_history_repo.go @@ -0,0 +1,34 @@ +package repositories + +import ( + "context" + "rijig/config" + "rijig/model" +) + +type PickupStatusHistoryRepository interface { + CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error + GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) +} + +type pickupStatusHistoryRepository struct{} + +func NewPickupStatusHistoryRepository() PickupStatusHistoryRepository { + return &pickupStatusHistoryRepository{} +} + +func (r *pickupStatusHistoryRepository) CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error { + return config.DB.WithContext(ctx).Create(&history).Error +} + +func (r *pickupStatusHistoryRepository) GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) { + var histories []model.PickupStatusHistory + err := config.DB.WithContext(ctx). + Where("request_id = ?", requestID). + Order("changed_at asc"). + Find(&histories).Error + if err != nil { + return nil, err + } + return histories, nil +} diff --git a/internal/repositories/rating_repo.go b/internal/repositories/rating_repo.go new file mode 100644 index 0000000..b593cd8 --- /dev/null +++ b/internal/repositories/rating_repo.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "context" + "rijig/config" + "rijig/model" +) + +type PickupRatingRepository interface { + CreateRating(ctx context.Context, rating model.PickupRating) error + GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) + CalculateAverageRating(ctx context.Context, collectorID string) (float32, error) +} + +type pickupRatingRepository struct{} + +func NewPickupRatingRepository() PickupRatingRepository { + return &pickupRatingRepository{} +} + +func (r *pickupRatingRepository) CreateRating(ctx context.Context, rating model.PickupRating) error { + return config.DB.WithContext(ctx).Create(&rating).Error +} + +func (r *pickupRatingRepository) GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) { + var ratings []model.PickupRating + err := config.DB.WithContext(ctx). + Where("collector_id = ?", collectorID). + Order("created_at desc"). + Find(&ratings).Error + if err != nil { + return nil, err + } + return ratings, nil +} + +func (r *pickupRatingRepository) CalculateAverageRating(ctx context.Context, collectorID string) (float32, error) { + var avg float32 + err := config.DB.WithContext(ctx). + Model(&model.PickupRating{}). + Select("AVG(rating)"). + Where("collector_id = ?", collectorID). + Scan(&avg).Error + if err != nil { + return 0, err + } + return avg, nil +} diff --git a/internal/repositories/request_pickup_repo.go b/internal/repositories/request_pickup_repo.go new file mode 100644 index 0000000..25f3225 --- /dev/null +++ b/internal/repositories/request_pickup_repo.go @@ -0,0 +1,143 @@ +package repositories + +import ( + "context" + "rijig/config" + "rijig/dto" + "rijig/model" + "time" +) + +type RequestPickupRepository interface { + CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error + GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) + GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) + UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error + GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) + UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error + UpdatePickupStatus(ctx context.Context, pickupID string, status string) error + UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error +} + +type requestPickupRepository struct{} + +func NewRequestPickupRepository() RequestPickupRepository { + return &requestPickupRepository{} +} + +func (r *requestPickupRepository) CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error { + return config.DB.WithContext(ctx).Create(pickup).Error +} + +func (r *requestPickupRepository) GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) { + var pickup model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("RequestItems"). + Preload("Address"). + Where("id = ?", id). + First(&pickup).Error + + if err != nil { + return nil, err + } + return &pickup, nil +} + +func (r *requestPickupRepository) UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Update("collector_id", collectorID). + Error +} + +func (r *requestPickupRepository) GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) { + var pickups []model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("RequestItems"). + Preload("Address"). + Where("request_method = ?", "otomatis"). + Find(&pickups).Error + + if err != nil { + return nil, err + } + return pickups, nil +} + +func (r *requestPickupRepository) GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) { + var pickups []model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("RequestItems"). + Where("collector_id = ? AND status_pickup = ?", collectorID, "waiting_collector"). + Find(&pickups).Error + + if err != nil { + return nil, err + } + return pickups, nil +} + +func (r *requestPickupRepository) UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Updates(map[string]interface{}{ + "status_pickup": status, + "confirmed_by_collector_at": confirmedAt, + }).Error +} + +func (r *requestPickupRepository) UpdatePickupStatus(ctx context.Context, pickupID string, status string) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Update("status_pickup", status). + Error +} + +func (r *requestPickupRepository) UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error { + // ambil collector_id dulu dari pickup + var pickup model.RequestPickup + if err := config.DB.WithContext(ctx). + Select("collector_id"). + Where("id = ?", pickupID). + First(&pickup).Error; err != nil { + return err + } + + for _, item := range items { + var pickupItem model.RequestPickupItem + err := config.DB.WithContext(ctx). + Where("id = ? AND request_pickup_id = ?", item.ItemID, pickupID). + First(&pickupItem).Error + if err != nil { + return err + } + + var price float64 + err = config.DB.WithContext(ctx). + Model(&model.AvaibleTrashByCollector{}). + Where("collector_id = ? AND trash_category_id = ?", pickup.CollectorID, pickupItem.TrashCategoryId). + Select("price"). + Scan(&price).Error + if err != nil { + return err + } + + finalPrice := item.Amount * price + err = config.DB.WithContext(ctx). + Model(&model.RequestPickupItem{}). + Where("id = ?", item.ItemID). + Updates(map[string]interface{}{ + "estimated_amount": item.Amount, + "final_price": finalPrice, + }).Error + if err != nil { + return err + } + } + return nil +} diff --git a/internal/repositories/requestpickup_repo.go b/internal/repositories/requestpickup_repo.go index 76fd64e..9c6a57a 100644 --- a/internal/repositories/requestpickup_repo.go +++ b/internal/repositories/requestpickup_repo.go @@ -1,181 +1,181 @@ package repositories -import ( - "fmt" - "rijig/model" +// import ( +// "fmt" +// "rijig/model" - "gorm.io/gorm" -) +// "gorm.io/gorm" +// ) -type RequestPickupRepository interface { - CreateRequestPickup(request *model.RequestPickup) error - CreateRequestPickupItem(item *model.RequestPickupItem) error - FindRequestPickupByID(id string) (*model.RequestPickup, error) - FindAllRequestPickups(userId string) ([]model.RequestPickup, error) - FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) - FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) - FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) - GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) - GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) - GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) - // SelectCollectorInRequest(userId string, collectorId string) error - UpdateRequestPickup(id string, request *model.RequestPickup) error - DeleteRequestPickup(id string) error -} +// type RequestPickupRepository interface { +// CreateRequestPickup(request *model.RequestPickup) error +// CreateRequestPickupItem(item *model.RequestPickupItem) error +// FindRequestPickupByID(id string) (*model.RequestPickup, error) +// FindAllRequestPickups(userId string) ([]model.RequestPickup, error) +// FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) +// FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) +// FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) +// GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) +// GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) +// GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) +// // SelectCollectorInRequest(userId string, collectorId string) error +// UpdateRequestPickup(id string, request *model.RequestPickup) error +// DeleteRequestPickup(id string) error +// } -type requestPickupRepository struct { - DB *gorm.DB -} +// type requestPickupRepository struct { +// DB *gorm.DB +// } -func NewRequestPickupRepository(db *gorm.DB) RequestPickupRepository { - return &requestPickupRepository{DB: db} -} +// func NewRequestPickupRepository(db *gorm.DB) RequestPickupRepository { +// return &requestPickupRepository{DB: db} +// } -func (r *requestPickupRepository) CreateRequestPickup(request *model.RequestPickup) error { - if err := r.DB.Create(request).Error; err != nil { - return fmt.Errorf("failed to create request pickup: %v", err) - } - - for _, item := range request.RequestItems { - item.RequestPickupId = request.ID - if err := r.DB.Create(&item).Error; err != nil { - return fmt.Errorf("failed to create request pickup item: %v", err) - } - } - - return nil -} - -func (r *requestPickupRepository) CreateRequestPickupItem(item *model.RequestPickupItem) error { - if err := r.DB.Create(item).Error; err != nil { - return fmt.Errorf("failed to create request pickup item: %v", err) - } - return nil -} - -func (r *requestPickupRepository) FindRequestPickupByID(id string) (*model.RequestPickup, error) { - var request model.RequestPickup - err := r.DB.Preload("RequestItems").First(&request, "id = ?", id).Error - if err != nil { - return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) - } - return &request, nil -} - -func (r *requestPickupRepository) FindAllRequestPickups(userId string) ([]model.RequestPickup, error) { - var requests []model.RequestPickup - err := r.DB.Preload("RequestItems").Where("user_id = ?", userId).Find(&requests).Error - if err != nil { - return nil, fmt.Errorf("failed to fetch all request pickups: %v", err) - } - return requests, nil -} - -func (r *requestPickupRepository) FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) { - var requests []model.RequestPickup - err := r.DB.Preload("RequestItems").Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup).Find(&requests).Error - if err != nil { - return nil, fmt.Errorf("error fetching request pickups with request_method %s: %v", requestMethod, err) - } - - return requests, nil -} - -func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) { - var request model.RequestPickup - err := r.DB.Preload("Address").Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, fmt.Errorf("failed to check existing request pickup: %v", err) - } - return &request, nil -} - -func (r *requestPickupRepository) FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) { - var request model.RequestPickup - err := r.DB.Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, fmt.Errorf("failed to check existing request pickup: %v", err) - } - return &request, nil -} - -func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model.RequestPickup) error { - err := r.DB.Model(&model.RequestPickup{}).Where("id = ?", id).Updates(request).Error - if err != nil { - return fmt.Errorf("failed to update request pickup: %v", err) - } - - return nil -} - -// func (r *requestPickupRepository) SelectCollectorInRequest(userId string, collectorId string) error { -// var request model.RequestPickup -// err := r.DB.Model(&model.RequestPickup{}). -// Where("user_id = ? AND status_pickup = ? AND request_method = ? AND collector_id IS NULL", userId, "waiting_collector", "manual"). -// First(&request).Error -// if err != nil { -// if err == gorm.ErrRecordNotFound { -// return fmt.Errorf("no matching request pickup found for user %s", userId) -// } -// return fmt.Errorf("failed to find request pickup: %v", err) +// func (r *requestPickupRepository) CreateRequestPickup(request *model.RequestPickup) error { +// if err := r.DB.Create(request).Error; err != nil { +// return fmt.Errorf("failed to create request pickup: %v", err) // } -// err = r.DB.Model(&model.RequestPickup{}). -// Where("id = ?", request.ID). -// Update("collector_id", collectorId). -// Error -// if err != nil { -// return fmt.Errorf("failed to update collector_id: %v", err) +// for _, item := range request.RequestItems { +// item.RequestPickupId = request.ID +// if err := r.DB.Create(&item).Error; err != nil { +// return fmt.Errorf("failed to create request pickup item: %v", err) +// } +// } + +// return nil +// } + +// func (r *requestPickupRepository) CreateRequestPickupItem(item *model.RequestPickupItem) error { +// if err := r.DB.Create(item).Error; err != nil { +// return fmt.Errorf("failed to create request pickup item: %v", err) // } // return nil // } -func (r *requestPickupRepository) DeleteRequestPickup(id string) error { +// func (r *requestPickupRepository) FindRequestPickupByID(id string) (*model.RequestPickup, error) { +// var request model.RequestPickup +// err := r.DB.Preload("RequestItems").First(&request, "id = ?", id).Error +// if err != nil { +// return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) +// } +// return &request, nil +// } - if err := r.DB.Where("request_pickup_id = ?", id).Delete(&model.RequestPickupItem{}).Error; err != nil { - return fmt.Errorf("failed to delete request pickup items: %v", err) - } +// func (r *requestPickupRepository) FindAllRequestPickups(userId string) ([]model.RequestPickup, error) { +// var requests []model.RequestPickup +// err := r.DB.Preload("RequestItems").Where("user_id = ?", userId).Find(&requests).Error +// if err != nil { +// return nil, fmt.Errorf("failed to fetch all request pickups: %v", err) +// } +// return requests, nil +// } - err := r.DB.Delete(&model.RequestPickup{}, "id = ?", id).Error - if err != nil { - return fmt.Errorf("failed to delete request pickup: %v", err) - } - return nil -} +// func (r *requestPickupRepository) FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) { +// var requests []model.RequestPickup +// err := r.DB.Preload("RequestItems").Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup).Find(&requests).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching request pickups with request_method %s: %v", requestMethod, err) +// } -func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) { - var requests []model.RequestPickup - err := r.DB.Preload("Address"). - Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", nil). - Find(&requests).Error - if err != nil { - return nil, fmt.Errorf("error fetching pickup requests: %v", err) - } - return requests, nil -} +// return requests, nil +// } -func (r *requestPickupRepository) GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) { - var requests []model.RequestPickup - err := r.DB.Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", collector_id). - Find(&requests).Error - if err != nil { - return nil, fmt.Errorf("error fetching pickup requests: %v", err) - } - return requests, nil -} +// func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) { +// var request model.RequestPickup +// err := r.DB.Preload("Address").Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error +// if err != nil { +// if err == gorm.ErrRecordNotFound { +// return nil, nil +// } +// return nil, fmt.Errorf("failed to check existing request pickup: %v", err) +// } +// return &request, nil +// } -func (r *requestPickupRepository) GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) { - var items []model.RequestPickupItem +// func (r *requestPickupRepository) FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) { +// var request model.RequestPickup +// err := r.DB.Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error +// if err != nil { +// if err == gorm.ErrRecordNotFound { +// return nil, nil +// } +// return nil, fmt.Errorf("failed to check existing request pickup: %v", err) +// } +// return &request, nil +// } - err := r.DB.Preload("TrashCategory").Where("request_pickup_id = ?", requestPickupId).Find(&items).Error - if err != nil { - return nil, fmt.Errorf("error fetching request pickup items: %v", err) - } - return items, nil -} +// func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model.RequestPickup) error { +// err := r.DB.Model(&model.RequestPickup{}).Where("id = ?", id).Updates(request).Error +// if err != nil { +// return fmt.Errorf("failed to update request pickup: %v", err) +// } + +// return nil +// } + +// // func (r *requestPickupRepository) SelectCollectorInRequest(userId string, collectorId string) error { +// // var request model.RequestPickup +// // err := r.DB.Model(&model.RequestPickup{}). +// // Where("user_id = ? AND status_pickup = ? AND request_method = ? AND collector_id IS NULL", userId, "waiting_collector", "manual"). +// // First(&request).Error +// // if err != nil { +// // if err == gorm.ErrRecordNotFound { +// // return fmt.Errorf("no matching request pickup found for user %s", userId) +// // } +// // return fmt.Errorf("failed to find request pickup: %v", err) +// // } + +// // err = r.DB.Model(&model.RequestPickup{}). +// // Where("id = ?", request.ID). +// // Update("collector_id", collectorId). +// // Error +// // if err != nil { +// // return fmt.Errorf("failed to update collector_id: %v", err) +// // } +// // return nil +// // } + +// func (r *requestPickupRepository) DeleteRequestPickup(id string) error { + +// if err := r.DB.Where("request_pickup_id = ?", id).Delete(&model.RequestPickupItem{}).Error; err != nil { +// return fmt.Errorf("failed to delete request pickup items: %v", err) +// } + +// err := r.DB.Delete(&model.RequestPickup{}, "id = ?", id).Error +// if err != nil { +// return fmt.Errorf("failed to delete request pickup: %v", err) +// } +// return nil +// } + +// func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) { +// var requests []model.RequestPickup +// err := r.DB.Preload("Address"). +// Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", nil). +// Find(&requests).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching pickup requests: %v", err) +// } +// return requests, nil +// } + +// func (r *requestPickupRepository) GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) { +// var requests []model.RequestPickup +// err := r.DB.Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", collector_id). +// Find(&requests).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching pickup requests: %v", err) +// } +// return requests, nil +// } + +// func (r *requestPickupRepository) GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) { +// var items []model.RequestPickupItem + +// err := r.DB.Preload("TrashCategory").Where("request_pickup_id = ?", requestPickupId).Find(&items).Error +// if err != nil { +// return nil, fmt.Errorf("error fetching request pickup items: %v", err) +// } +// return items, nil +// } diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 4b88d35..53815f9 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -20,6 +20,7 @@ type TrashRepository interface { FindCategoryId(id string) (*model.TrashCategory, error) GetTrashCategoryByName(name string) (*model.TrashCategory, error) GetTrashDetailByID(id string) (*model.TrashDetail, error) + GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) UpdateCategoryName(id string, newName string) error UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) @@ -68,6 +69,15 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } +// spesial code +func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { + var trash model.TrashCategory + if err := config.DB.WithContext(ctx).First(&trash, "id = ?", id).Error; err != nil { + return nil, err + } + return &trash, nil +} + func (r *trashRepository) FindCategoryId(id string) (*model.TrashCategory, error) { var category model.TrashCategory diff --git a/internal/services/pickup_history_service.go b/internal/services/pickup_history_service.go new file mode 100644 index 0000000..5362664 --- /dev/null +++ b/internal/services/pickup_history_service.go @@ -0,0 +1,36 @@ +package services + +import ( + "context" + "time" + "rijig/model" + "rijig/internal/repositories" +) + +type PickupStatusHistoryService interface { + LogStatusChange(ctx context.Context, requestID, status, changedByID, changedByRole string) error + GetStatusHistory(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) +} + +type pickupStatusHistoryService struct { + repo repositories.PickupStatusHistoryRepository +} + +func NewPickupStatusHistoryService(repo repositories.PickupStatusHistoryRepository) PickupStatusHistoryService { + return &pickupStatusHistoryService{repo: repo} +} + +func (s *pickupStatusHistoryService) LogStatusChange(ctx context.Context, requestID, status, changedByID, changedByRole string) error { + history := model.PickupStatusHistory{ + RequestID: requestID, + Status: status, + ChangedAt: time.Now(), + ChangedByID: changedByID, + ChangedByRole: changedByRole, + } + return s.repo.CreateStatusHistory(ctx, history) +} + +func (s *pickupStatusHistoryService) GetStatusHistory(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) { + return s.repo.GetStatusHistoryByRequestID(ctx, requestID) +} diff --git a/internal/services/pickup_maching_service.go b/internal/services/pickup_maching_service.go new file mode 100644 index 0000000..ae36e1a --- /dev/null +++ b/internal/services/pickup_maching_service.go @@ -0,0 +1,146 @@ +package services + +import ( + "context" + "fmt" + "rijig/dto" + "rijig/internal/repositories" + "rijig/utils" +) + +type PickupMatchingService interface { + FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]dto.NearbyCollectorDTO, error) + FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]dto.PickupRequestForCollectorDTO, error) +} + +type pickupMatchingService struct { + pickupRepo repositories.RequestPickupRepository + collectorRepo repositories.CollectorRepository +} + +func NewPickupMatchingService(pickupRepo repositories.RequestPickupRepository, collectorRepo repositories.CollectorRepository) PickupMatchingService { + return &pickupMatchingService{ + pickupRepo: pickupRepo, + collectorRepo: collectorRepo, + } +} + +func (s *pickupMatchingService) FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]dto.NearbyCollectorDTO, error) { + pickup, err := s.pickupRepo.GetPickupWithItemsAndAddress(ctx, pickupID) + if err != nil { + return nil, fmt.Errorf("pickup tidak ditemukan: %w", err) + } + + userCoord := utils.Coord{ + Lat: pickup.Address.Latitude, + Lon: pickup.Address.Longitude, + } + + requestedTrash := make(map[string]bool) + for _, item := range pickup.RequestItems { + requestedTrash[item.TrashCategoryId] = true + } + + collectors, err := s.collectorRepo.GetActiveCollectorsWithTrashAndAddress(ctx) + if err != nil { + return nil, fmt.Errorf("gagal mengambil data collector: %w", err) + } + + var result []dto.NearbyCollectorDTO + for _, col := range collectors { + coord := utils.Coord{ + Lat: col.Address.Latitude, + Lon: col.Address.Longitude, + } + + _, km := utils.Distance(userCoord, coord) + if km > 10 { + continue + } + + var matchedTrash []string + for _, item := range col.AvaibleTrashByCollector { + if requestedTrash[item.TrashCategoryID] { + matchedTrash = append(matchedTrash, item.TrashCategoryID) + } + } + + if len(matchedTrash) == 0 { + continue + } + + result = append(result, dto.NearbyCollectorDTO{ + CollectorID: col.ID, + Name: col.User.Name, + Phone: col.User.Phone, + Rating: col.Rating, + Latitude: col.Address.Latitude, + Longitude: col.Address.Longitude, + DistanceKm: km, + MatchedTrash: matchedTrash, + }) + } + + return result, nil +} + +// terdpaat error seperti ini: "undefined: dto.PickupRequestForCollectorDTO" dan seprti ini: s.collectorRepo.GetCollectorWithAddressAndTrash undefined (type repositories.CollectorRepository has no field or method GetCollectorWithAddressAndTrash) pada kode berikut: + +func (s *pickupMatchingService) FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]dto.PickupRequestForCollectorDTO, error) { + collector, err := s.collectorRepo.GetCollectorWithAddressAndTrash(ctx, collectorID) + if err != nil { + return nil, fmt.Errorf("collector tidak ditemukan: %w", err) + } + + pickupList, err := s.pickupRepo.GetAllAutomaticRequestsWithAddress(ctx) + if err != nil { + return nil, fmt.Errorf("gagal mengambil pickup otomatis: %w", err) + } + + collectorCoord := utils.Coord{ + Lat: collector.Address.Latitude, + Lon: collector.Address.Longitude, + } + + // map trash collector + collectorTrash := make(map[string]bool) + for _, t := range collector.AvaibleTrashByCollector { + collectorTrash[t.TrashCategoryID] = true + } + + var results []dto.PickupRequestForCollectorDTO + for _, p := range pickupList { + if p.StatusPickup != "waiting_collector" { + continue + } + coord := utils.Coord{ + Lat: p.Address.Latitude, + Lon: p.Address.Longitude, + } + _, km := utils.Distance(collectorCoord, coord) + if km > 10 { + continue + } + + match := false + var matchedTrash []string + for _, item := range p.RequestItems { + if collectorTrash[item.TrashCategoryId] { + match = true + matchedTrash = append(matchedTrash, item.TrashCategoryId) + } + } + if match { + results = append(results, dto.PickupRequestForCollectorDTO{ + PickupID: p.ID, + UserID: p.UserId, + Latitude: p.Address.Latitude, + Longitude: p.Address.Longitude, + DistanceKm: km, + MatchedTrash: matchedTrash, + }) + } + } + + return results, nil +} diff --git a/internal/services/rating_service.go b/internal/services/rating_service.go new file mode 100644 index 0000000..e5d7c55 --- /dev/null +++ b/internal/services/rating_service.go @@ -0,0 +1,43 @@ +package services + +import ( + "context" + "time" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" +) + +type PickupRatingService interface { + CreateRating(ctx context.Context, userID, pickupID, collectorID string, input dto.CreatePickupRatingDTO) error + GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) + GetAverageRating(ctx context.Context, collectorID string) (float32, error) +} + +type pickupRatingService struct { + repo repositories.PickupRatingRepository +} + +func NewPickupRatingService(repo repositories.PickupRatingRepository) PickupRatingService { + return &pickupRatingService{repo: repo} +} + +func (s *pickupRatingService) CreateRating(ctx context.Context, userID, pickupID, collectorID string, input dto.CreatePickupRatingDTO) error { + rating := model.PickupRating{ + RequestID: pickupID, + UserID: userID, + CollectorID: collectorID, + Rating: input.Rating, + Feedback: input.Feedback, + CreatedAt: time.Now(), + } + return s.repo.CreateRating(ctx, rating) +} + +func (s *pickupRatingService) GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) { + return s.repo.GetRatingsByCollector(ctx, collectorID) +} + +func (s *pickupRatingService) GetAverageRating(ctx context.Context, collectorID string) (float32, error) { + return s.repo.CalculateAverageRating(ctx, collectorID) +} diff --git a/internal/services/request_pickup_service.go b/internal/services/request_pickup_service.go new file mode 100644 index 0000000..a5eb89f --- /dev/null +++ b/internal/services/request_pickup_service.go @@ -0,0 +1,139 @@ +package services + +import ( + "context" + "fmt" + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" + "time" +) + +type RequestPickupService interface { + ConvertCartToRequestPickup(ctx context.Context, userID string, req dto.RequestPickupDTO) error + AssignCollectorToRequest(ctx context.Context, pickupID string, req dto.SelectCollectorDTO) error + FindRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]dto.AssignedPickupDTO, error) + ConfirmPickupByCollector(ctx context.Context, pickupID string, confirmedAt time.Time) error + UpdatePickupStatusToPickingUp(ctx context.Context, pickupID string) error + UpdateActualPickupItems(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error +} + +type requestPickupService struct { + trashRepo repositories.TrashRepository + pickupRepo repositories.RequestPickupRepository + cartService CartService + historyRepo repositories.PickupStatusHistoryRepository +} + +func NewRequestPickupService(trashRepo repositories.TrashRepository, pickupRepo repositories.RequestPickupRepository, cartService CartService, historyRepo repositories.PickupStatusHistoryRepository) RequestPickupService { + return &requestPickupService{ + trashRepo: trashRepo, + pickupRepo: pickupRepo, + cartService: cartService, + historyRepo: historyRepo, + } +} + +func (s *requestPickupService) ConvertCartToRequestPickup(ctx context.Context, userID string, req dto.RequestPickupDTO) error { + cart, err := s.cartService.GetCart(ctx, userID) + if err != nil || len(cart.CartItems) == 0 { + return fmt.Errorf("cart kosong atau tidak ditemukan") + } + + var requestItems []model.RequestPickupItem + for _, item := range cart.CartItems { + trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + continue + } + subtotal := float64(item.Amount) * trash.EstimatedPrice + + requestItems = append(requestItems, model.RequestPickupItem{ + TrashCategoryId: item.TrashID, + EstimatedAmount: float64(item.Amount), + EstimatedPricePerKg: trash.EstimatedPrice, + EstimatedSubtotalPrice: subtotal, + }) + } + + if len(requestItems) == 0 { + return fmt.Errorf("tidak ada item valid dalam cart") + } + + pickup := model.RequestPickup{ + UserId: userID, + AddressId: req.AddressID, + RequestMethod: req.RequestMethod, + Notes: req.Notes, + StatusPickup: "waiting_collector", + RequestItems: requestItems, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + if err := s.pickupRepo.CreateRequestPickup(ctx, &pickup); err != nil { + return fmt.Errorf("gagal menyimpan request pickup: %w", err) + } + + if err := s.cartService.ClearCart(ctx, userID); err != nil { + return fmt.Errorf("request berhasil, tapi gagal hapus cart: %w", err) + } + + return nil +} + +func (s *requestPickupService) AssignCollectorToRequest(ctx context.Context, pickupID string, req dto.SelectCollectorDTO) error { + if req.CollectorID == "" { + return fmt.Errorf("collector_id tidak boleh kosong") + } + return s.pickupRepo.UpdateCollectorID(ctx, pickupID, req.CollectorID) +} + +func (s *requestPickupService) FindRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]dto.AssignedPickupDTO, error) { + pickups, err := s.pickupRepo.GetRequestsAssignedToCollector(ctx, collectorID) + if err != nil { + return nil, err + } + + var result []dto.AssignedPickupDTO + for _, p := range pickups { + var matchedTrash []string + for _, item := range p.RequestItems { + matchedTrash = append(matchedTrash, item.TrashCategoryId) + } + + result = append(result, dto.AssignedPickupDTO{ + PickupID: p.ID, + UserID: p.UserId, + UserName: p.User.Name, + Latitude: p.Address.Latitude, + Longitude: p.Address.Longitude, + Notes: p.Notes, + MatchedTrash: matchedTrash, + }) + } + + return result, nil +} + +func (s *requestPickupService) ConfirmPickupByCollector(ctx context.Context, pickupID string, confirmedAt time.Time) error { + return s.pickupRepo.UpdatePickupStatusAndConfirmationTime(ctx, pickupID, "confirmed_by_collector", confirmedAt) +} + +func (s *requestPickupService) UpdatePickupStatusToPickingUp(ctx context.Context, pickupID string) error { + err := s.pickupRepo.UpdatePickupStatus(ctx, pickupID, "collector_are_picking_up") + if err != nil { + return err + } + return s.historyRepo.CreateStatusHistory(ctx, model.PickupStatusHistory{ + RequestID: pickupID, + Status: "collector_are_picking_up", + ChangedAt: time.Now(), + ChangedByID: "collector", + ChangedByRole: "collector", + }) +} + +func (s *requestPickupService) UpdateActualPickupItems(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error { + return s.pickupRepo.UpdateRequestPickupItemsAmountAndPrice(ctx, pickupID, items) +} diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go index 8656d06..63917c0 100644 --- a/internal/services/requestpickup_service.go +++ b/internal/services/requestpickup_service.go @@ -1,349 +1,349 @@ package services -import ( - "fmt" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) +// import ( +// "fmt" +// "rijig/dto" +// "rijig/internal/repositories" +// "rijig/model" +// "rijig/utils" +// ) -type RequestPickupService interface { - // CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) - // GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) - // GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) - // GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) - // SelectCollectorInRequest(userId, collectorId string) error -} +// type RequestPickupService interface { +// // CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) +// // GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) +// // GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) +// // GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) +// // SelectCollectorInRequest(userId, collectorId string) error +// } -type requestPickupService struct { - repo repositories.RequestPickupRepository - repoColl repositories.CollectorRepository - repoAddress repositories.AddressRepository - repoTrash repositories.TrashRepository - repoUser repositories.UserProfilRepository -} +// type requestPickupService struct { +// repo repositories.RequestPickupRepository +// repoColl repositories.CollectorRepository +// repoAddress repositories.AddressRepository +// repoTrash repositories.TrashRepository +// repoUser repositories.UserProfilRepository +// } -func NewRequestPickupService(repo repositories.RequestPickupRepository, - repoColl repositories.CollectorRepository, - repoAddress repositories.AddressRepository, - repoTrash repositories.TrashRepository, - repoUser repositories.UserProfilRepository) RequestPickupService { - return &requestPickupService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoTrash: repoTrash, repoUser: repoUser} -} +// func NewRequestPickupService(repo repositories.RequestPickupRepository, +// repoColl repositories.CollectorRepository, +// repoAddress repositories.AddressRepository, +// repoTrash repositories.TrashRepository, +// repoUser repositories.UserProfilRepository) RequestPickupService { +// return &requestPickupService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoTrash: repoTrash, repoUser: repoUser} +// } -func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) { +// func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) { - errors, valid := request.ValidateRequestPickup() - if !valid { - return nil, fmt.Errorf("validation errors: %v", errors) - } - - _, err := s.repoAddress.FindAddressByID(request.AddressID) - if err != nil { - return nil, fmt.Errorf("address with ID %s not found", request.AddressID) - } - - existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector", "otomatis") - if err != nil { - return nil, fmt.Errorf("error checking for existing request pickup: %v", err) - } - if existingRequest != nil { - return nil, fmt.Errorf("there is already a pending pickup request for address %s", request.AddressID) - } - - modelRequest := model.RequestPickup{ - UserId: UserId, - AddressId: request.AddressID, - EvidenceImage: request.EvidenceImage, - RequestMethod: request.RequestMethod, - } - - err = s.repo.CreateRequestPickup(&modelRequest) - if err != nil { - return nil, fmt.Errorf("failed to create request pickup: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.UpdatedAt) - - response := &dto.ResponseRequestPickup{ - ID: modelRequest.ID, - UserId: UserId, - AddressID: modelRequest.AddressId, - EvidenceImage: modelRequest.EvidenceImage, - StatusPickup: modelRequest.StatusPickup, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - for _, item := range request.RequestItems { - - findTrashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryID) - if err != nil { - return nil, fmt.Errorf("trash category with ID %s not found", item.TrashCategoryID) - } - - modelItem := model.RequestPickupItem{ - RequestPickupId: modelRequest.ID, - TrashCategoryId: findTrashCategory.ID, - EstimatedAmount: item.EstimatedAmount, - } - err = s.repo.CreateRequestPickupItem(&modelItem) - if err != nil { - return nil, fmt.Errorf("failed to create request pickup item: %v", err) - } - - response.RequestItems = append(response.RequestItems, dto.ResponseRequestPickupItem{ - ID: modelItem.ID, - TrashCategory: []dto.ResponseTrashCategoryDTO{{Name: findTrashCategory.Name, Icon: findTrashCategory.Icon}}, - EstimatedAmount: modelItem.EstimatedAmount, - }) - } - - return response, nil -} - -func (s *requestPickupService) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) { - - request, err := s.repo.FindRequestPickupByID(id) - if err != nil { - return nil, fmt.Errorf("error fetching request pickup with ID %s: %v", id, err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) - - response := &dto.ResponseRequestPickup{ - ID: request.ID, - UserId: request.UserId, - AddressID: request.AddressId, - EvidenceImage: request.EvidenceImage, - StatusPickup: request.StatusPickup, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return response, nil -} - -func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) { - - requests, err := s.repo.FindAllRequestPickups(userid) - if err != nil { - return nil, fmt.Errorf("error fetching all request pickups: %v", err) - } - - var response []dto.ResponseRequestPickup - for _, request := range requests { - createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) - response = append(response, dto.ResponseRequestPickup{ - ID: request.ID, - UserId: request.UserId, - AddressID: request.AddressId, - EvidenceImage: request.EvidenceImage, - StatusPickup: request.StatusPickup, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - return response, nil -} - -// func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { -// requests, err := s.repo.GetAutomaticRequestPickupsForCollector() -// if err != nil { -// return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) +// errors, valid := request.ValidateRequestPickup() +// if !valid { +// return nil, fmt.Errorf("validation errors: %v", errors) // } -// var response []dto.ResponseRequestPickup +// _, err := s.repoAddress.FindAddressByID(request.AddressID) +// if err != nil { +// return nil, fmt.Errorf("address with ID %s not found", request.AddressID) +// } -// for _, req := range requests { +// existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector", "otomatis") +// if err != nil { +// return nil, fmt.Errorf("error checking for existing request pickup: %v", err) +// } +// if existingRequest != nil { +// return nil, fmt.Errorf("there is already a pending pickup request for address %s", request.AddressID) +// } -// collector, err := s.repoColl.FindCollectorById(collectorId) +// modelRequest := model.RequestPickup{ +// UserId: UserId, +// AddressId: request.AddressID, +// EvidenceImage: request.EvidenceImage, +// RequestMethod: request.RequestMethod, +// } + +// err = s.repo.CreateRequestPickup(&modelRequest) +// if err != nil { +// return nil, fmt.Errorf("failed to create request pickup: %v", err) +// } + +// createdAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.CreatedAt) +// updatedAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.UpdatedAt) + +// response := &dto.ResponseRequestPickup{ +// ID: modelRequest.ID, +// UserId: UserId, +// AddressID: modelRequest.AddressId, +// EvidenceImage: modelRequest.EvidenceImage, +// StatusPickup: modelRequest.StatusPickup, +// CreatedAt: createdAt, +// UpdatedAt: updatedAt, +// } + +// for _, item := range request.RequestItems { + +// findTrashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryID) // if err != nil { -// return nil, fmt.Errorf("error fetching collector data: %v", err) +// return nil, fmt.Errorf("trash category with ID %s not found", item.TrashCategoryID) // } -// _, distance := utils.Distance( -// utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, -// utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, -// ) - -// if distance <= 20 { - -// mappedRequest := dto.ResponseRequestPickup{ -// ID: req.ID, -// UserId: req.UserId, -// AddressID: req.AddressId, -// EvidenceImage: req.EvidenceImage, -// StatusPickup: req.StatusPickup, -// CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), -// UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), -// } - -// user, err := s.repoUser.FindByID(req.UserId) -// if err != nil { -// return nil, fmt.Errorf("error fetching user data: %v", err) -// } -// mappedRequest.User = []dto.UserResponseDTO{ -// { -// Name: user.Name, -// Phone: user.Phone, -// }, -// } - -// address, err := s.repoAddress.FindAddressByID(req.AddressId) -// if err != nil { -// return nil, fmt.Errorf("error fetching address data: %v", err) -// } -// mappedRequest.Address = []dto.AddressResponseDTO{ -// { -// District: address.District, -// Village: address.Village, -// Detail: address.Detail, -// }, -// } - -// requestItems, err := s.repo.GetRequestPickupItems(req.ID) -// if err != nil { -// return nil, fmt.Errorf("error fetching request items: %v", err) -// } - -// var mappedRequestItems []dto.ResponseRequestPickupItem - -// for _, item := range requestItems { -// trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) -// if err != nil { -// return nil, fmt.Errorf("error fetching trash category: %v", err) -// } - -// mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ -// ID: item.ID, -// TrashCategory: []dto.ResponseTrashCategoryDTO{{ -// Name: trashCategory.Name, -// Icon: trashCategory.Icon, -// }}, -// EstimatedAmount: item.EstimatedAmount, -// }) -// } - -// mappedRequest.RequestItems = mappedRequestItems - -// response = append(response, mappedRequest) +// modelItem := model.RequestPickupItem{ +// RequestPickupId: modelRequest.ID, +// TrashCategoryId: findTrashCategory.ID, +// EstimatedAmount: item.EstimatedAmount, // } +// err = s.repo.CreateRequestPickupItem(&modelItem) +// if err != nil { +// return nil, fmt.Errorf("failed to create request pickup item: %v", err) +// } + +// response.RequestItems = append(response.RequestItems, dto.ResponseRequestPickupItem{ +// ID: modelItem.ID, +// TrashCategory: []dto.ResponseTrashCategoryDTO{{Name: findTrashCategory.Name, Icon: findTrashCategory.Icon}}, +// EstimatedAmount: modelItem.EstimatedAmount, +// }) // } // return response, nil // } -// func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { +// func (s *requestPickupService) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) { -// collector, err := s.repoColl.FindCollectorById(collectorId) +// request, err := s.repo.FindRequestPickupByID(id) // if err != nil { -// return nil, fmt.Errorf("error fetching collector data: %v", err) +// return nil, fmt.Errorf("error fetching request pickup with ID %s: %v", id, err) // } -// requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) + +// createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) +// updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) + +// response := &dto.ResponseRequestPickup{ +// ID: request.ID, +// UserId: request.UserId, +// AddressID: request.AddressId, +// EvidenceImage: request.EvidenceImage, +// StatusPickup: request.StatusPickup, +// CreatedAt: createdAt, +// UpdatedAt: updatedAt, +// } + +// return response, nil +// } + +// func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) { + +// requests, err := s.repo.FindAllRequestPickups(userid) // if err != nil { -// return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) +// return nil, fmt.Errorf("error fetching all request pickups: %v", err) // } // var response []dto.ResponseRequestPickup - -// for _, req := range requests { - -// createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) -// updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) - -// mappedRequest := dto.ResponseRequestPickup{ -// ID: req.ID, -// UserId: req.UserId, -// AddressID: req.AddressId, -// EvidenceImage: req.EvidenceImage, -// StatusPickup: req.StatusPickup, +// for _, request := range requests { +// createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) +// updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) +// response = append(response, dto.ResponseRequestPickup{ +// ID: request.ID, +// UserId: request.UserId, +// AddressID: request.AddressId, +// EvidenceImage: request.EvidenceImage, +// StatusPickup: request.StatusPickup, // CreatedAt: createdAt, // UpdatedAt: updatedAt, -// } - -// user, err := s.repoUser.FindByID(req.UserId) -// if err != nil { -// return nil, fmt.Errorf("error fetching user data: %v", err) -// } -// mappedRequest.User = []dto.UserResponseDTO{ -// { -// Name: user.Name, -// Phone: user.Phone, -// }, -// } - -// address, err := s.repoAddress.FindAddressByID(req.AddressId) -// if err != nil { -// return nil, fmt.Errorf("error fetching address data: %v", err) -// } -// mappedRequest.Address = []dto.AddressResponseDTO{ -// { -// District: address.District, -// Village: address.Village, -// Detail: address.Detail, -// }, -// } - -// requestItems, err := s.repo.GetRequestPickupItems(req.ID) -// if err != nil { -// return nil, fmt.Errorf("error fetching request items: %v", err) -// } - -// var mappedRequestItems []dto.ResponseRequestPickupItem - -// for _, item := range requestItems { - -// trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) -// if err != nil { -// return nil, fmt.Errorf("error fetching trash category: %v", err) -// } - -// mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ -// ID: item.ID, -// TrashCategory: []dto.ResponseTrashCategoryDTO{{ -// Name: trashCategory.Name, -// Icon: trashCategory.Icon, -// }}, -// EstimatedAmount: item.EstimatedAmount, -// }) -// } - -// mappedRequest.RequestItems = mappedRequestItems - -// response = append(response, mappedRequest) +// }) // } // return response, nil // } -// func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { +// // func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { +// // requests, err := s.repo.GetAutomaticRequestPickupsForCollector() +// // if err != nil { +// // return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) +// // } -// request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") -// if err != nil { -// return fmt.Errorf("request pickup not found: %v", err) -// } +// // var response []dto.ResponseRequestPickup -// if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { -// return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") -// } +// // for _, req := range requests { -// collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) -// if err != nil { -// return fmt.Errorf("collector tidak ditemukan: %v", err) -// } +// // collector, err := s.repoColl.FindCollectorById(collectorId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching collector data: %v", err) +// // } -// request.CollectorID = &collector.ID +// // _, distance := utils.Distance( +// // utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, +// // utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, +// // ) -// err = s.repo.UpdateRequestPickup(request.ID, request) -// if err != nil { -// return fmt.Errorf("failed to update request pickup: %v", err) -// } +// // if distance <= 20 { -// return nil -// } +// // mappedRequest := dto.ResponseRequestPickup{ +// // ID: req.ID, +// // UserId: req.UserId, +// // AddressID: req.AddressId, +// // EvidenceImage: req.EvidenceImage, +// // StatusPickup: req.StatusPickup, +// // CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), +// // UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), +// // } + +// // user, err := s.repoUser.FindByID(req.UserId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching user data: %v", err) +// // } +// // mappedRequest.User = []dto.UserResponseDTO{ +// // { +// // Name: user.Name, +// // Phone: user.Phone, +// // }, +// // } + +// // address, err := s.repoAddress.FindAddressByID(req.AddressId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching address data: %v", err) +// // } +// // mappedRequest.Address = []dto.AddressResponseDTO{ +// // { +// // District: address.District, +// // Village: address.Village, +// // Detail: address.Detail, +// // }, +// // } + +// // requestItems, err := s.repo.GetRequestPickupItems(req.ID) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching request items: %v", err) +// // } + +// // var mappedRequestItems []dto.ResponseRequestPickupItem + +// // for _, item := range requestItems { +// // trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching trash category: %v", err) +// // } + +// // mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ +// // ID: item.ID, +// // TrashCategory: []dto.ResponseTrashCategoryDTO{{ +// // Name: trashCategory.Name, +// // Icon: trashCategory.Icon, +// // }}, +// // EstimatedAmount: item.EstimatedAmount, +// // }) +// // } + +// // mappedRequest.RequestItems = mappedRequestItems + +// // response = append(response, mappedRequest) +// // } +// // } + +// // return response, nil +// // } + +// // func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { + +// // collector, err := s.repoColl.FindCollectorById(collectorId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching collector data: %v", err) +// // } +// // requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) +// // if err != nil { +// // return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) +// // } + +// // var response []dto.ResponseRequestPickup + +// // for _, req := range requests { + +// // createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) +// // updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) + +// // mappedRequest := dto.ResponseRequestPickup{ +// // ID: req.ID, +// // UserId: req.UserId, +// // AddressID: req.AddressId, +// // EvidenceImage: req.EvidenceImage, +// // StatusPickup: req.StatusPickup, +// // CreatedAt: createdAt, +// // UpdatedAt: updatedAt, +// // } + +// // user, err := s.repoUser.FindByID(req.UserId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching user data: %v", err) +// // } +// // mappedRequest.User = []dto.UserResponseDTO{ +// // { +// // Name: user.Name, +// // Phone: user.Phone, +// // }, +// // } + +// // address, err := s.repoAddress.FindAddressByID(req.AddressId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching address data: %v", err) +// // } +// // mappedRequest.Address = []dto.AddressResponseDTO{ +// // { +// // District: address.District, +// // Village: address.Village, +// // Detail: address.Detail, +// // }, +// // } + +// // requestItems, err := s.repo.GetRequestPickupItems(req.ID) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching request items: %v", err) +// // } + +// // var mappedRequestItems []dto.ResponseRequestPickupItem + +// // for _, item := range requestItems { + +// // trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) +// // if err != nil { +// // return nil, fmt.Errorf("error fetching trash category: %v", err) +// // } + +// // mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ +// // ID: item.ID, +// // TrashCategory: []dto.ResponseTrashCategoryDTO{{ +// // Name: trashCategory.Name, +// // Icon: trashCategory.Icon, +// // }}, +// // EstimatedAmount: item.EstimatedAmount, +// // }) +// // } + +// // mappedRequest.RequestItems = mappedRequestItems + +// // response = append(response, mappedRequest) +// // } + +// // return response, nil +// // } + +// // func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { + +// // request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") +// // if err != nil { +// // return fmt.Errorf("request pickup not found: %v", err) +// // } + +// // if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { +// // return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") +// // } + +// // collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) +// // if err != nil { +// // return fmt.Errorf("collector tidak ditemukan: %v", err) +// // } + +// // request.CollectorID = &collector.ID + +// // err = s.repo.UpdateRequestPickup(request.ID, request) +// // if err != nil { +// // return fmt.Errorf("failed to update request pickup: %v", err) +// // } + +// // return nil +// // } diff --git a/model/pickup_history_model.go b/model/pickup_history_model.go new file mode 100644 index 0000000..71c2504 --- /dev/null +++ b/model/pickup_history_model.go @@ -0,0 +1,12 @@ +package model + +import "time" + +type PickupStatusHistory struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + RequestID string `gorm:"not null" json:"request_id"` + Status string `gorm:"not null" json:"status"` + ChangedAt time.Time `gorm:"not null" json:"changed_at"` + ChangedByID string `gorm:"not null" json:"changed_by_id"` + ChangedByRole string `gorm:"not null" json:"changed_by_role"` +} diff --git a/model/rating_model.go b/model/rating_model.go new file mode 100644 index 0000000..8cceafb --- /dev/null +++ b/model/rating_model.go @@ -0,0 +1,13 @@ +package model + +import "time" + +type PickupRating struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + RequestID string `gorm:"not null;unique" json:"request_id"` + UserID string `gorm:"not null" json:"user_id"` + CollectorID string `gorm:"not null" json:"collector_id"` + Rating float32 `gorm:"not null" json:"rating"` + Feedback string `json:"feedback,omitempty"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` +} diff --git a/model/requestpickup_model.go b/model/requestpickup_model.go index 13402ca..0f7ecb8 100644 --- a/model/requestpickup_model.go +++ b/model/requestpickup_model.go @@ -7,28 +7,30 @@ import ( type RequestPickup struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` UserId string `gorm:"not null" json:"user_id"` - User User `gorm:"foreignKey:UserId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + User *User `gorm:"foreignKey:UserId" json:"user"` AddressId string `gorm:"not null" json:"address_id"` - Address Address `gorm:"foreignKey:AddressId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"address"` + Address *Address `gorm:"foreignKey:AddressId" json:"address"` RequestItems []RequestPickupItem `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;" json:"request_items"` - EvidenceImage string `json:"evidence_image"` Notes string `json:"notes"` StatusPickup string `gorm:"default:'waiting_collector'" json:"status_pickup"` CollectorID *string `gorm:"type:uuid" json:"collector_id,omitempty"` - Collector Collector `gorm:"foreignKey:CollectorID;constraint:OnDelete:CASCADE;" json:"collector"` + Collector *Collector `gorm:"foreignKey:CollectorID;references:ID" json:"collector,omitempty"` ConfirmedByCollectorAt *time.Time `json:"confirmed_by_collector_at,omitempty"` - RequestMethod string `gorm:"not null" json:"request_method"` - CreatedAt time.Time `gorm:"default:current_timestamp" json:"created_at"` - UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updated_at"` + RequestMethod string `gorm:"not null" json:"request_method"` // manual / otomatis + FinalPrice float64 `json:"final_price"` // diisi setelah collector update berat + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } type RequestPickupItem struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - RequestPickupId string `gorm:"not null" json:"request_pickup_id"` - RequestPickup RequestPickup `gorm:"foreignKey:RequestPickupId;constraint:OnDelete:CASCADE;"` - TrashCategoryId string `gorm:"not null" json:"trash_category_id"` - TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryId;constraint:OnDelete:CASCADE;" json:"trash_category"` - EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` + RequestPickupId string `gorm:"not null" json:"request_pickup_id"` + RequestPickup *RequestPickup `gorm:"foreignKey:RequestPickupId" json:"-"` + TrashCategoryId string `gorm:"not null" json:"trash_category_id"` + TrashCategory *TrashCategory `gorm:"foreignKey:TrashCategoryId" json:"trash_category"` + EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` + EstimatedPricePerKg float64 `gorm:"not null" json:"estimated_price_per_kg"` // harga pada saat itu + EstimatedSubtotalPrice float64 `gorm:"not null" json:"estimated_subtotal_price"` // amount * price per kg } // request_method { diff --git a/presentation/collector_route.go b/presentation/collector_route.go index 06f200a..0114967 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -30,8 +30,15 @@ func CollectorRouter(api fiber.Router) { // Middleware Auth dan Role // Inisialisasi repository dan service - collectorRepo := repositories.NewCollectorRepository() + pickupRepo := repositories.NewRequestPickupRepository() trashRepo := repositories.NewTrashRepository(config.DB) + historyRepo := repositories.NewPickupStatusHistoryRepository() + cartService := services.NewCartService() + + pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) + pickupHandler := handler.NewRequestPickupHandler(pickupService) + collectorRepo := repositories.NewCollectorRepository() + // trashRepo := repositories.NewTrashRepository(config.DB) collectorService := services.NewCollectorService(collectorRepo, trashRepo) collectorHandler := handler.NewCollectorHandler(collectorService) @@ -43,7 +50,9 @@ func CollectorRouter(api fiber.Router) { collectors.Post("/", collectorHandler.CreateCollector) // Create collector collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) // Add trash to collector collectors.Get("/:id", collectorHandler.GetCollectorByID) // Get collector by ID - collectors.Get("/", collectorHandler.GetCollectorByUserID) // Get collector by ID + collectors.Get("/", collectorHandler.GetCollectorByUserID) + collectors.Get("/pickup/assigned-to-me", pickupHandler.GetAssignedPickup) + // Get collector by ID collectors.Patch("/:id", collectorHandler.UpdateCollector) // Update collector fields collectors.Patch("/:id/trash", collectorHandler.UpdateTrash) collectors.Patch("/:id/job-status", collectorHandler.UpdateJobStatus) diff --git a/presentation/pickup_matching_route.go b/presentation/pickup_matching_route.go new file mode 100644 index 0000000..025252b --- /dev/null +++ b/presentation/pickup_matching_route.go @@ -0,0 +1,25 @@ +package presentation + +import ( + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func PickupMatchingRouter(api fiber.Router) { + pickupRepo := repositories.NewRequestPickupRepository() + collectorRepo := repositories.NewCollectorRepository() + service := services.NewPickupMatchingService(pickupRepo, collectorRepo) + handler := handler.NewPickupMatchingHandler(service) + + manual := api.Group("/pickup/manual") + manual.Use(middleware.AuthMiddleware) + manual.Get("/:pickupID/nearby-collectors", handler.GetNearbyCollectorsForPickup) + + auto := api.Group("/pickup/otomatis") + auto.Use(middleware.AuthMiddleware) + auto.Get("/available-requests", handler.GetAvailablePickupForCollector) +} diff --git a/presentation/rating_route.go b/presentation/rating_route.go new file mode 100644 index 0000000..a86d8ec --- /dev/null +++ b/presentation/rating_route.go @@ -0,0 +1,24 @@ +package presentation + +import ( + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func PickupRatingRouter(api fiber.Router) { + ratingRepo := repositories.NewPickupRatingRepository() + ratingService := services.NewPickupRatingService(ratingRepo) + ratingHandler := handler.NewPickupRatingHandler(ratingService) + + rating := api.Group("/pickup") + rating.Use(middleware.AuthMiddleware) + rating.Post("/:id/rating", ratingHandler.CreateRating) + + collector := api.Group("/collector") + collector.Get("/:id/ratings", ratingHandler.GetRatingsByCollector) + collector.Get("/:id/ratings/average", ratingHandler.GetAverageRating) +} diff --git a/presentation/request_pickup_route.go b/presentation/request_pickup_route.go new file mode 100644 index 0000000..cb968e6 --- /dev/null +++ b/presentation/request_pickup_route.go @@ -0,0 +1,33 @@ +package presentation + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func RequestPickupRouter(api fiber.Router) { + pickupRepo := repositories.NewRequestPickupRepository() + historyRepo := repositories.NewPickupStatusHistoryRepository() + trashRepo := repositories.NewTrashRepository(config.DB) + cartService := services.NewCartService() + historyService := services.NewPickupStatusHistoryService(historyRepo) + + pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) + pickupHandler := handler.NewRequestPickupHandler(pickupService) + statuspickupHandler := handler.NewPickupStatusHistoryHandler(historyService) + + reqpickup := api.Group("/reqpickup") + reqpickup.Use(middleware.AuthMiddleware) + + reqpickup.Post("/manual", pickupHandler.CreateRequestPickup) + reqpickup.Get("/pickup/:id/history", statuspickupHandler.GetStatusHistory) + reqpickup.Post("/otomatis", pickupHandler.CreateRequestPickup) + reqpickup.Put("/:id/select-collector", pickupHandler.SelectCollector) + reqpickup.Put("/pickup/:id/status", pickupHandler.UpdatePickupStatus) + reqpickup.Put("/pickup/:id/item/update-actual", pickupHandler.UpdatePickupItemActualAmount) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 4c7e163..4a6e4ea 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -26,7 +26,10 @@ func SetupRoutes(app *fiber.App) { // || auth router || // presentation.IdentityCardRouter(api) presentation.CompanyProfileRouter(api) - // presentation.RequestPickupRouter(api) + presentation.RequestPickupRouter(api) + presentation.PickupMatchingRouter(api) + presentation.PickupRatingRouter(api) + presentation.CollectorRouter(api) presentation.TrashCartRouter(api) From 29b04db0cbf879ec9d9256ff62a1ef5807eb3d1a Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 22 May 2025 12:40:27 +0700 Subject: [PATCH 39/48] refact: tidy up code --- dto/identitycard_dto.go | 10 - dto/request_pickup_dto.go | 13 +- dto/trashcart_dto.go | 2 - internal/handler/cart_handler.go | 6 - internal/handler/collector_handler.go | 60 +--- internal/handler/requestpickup_handler.go | 92 ------ internal/repositories/collector_repo.go | 2 - internal/repositories/requestpickup_repo.go | 181 ---------- internal/services/requestpickup_service.go | 349 -------------------- model/requestpickup_model.go | 21 +- presentation/collector_route.go | 36 +- presentation/requestpickup_route.go | 46 --- 12 files changed, 14 insertions(+), 804 deletions(-) delete mode 100644 internal/handler/requestpickup_handler.go delete mode 100644 internal/repositories/requestpickup_repo.go delete mode 100644 internal/services/requestpickup_service.go delete mode 100644 presentation/requestpickup_route.go diff --git a/dto/identitycard_dto.go b/dto/identitycard_dto.go index 14967f6..aa4274e 100644 --- a/dto/identitycard_dto.go +++ b/dto/identitycard_dto.go @@ -47,11 +47,6 @@ func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]strin errors := make(map[string][]string) isValid := true - // if strings.TrimSpace(r.UserID) == "" { - // errors["userId"] = append(errors["userId"], "UserID harus diisi") - // isValid = false - // } - if strings.TrimSpace(r.Identificationumber) == "" { errors["identificationumber"] = append(errors["identificationumber"], "Nomor identifikasi harus diisi") isValid = false @@ -117,10 +112,5 @@ func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]strin isValid = false } - // if strings.TrimSpace(r.Cardphoto) == "" { - // errors["cardphoto"] = append(errors["cardphoto"], "Foto KTP harus diisi") - // isValid = false - // } - return errors, isValid } diff --git a/dto/request_pickup_dto.go b/dto/request_pickup_dto.go index 480ed9e..ee63b38 100644 --- a/dto/request_pickup_dto.go +++ b/dto/request_pickup_dto.go @@ -4,17 +4,6 @@ import ( "strings" ) -// type NearbyCollectorDTO struct { -// CollectorID string `json:"collector_id"` -// Name string `json:"name"` -// Phone string `json:"phone"` -// Rating float32 `json:"rating"` -// Latitude float64 `json:"latitude"` -// Longitude float64 `json:"longitude"` -// DistanceKm float64 `json:"distance_km"` -// MatchedTrash []string `json:"matched_trash_ids"` -// } - type SelectCollectorDTO struct { CollectorID string `json:"collector_id"` } @@ -62,7 +51,7 @@ type PickupRequestForCollectorDTO struct { type RequestPickupDTO struct { AddressID string `json:"address_id"` - RequestMethod string `json:"request_method"` // "manual" atau "otomatis" + RequestMethod string `json:"request_method"` Notes string `json:"notes,omitempty"` } diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index fe124fa..2815a9f 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -31,8 +31,6 @@ type ResponseCartDTO struct { CartItems []ResponseCartItemDTO `json:"cart_items"` } -// ==== VALIDATION ==== - func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { errors := make(map[string][]string) diff --git a/internal/handler/cart_handler.go b/internal/handler/cart_handler.go index ab98fd7..ca1f3c8 100644 --- a/internal/handler/cart_handler.go +++ b/internal/handler/cart_handler.go @@ -25,7 +25,6 @@ func NewCartHandler(service services.CartService) CartHandler { return &cartHandler{service: service} } -// GET /cart func (h *cartHandler) GetCart(c *fiber.Ctx) error { userID := c.Locals("userID").(string) @@ -37,7 +36,6 @@ func (h *cartHandler) GetCart(c *fiber.Ctx) error { return utils.SuccessResponse(c, cart, "Data cart berhasil diambil") } -// POST /cart/item func (h *cartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { userID := c.Locals("userID").(string) @@ -60,7 +58,6 @@ func (h *cartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Item berhasil ditambahkan/diupdate di cart") } -// POST /cart/items func (h *cartHandler) AddMultipleCartItems(c *fiber.Ctx) error { userID := c.Locals("userID").(string) @@ -84,8 +81,6 @@ func (h *cartHandler) AddMultipleCartItems(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Semua item berhasil ditambahkan/diupdate ke cart") } - -// DELETE /cart/item/:trashID func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error { userID := c.Locals("userID").(string) trashID := c.Params("trashID") @@ -102,7 +97,6 @@ func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Item berhasil dihapus dari cart") } -// DELETE /cart func (h *cartHandler) ClearCart(c *fiber.Ctx) error { userID := c.Locals("userID").(string) diff --git a/internal/handler/collector_handler.go b/internal/handler/collector_handler.go index aad6c5d..25a73fb 100644 --- a/internal/handler/collector_handler.go +++ b/internal/handler/collector_handler.go @@ -27,58 +27,6 @@ func NewCollectorHandler(service services.CollectorService) CollectorHandler { return &collectorHandler{service: service} } -// func (h *CollectorHandler) ConfirmRequestPickup(c *fiber.Ctx) error { - -// collectorId, ok := c.Locals("userID").(string) -// if !ok || collectorId == "" { -// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") -// } - -// requestPickupId := c.Params("id") -// if requestPickupId == "" { -// return utils.ErrorResponse(c, "RequestPickup ID is required") -// } - -// req, err := h.service.ConfirmRequestPickup(requestPickupId, collectorId) -// if err != nil { -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, req, "Request pickup confirmed successfully") -// } - -// func (h *CollectorHandler) GetAvaibleCollector(c *fiber.Ctx) error { - -// userId := c.Locals("userID").(string) - -// requests, err := h.service.FindCollectorsNearby(userId) -// if err != nil { -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, requests, "menampilkan data collector terdekat") -// } - -// func (h *CollectorHandler) ConfirmRequestManualPickup(c *fiber.Ctx) error { -// userId := c.Locals("userID").(string) -// requestId := c.Params("request_id") -// if requestId == "" { -// fmt.Println("requestid dibutuhkan") -// } - -// var request dto.SelectCollectorRequest -// if err := c.BodyParser(&request); err != nil { -// return fmt.Errorf("error parsing request body: %v", err) -// } - -// message, err := h.service.ConfirmRequestManualPickup(requestId, userId) -// if err != nil { -// return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error confirming pickup: %v", err)) -// } - -// return utils.SuccessResponse(c, message, "berhasil konfirmasi request") -// } - func (h *collectorHandler) CreateCollector(c *fiber.Ctx) error { var req dto.RequestCollectorDTO if err := c.BodyParser(&req); err != nil { @@ -100,7 +48,6 @@ func (h *collectorHandler) CreateCollector(c *fiber.Ctx) error { return utils.CreateResponse(c, nil, "Collector berhasil dibuat") } -// POST /collectors/:id/trash func (h *collectorHandler) AddTrashToCollector(c *fiber.Ctx) error { collectorID := c.Params("id") var req dto.RequestAddAvaibleTrash @@ -123,7 +70,6 @@ func (h *collectorHandler) AddTrashToCollector(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Trash berhasil ditambahkan") } -// GET /collectors/:id func (h *collectorHandler) GetCollectorByID(c *fiber.Ctx) error { collectorID := c.Params("id") result, err := h.service.GetCollectorByID(context.Background(), collectorID) @@ -133,7 +79,7 @@ func (h *collectorHandler) GetCollectorByID(c *fiber.Ctx) error { return utils.SuccessResponse(c, result, "Data collector berhasil diambil") } func (h *collectorHandler) GetCollectorByUserID(c *fiber.Ctx) error { - // collectorID := c.Params("id") + userID, ok := c.Locals("userID").(string) if !ok || userID == "" { return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") @@ -146,7 +92,6 @@ func (h *collectorHandler) GetCollectorByUserID(c *fiber.Ctx) error { return utils.SuccessResponse(c, result, "Data collector berhasil diambil") } -// PATCH /collectors/:id func (h *collectorHandler) UpdateCollector(c *fiber.Ctx) error { collectorID := c.Params("id") var req struct { @@ -201,8 +146,6 @@ func (h *collectorHandler) UpdateJobStatus(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Status collector berhasil diperbarui") } - -// PATCH /collectors/:id/trash func (h *collectorHandler) UpdateTrash(c *fiber.Ctx) error { collectorID := c.Params("id") var req []dto.RequestAvaibleTrashbyCollector @@ -234,7 +177,6 @@ func (h *collectorHandler) UpdateTrash(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Trash berhasil diperbarui") } -// DELETE /collectors/trash/:id func (h *collectorHandler) DeleteTrash(c *fiber.Ctx) error { trashID := c.Params("id") if trashID == "" { diff --git a/internal/handler/requestpickup_handler.go b/internal/handler/requestpickup_handler.go deleted file mode 100644 index 4ae4d72..0000000 --- a/internal/handler/requestpickup_handler.go +++ /dev/null @@ -1,92 +0,0 @@ -package handler - -// import ( -// "fmt" -// "rijig/dto" -// "rijig/internal/services" -// "rijig/utils" - -// "github.com/gofiber/fiber/v2" -// ) - -// type RequestPickupHandler struct { -// service services.RequestPickupService -// } - -// func NewRequestPickupHandler(service services.RequestPickupService) *RequestPickupHandler { -// return &RequestPickupHandler{service: service} -// } - -// func (h *RequestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { -// userID, ok := c.Locals("userID").(string) -// if !ok || userID == "" { -// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") -// } - -// var request dto.RequestPickup - -// if err := c.BodyParser(&request); err != nil { -// return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") -// } - -// errors, valid := request.ValidateRequestPickup() -// if !valid { -// return utils.ValidationErrorResponse(c, errors) -// } - -// response, err := h.service.CreateRequestPickup(request, userID) -// if err != nil { -// return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating request pickup: %v", err)) -// } - -// return utils.SuccessResponse(c, response, "Request pickup created successfully") -// } - -// func (h *RequestPickupHandler) GetRequestPickupByID(c *fiber.Ctx) error { -// id := c.Params("id") - -// response, err := h.service.GetRequestPickupByID(id) -// if err != nil { -// return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Request pickup with ID %s not found: %v", id, err)) -// } - -// return utils.SuccessResponse(c, response, "Request pickup retrieved successfully") -// } - -// func (h *RequestPickupHandler) GetRequestPickups(c *fiber.Ctx) error { - -// collectorId := c.Locals("userID").(string) - -// requests, err := h.service.GetRequestPickupsForCollector(collectorId) -// if err != nil { -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, requests, "Automatic request pickups retrieved successfully") -// } - -// func (h *RequestPickupHandler) AssignCollectorToRequest(c *fiber.Ctx) error { -// userId, ok := c.Locals("userID").(string) -// if !ok || userId == "" { -// return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") -// } - -// var request dto.SelectCollectorRequest -// errors, valid := request.ValidateSelectCollectorRequest() -// if !valid { -// return utils.ValidationErrorResponse(c, errors) -// } - -// if err := c.BodyParser(&request); err != nil { -// return fmt.Errorf("error parsing request body: %v", err) -// } - -// err := h.service.SelectCollectorInRequest(userId, request.Collector_id) -// if err != nil { - -// return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Error assigning collector: %v", err)) -// } - -// return utils.GenericResponse(c, fiber.StatusOK, "berhasil memilih collector") -// } - diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go index aa41c71..7cfc218 100644 --- a/internal/repositories/collector_repo.go +++ b/internal/repositories/collector_repo.go @@ -104,8 +104,6 @@ func (r *collectorRepository) DeleteAvaibleTrash(ctx context.Context, trashID st Delete(&model.AvaibleTrashByCollector{}, "id = ?", trashID).Error } - -// func (r *collectorRepository) GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) { var collectors []model.Collector err := config.DB.WithContext(ctx). diff --git a/internal/repositories/requestpickup_repo.go b/internal/repositories/requestpickup_repo.go deleted file mode 100644 index 9c6a57a..0000000 --- a/internal/repositories/requestpickup_repo.go +++ /dev/null @@ -1,181 +0,0 @@ -package repositories - -// import ( -// "fmt" -// "rijig/model" - -// "gorm.io/gorm" -// ) - -// type RequestPickupRepository interface { -// CreateRequestPickup(request *model.RequestPickup) error -// CreateRequestPickupItem(item *model.RequestPickupItem) error -// FindRequestPickupByID(id string) (*model.RequestPickup, error) -// FindAllRequestPickups(userId string) ([]model.RequestPickup, error) -// FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) -// FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) -// FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) -// GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) -// GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) -// GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) -// // SelectCollectorInRequest(userId string, collectorId string) error -// UpdateRequestPickup(id string, request *model.RequestPickup) error -// DeleteRequestPickup(id string) error -// } - -// type requestPickupRepository struct { -// DB *gorm.DB -// } - -// func NewRequestPickupRepository(db *gorm.DB) RequestPickupRepository { -// return &requestPickupRepository{DB: db} -// } - -// func (r *requestPickupRepository) CreateRequestPickup(request *model.RequestPickup) error { -// if err := r.DB.Create(request).Error; err != nil { -// return fmt.Errorf("failed to create request pickup: %v", err) -// } - -// for _, item := range request.RequestItems { -// item.RequestPickupId = request.ID -// if err := r.DB.Create(&item).Error; err != nil { -// return fmt.Errorf("failed to create request pickup item: %v", err) -// } -// } - -// return nil -// } - -// func (r *requestPickupRepository) CreateRequestPickupItem(item *model.RequestPickupItem) error { -// if err := r.DB.Create(item).Error; err != nil { -// return fmt.Errorf("failed to create request pickup item: %v", err) -// } -// return nil -// } - -// func (r *requestPickupRepository) FindRequestPickupByID(id string) (*model.RequestPickup, error) { -// var request model.RequestPickup -// err := r.DB.Preload("RequestItems").First(&request, "id = ?", id).Error -// if err != nil { -// return nil, fmt.Errorf("request pickup with ID %s not found: %v", id, err) -// } -// return &request, nil -// } - -// func (r *requestPickupRepository) FindAllRequestPickups(userId string) ([]model.RequestPickup, error) { -// var requests []model.RequestPickup -// err := r.DB.Preload("RequestItems").Where("user_id = ?", userId).Find(&requests).Error -// if err != nil { -// return nil, fmt.Errorf("failed to fetch all request pickups: %v", err) -// } -// return requests, nil -// } - -// func (r *requestPickupRepository) FindAllAutomaticMethodRequest(requestMethod, statuspickup string) ([]model.RequestPickup, error) { -// var requests []model.RequestPickup -// err := r.DB.Preload("RequestItems").Where("request_method = ? AND status_pickup = ?", requestMethod, statuspickup).Find(&requests).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching request pickups with request_method %s: %v", requestMethod, err) -// } - -// return requests, nil -// } - -// func (r *requestPickupRepository) FindRequestPickupByAddressAndStatus(userId, status, method string) (*model.RequestPickup, error) { -// var request model.RequestPickup -// err := r.DB.Preload("Address").Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error -// if err != nil { -// if err == gorm.ErrRecordNotFound { -// return nil, nil -// } -// return nil, fmt.Errorf("failed to check existing request pickup: %v", err) -// } -// return &request, nil -// } - -// func (r *requestPickupRepository) FindRequestPickupByStatus(userId, status, method string) (*model.RequestPickup, error) { -// var request model.RequestPickup -// err := r.DB.Where("user_id = ? AND status_pickup = ? AND request_method =?", userId, status, method).First(&request).Error -// if err != nil { -// if err == gorm.ErrRecordNotFound { -// return nil, nil -// } -// return nil, fmt.Errorf("failed to check existing request pickup: %v", err) -// } -// return &request, nil -// } - -// func (r *requestPickupRepository) UpdateRequestPickup(id string, request *model.RequestPickup) error { -// err := r.DB.Model(&model.RequestPickup{}).Where("id = ?", id).Updates(request).Error -// if err != nil { -// return fmt.Errorf("failed to update request pickup: %v", err) -// } - -// return nil -// } - -// // func (r *requestPickupRepository) SelectCollectorInRequest(userId string, collectorId string) error { -// // var request model.RequestPickup -// // err := r.DB.Model(&model.RequestPickup{}). -// // Where("user_id = ? AND status_pickup = ? AND request_method = ? AND collector_id IS NULL", userId, "waiting_collector", "manual"). -// // First(&request).Error -// // if err != nil { -// // if err == gorm.ErrRecordNotFound { -// // return fmt.Errorf("no matching request pickup found for user %s", userId) -// // } -// // return fmt.Errorf("failed to find request pickup: %v", err) -// // } - -// // err = r.DB.Model(&model.RequestPickup{}). -// // Where("id = ?", request.ID). -// // Update("collector_id", collectorId). -// // Error -// // if err != nil { -// // return fmt.Errorf("failed to update collector_id: %v", err) -// // } -// // return nil -// // } - -// func (r *requestPickupRepository) DeleteRequestPickup(id string) error { - -// if err := r.DB.Where("request_pickup_id = ?", id).Delete(&model.RequestPickupItem{}).Error; err != nil { -// return fmt.Errorf("failed to delete request pickup items: %v", err) -// } - -// err := r.DB.Delete(&model.RequestPickup{}, "id = ?", id).Error -// if err != nil { -// return fmt.Errorf("failed to delete request pickup: %v", err) -// } -// return nil -// } - -// func (r *requestPickupRepository) GetAutomaticRequestPickupsForCollector() ([]model.RequestPickup, error) { -// var requests []model.RequestPickup -// err := r.DB.Preload("Address"). -// Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", nil). -// Find(&requests).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching pickup requests: %v", err) -// } -// return requests, nil -// } - -// func (r *requestPickupRepository) GetManualReqMethodforCollect(collector_id string) ([]model.RequestPickup, error) { -// var requests []model.RequestPickup -// err := r.DB.Where("request_method = ? AND status_pickup = ? AND collector_id = ?", "otomatis", "waiting_collector", collector_id). -// Find(&requests).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching pickup requests: %v", err) -// } -// return requests, nil -// } - -// func (r *requestPickupRepository) GetRequestPickupItems(requestPickupId string) ([]model.RequestPickupItem, error) { -// var items []model.RequestPickupItem - -// err := r.DB.Preload("TrashCategory").Where("request_pickup_id = ?", requestPickupId).Find(&items).Error -// if err != nil { -// return nil, fmt.Errorf("error fetching request pickup items: %v", err) -// } -// return items, nil -// } diff --git a/internal/services/requestpickup_service.go b/internal/services/requestpickup_service.go deleted file mode 100644 index 63917c0..0000000 --- a/internal/services/requestpickup_service.go +++ /dev/null @@ -1,349 +0,0 @@ -package services - -// import ( -// "fmt" -// "rijig/dto" -// "rijig/internal/repositories" -// "rijig/model" -// "rijig/utils" -// ) - -// type RequestPickupService interface { -// // CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) -// // GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) -// // GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) -// // GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) -// // SelectCollectorInRequest(userId, collectorId string) error -// } - -// type requestPickupService struct { -// repo repositories.RequestPickupRepository -// repoColl repositories.CollectorRepository -// repoAddress repositories.AddressRepository -// repoTrash repositories.TrashRepository -// repoUser repositories.UserProfilRepository -// } - -// func NewRequestPickupService(repo repositories.RequestPickupRepository, -// repoColl repositories.CollectorRepository, -// repoAddress repositories.AddressRepository, -// repoTrash repositories.TrashRepository, -// repoUser repositories.UserProfilRepository) RequestPickupService { -// return &requestPickupService{repo: repo, repoColl: repoColl, repoAddress: repoAddress, repoTrash: repoTrash, repoUser: repoUser} -// } - -// func (s *requestPickupService) CreateRequestPickup(request dto.RequestPickup, UserId string) (*dto.ResponseRequestPickup, error) { - -// errors, valid := request.ValidateRequestPickup() -// if !valid { -// return nil, fmt.Errorf("validation errors: %v", errors) -// } - -// _, err := s.repoAddress.FindAddressByID(request.AddressID) -// if err != nil { -// return nil, fmt.Errorf("address with ID %s not found", request.AddressID) -// } - -// existingRequest, err := s.repo.FindRequestPickupByAddressAndStatus(UserId, "waiting_collector", "otomatis") -// if err != nil { -// return nil, fmt.Errorf("error checking for existing request pickup: %v", err) -// } -// if existingRequest != nil { -// return nil, fmt.Errorf("there is already a pending pickup request for address %s", request.AddressID) -// } - -// modelRequest := model.RequestPickup{ -// UserId: UserId, -// AddressId: request.AddressID, -// EvidenceImage: request.EvidenceImage, -// RequestMethod: request.RequestMethod, -// } - -// err = s.repo.CreateRequestPickup(&modelRequest) -// if err != nil { -// return nil, fmt.Errorf("failed to create request pickup: %v", err) -// } - -// createdAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.CreatedAt) -// updatedAt, _ := utils.FormatDateToIndonesianFormat(modelRequest.UpdatedAt) - -// response := &dto.ResponseRequestPickup{ -// ID: modelRequest.ID, -// UserId: UserId, -// AddressID: modelRequest.AddressId, -// EvidenceImage: modelRequest.EvidenceImage, -// StatusPickup: modelRequest.StatusPickup, -// CreatedAt: createdAt, -// UpdatedAt: updatedAt, -// } - -// for _, item := range request.RequestItems { - -// findTrashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryID) -// if err != nil { -// return nil, fmt.Errorf("trash category with ID %s not found", item.TrashCategoryID) -// } - -// modelItem := model.RequestPickupItem{ -// RequestPickupId: modelRequest.ID, -// TrashCategoryId: findTrashCategory.ID, -// EstimatedAmount: item.EstimatedAmount, -// } -// err = s.repo.CreateRequestPickupItem(&modelItem) -// if err != nil { -// return nil, fmt.Errorf("failed to create request pickup item: %v", err) -// } - -// response.RequestItems = append(response.RequestItems, dto.ResponseRequestPickupItem{ -// ID: modelItem.ID, -// TrashCategory: []dto.ResponseTrashCategoryDTO{{Name: findTrashCategory.Name, Icon: findTrashCategory.Icon}}, -// EstimatedAmount: modelItem.EstimatedAmount, -// }) -// } - -// return response, nil -// } - -// func (s *requestPickupService) GetRequestPickupByID(id string) (*dto.ResponseRequestPickup, error) { - -// request, err := s.repo.FindRequestPickupByID(id) -// if err != nil { -// return nil, fmt.Errorf("error fetching request pickup with ID %s: %v", id, err) -// } - -// createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) -// updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) - -// response := &dto.ResponseRequestPickup{ -// ID: request.ID, -// UserId: request.UserId, -// AddressID: request.AddressId, -// EvidenceImage: request.EvidenceImage, -// StatusPickup: request.StatusPickup, -// CreatedAt: createdAt, -// UpdatedAt: updatedAt, -// } - -// return response, nil -// } - -// func (s *requestPickupService) GetAllRequestPickups(userid string) ([]dto.ResponseRequestPickup, error) { - -// requests, err := s.repo.FindAllRequestPickups(userid) -// if err != nil { -// return nil, fmt.Errorf("error fetching all request pickups: %v", err) -// } - -// var response []dto.ResponseRequestPickup -// for _, request := range requests { -// createdAt, _ := utils.FormatDateToIndonesianFormat(request.CreatedAt) -// updatedAt, _ := utils.FormatDateToIndonesianFormat(request.UpdatedAt) -// response = append(response, dto.ResponseRequestPickup{ -// ID: request.ID, -// UserId: request.UserId, -// AddressID: request.AddressId, -// EvidenceImage: request.EvidenceImage, -// StatusPickup: request.StatusPickup, -// CreatedAt: createdAt, -// UpdatedAt: updatedAt, -// }) -// } - -// return response, nil -// } - -// // func (s *requestPickupService) GetRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { -// // requests, err := s.repo.GetAutomaticRequestPickupsForCollector() -// // if err != nil { -// // return nil, fmt.Errorf("error retrieving automatic pickup requests: %v", err) -// // } - -// // var response []dto.ResponseRequestPickup - -// // for _, req := range requests { - -// // collector, err := s.repoColl.FindCollectorById(collectorId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching collector data: %v", err) -// // } - -// // _, distance := utils.Distance( -// // utils.Coord{Lat: collector.Address.Latitude, Lon: collector.Address.Longitude}, -// // utils.Coord{Lat: req.Address.Latitude, Lon: req.Address.Longitude}, -// // ) - -// // if distance <= 20 { - -// // mappedRequest := dto.ResponseRequestPickup{ -// // ID: req.ID, -// // UserId: req.UserId, -// // AddressID: req.AddressId, -// // EvidenceImage: req.EvidenceImage, -// // StatusPickup: req.StatusPickup, -// // CreatedAt: req.CreatedAt.Format("2006-01-02 15:04:05"), -// // UpdatedAt: req.UpdatedAt.Format("2006-01-02 15:04:05"), -// // } - -// // user, err := s.repoUser.FindByID(req.UserId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching user data: %v", err) -// // } -// // mappedRequest.User = []dto.UserResponseDTO{ -// // { -// // Name: user.Name, -// // Phone: user.Phone, -// // }, -// // } - -// // address, err := s.repoAddress.FindAddressByID(req.AddressId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching address data: %v", err) -// // } -// // mappedRequest.Address = []dto.AddressResponseDTO{ -// // { -// // District: address.District, -// // Village: address.Village, -// // Detail: address.Detail, -// // }, -// // } - -// // requestItems, err := s.repo.GetRequestPickupItems(req.ID) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching request items: %v", err) -// // } - -// // var mappedRequestItems []dto.ResponseRequestPickupItem - -// // for _, item := range requestItems { -// // trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching trash category: %v", err) -// // } - -// // mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ -// // ID: item.ID, -// // TrashCategory: []dto.ResponseTrashCategoryDTO{{ -// // Name: trashCategory.Name, -// // Icon: trashCategory.Icon, -// // }}, -// // EstimatedAmount: item.EstimatedAmount, -// // }) -// // } - -// // mappedRequest.RequestItems = mappedRequestItems - -// // response = append(response, mappedRequest) -// // } -// // } - -// // return response, nil -// // } - -// // func (s *requestPickupService) GetManualRequestPickupsForCollector(collectorId string) ([]dto.ResponseRequestPickup, error) { - -// // collector, err := s.repoColl.FindCollectorById(collectorId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching collector data: %v", err) -// // } -// // requests, err := s.repo.GetManualReqMethodforCollect(collector.ID) -// // if err != nil { -// // return nil, fmt.Errorf("error retrieving manual pickup requests: %v", err) -// // } - -// // var response []dto.ResponseRequestPickup - -// // for _, req := range requests { - -// // createdAt, _ := utils.FormatDateToIndonesianFormat(req.CreatedAt) -// // updatedAt, _ := utils.FormatDateToIndonesianFormat(req.UpdatedAt) - -// // mappedRequest := dto.ResponseRequestPickup{ -// // ID: req.ID, -// // UserId: req.UserId, -// // AddressID: req.AddressId, -// // EvidenceImage: req.EvidenceImage, -// // StatusPickup: req.StatusPickup, -// // CreatedAt: createdAt, -// // UpdatedAt: updatedAt, -// // } - -// // user, err := s.repoUser.FindByID(req.UserId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching user data: %v", err) -// // } -// // mappedRequest.User = []dto.UserResponseDTO{ -// // { -// // Name: user.Name, -// // Phone: user.Phone, -// // }, -// // } - -// // address, err := s.repoAddress.FindAddressByID(req.AddressId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching address data: %v", err) -// // } -// // mappedRequest.Address = []dto.AddressResponseDTO{ -// // { -// // District: address.District, -// // Village: address.Village, -// // Detail: address.Detail, -// // }, -// // } - -// // requestItems, err := s.repo.GetRequestPickupItems(req.ID) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching request items: %v", err) -// // } - -// // var mappedRequestItems []dto.ResponseRequestPickupItem - -// // for _, item := range requestItems { - -// // trashCategory, err := s.repoTrash.GetCategoryByID(item.TrashCategoryId) -// // if err != nil { -// // return nil, fmt.Errorf("error fetching trash category: %v", err) -// // } - -// // mappedRequestItems = append(mappedRequestItems, dto.ResponseRequestPickupItem{ -// // ID: item.ID, -// // TrashCategory: []dto.ResponseTrashCategoryDTO{{ -// // Name: trashCategory.Name, -// // Icon: trashCategory.Icon, -// // }}, -// // EstimatedAmount: item.EstimatedAmount, -// // }) -// // } - -// // mappedRequest.RequestItems = mappedRequestItems - -// // response = append(response, mappedRequest) -// // } - -// // return response, nil -// // } - -// // func (s *requestPickupService) SelectCollectorInRequest(userId, collectorId string) error { - -// // request, err := s.repo.FindRequestPickupByStatus(userId, "waiting_collector", "manual") -// // if err != nil { -// // return fmt.Errorf("request pickup not found: %v", err) -// // } - -// // if request.StatusPickup != "waiting_collector" && request.RequestMethod != "manual" { -// // return fmt.Errorf("pickup request is not in 'waiting_collector' status and not 'manual' method") -// // } - -// // collector, err := s.repoColl.FindCollectorByIdWithoutAddr(collectorId) -// // if err != nil { -// // return fmt.Errorf("collector tidak ditemukan: %v", err) -// // } - -// // request.CollectorID = &collector.ID - -// // err = s.repo.UpdateRequestPickup(request.ID, request) -// // if err != nil { -// // return fmt.Errorf("failed to update request pickup: %v", err) -// // } - -// // return nil -// // } diff --git a/model/requestpickup_model.go b/model/requestpickup_model.go index 0f7ecb8..9128768 100644 --- a/model/requestpickup_model.go +++ b/model/requestpickup_model.go @@ -16,8 +16,8 @@ type RequestPickup struct { CollectorID *string `gorm:"type:uuid" json:"collector_id,omitempty"` Collector *Collector `gorm:"foreignKey:CollectorID;references:ID" json:"collector,omitempty"` ConfirmedByCollectorAt *time.Time `json:"confirmed_by_collector_at,omitempty"` - RequestMethod string `gorm:"not null" json:"request_method"` // manual / otomatis - FinalPrice float64 `json:"final_price"` // diisi setelah collector update berat + RequestMethod string `gorm:"not null" json:"request_method"` + FinalPrice float64 `json:"final_price"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } @@ -29,19 +29,6 @@ type RequestPickupItem struct { TrashCategoryId string `gorm:"not null" json:"trash_category_id"` TrashCategory *TrashCategory `gorm:"foreignKey:TrashCategoryId" json:"trash_category"` EstimatedAmount float64 `gorm:"not null" json:"estimated_amount"` - EstimatedPricePerKg float64 `gorm:"not null" json:"estimated_price_per_kg"` // harga pada saat itu - EstimatedSubtotalPrice float64 `gorm:"not null" json:"estimated_subtotal_price"` // amount * price per kg + EstimatedPricePerKg float64 `gorm:"not null" json:"estimated_price_per_kg"` + EstimatedSubtotalPrice float64 `gorm:"not null" json:"estimated_subtotal_price"` } - -// request_method { -// "otomatis", -// "manual" -// } - -// status_pickup { -// "waiting_collector", -// "confirmed", -// "collector_picking", -// "completed" -// "canceled" -// } diff --git a/presentation/collector_route.go b/presentation/collector_route.go index 0114967..7d59807 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -7,29 +7,11 @@ import ( "rijig/internal/services" "rijig/middleware" - // "rijig/utils" - "github.com/gofiber/fiber/v2" ) func CollectorRouter(api fiber.Router) { - // repo := repositories.NewCollectorRepository(config.DB) - // repoReq := repositories.NewRequestPickupRepository(config.DB) - // repoAddress := repositories.NewAddressRepository(config.DB) - // repoUser := repositories.NewUserProfilRepository(config.DB) - // colectorService := services.NewCollectorService(repo, repoReq, repoAddress, repoUser) - // collectorHandler := handler.NewCollectorHandler(colectorService) - // collector := api.Group("/collector") - // collector.Use(middleware.AuthMiddleware) - - // collector.Put("confirmrequest/:id", collectorHandler.ConfirmRequestPickup) - // collector.Put("confirm-manual/request/:request_id", collectorHandler.ConfirmRequestManualPickup) - // collector.Get("/avaible", collectorHandler.GetAvaibleCollector) - - // Middleware Auth dan Role - - // Inisialisasi repository dan service pickupRepo := repositories.NewRequestPickupRepository() trashRepo := repositories.NewTrashRepository(config.DB) historyRepo := repositories.NewPickupStatusHistoryRepository() @@ -38,22 +20,20 @@ func CollectorRouter(api fiber.Router) { pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) pickupHandler := handler.NewRequestPickupHandler(pickupService) collectorRepo := repositories.NewCollectorRepository() - // trashRepo := repositories.NewTrashRepository(config.DB) + collectorService := services.NewCollectorService(collectorRepo, trashRepo) collectorHandler := handler.NewCollectorHandler(collectorService) - // Group collector collectors := api.Group("/collectors") collectors.Use(middleware.AuthMiddleware) - // === Collector routes === - collectors.Post("/", collectorHandler.CreateCollector) // Create collector - collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) // Add trash to collector - collectors.Get("/:id", collectorHandler.GetCollectorByID) // Get collector by ID - collectors.Get("/", collectorHandler.GetCollectorByUserID) - collectors.Get("/pickup/assigned-to-me", pickupHandler.GetAssignedPickup) - // Get collector by ID - collectors.Patch("/:id", collectorHandler.UpdateCollector) // Update collector fields + collectors.Post("/", collectorHandler.CreateCollector) + collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) + collectors.Get("/:id", collectorHandler.GetCollectorByID) + collectors.Get("/", collectorHandler.GetCollectorByUserID) + collectors.Get("/pickup/assigned-to-me", pickupHandler.GetAssignedPickup) + + collectors.Patch("/:id", collectorHandler.UpdateCollector) collectors.Patch("/:id/trash", collectorHandler.UpdateTrash) collectors.Patch("/:id/job-status", collectorHandler.UpdateJobStatus) collectors.Delete("/trash/:id", collectorHandler.DeleteTrash) diff --git a/presentation/requestpickup_route.go b/presentation/requestpickup_route.go deleted file mode 100644 index 029826a..0000000 --- a/presentation/requestpickup_route.go +++ /dev/null @@ -1,46 +0,0 @@ -package presentation - -// import ( -// "rijig/config" -// "rijig/internal/handler" -// "rijig/internal/repositories" -// "rijig/internal/services" -// "rijig/middleware" - -// "github.com/gofiber/fiber/v2" -// ) - -// func RequestPickupRouter(api fiber.Router) { -// // repo repositories.RequestPickupRepository -// // repoColl repositories.CollectorRepository -// // repoAddress repositories.AddressRepository -// // repoTrash repositories.TrashRepository -// // repoUser repositories.UserProfilRepository - -// requestRepo := repositories.NewRequestPickupRepository(config.DB) -// // repoColl := repositories.NewCollectorRepository(config.DB) -// repoColl := repositories.NewCollectorRepository() -// repoAddress := repositories.NewAddressRepository(config.DB) -// Trashrepo := repositories.NewTrashRepository(config.DB) -// repouser := repositories.NewUserProfilRepository(config.DB) -// // collectorRepo := repositories.NewCollectorRepository(config.DB) - -// requestPickupServices := services.NewRequestPickupService(requestRepo, repoColl, repoAddress, Trashrepo, repouser) -// // collectorService := services.NewCollectorService(collectorRepo, requestRepo, repoAddress) -// // service services.RequestPickupService, -// // collectorService services.CollectorService - -// requestPickupHandler := handler.NewRequestPickupHandler(requestPickupServices) - -// requestPickupAPI := api.Group("/requestpickup") -// requestPickupAPI.Use(middleware.AuthMiddleware) - -// requestPickupAPI.Post("/", requestPickupHandler.CreateRequestPickup) -// // requestPickupAPI.Get("/get", middleware.AuthMiddleware, requestPickupHandler.GetAutomaticRequestByUser) -// requestPickupAPI.Get("/get-allrequest", requestPickupHandler.GetRequestPickups) -// requestPickupAPI.Patch("/select-collector", requestPickupHandler.AssignCollectorToRequest) -// // requestPickupAPI.Get("/:id", requestPickupHandler.GetRequestPickupByID) -// // requestPickupAPI.Get("/", requestPickupHandler.GetAllRequestPickups) -// // requestPickupAPI.Put("/:id", requestPickupHandler.UpdateRequestPickup) -// // requestPickupAPI.Delete("/:id", requestPickupHandler.DeleteRequestPickup) -// } From 58f843cac29e1f2414f467230a5896414cd1f95b Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Thu, 29 May 2025 16:44:47 +0700 Subject: [PATCH 40/48] fix: auto commit to DB --- cmd/main.go | 63 ++--- dto/trashcart_dto.go | 30 +- go.mod | 1 - internal/handler/cart_handler.go | 119 ++++---- internal/repositories/trashcart_repo.go | 164 +++++++++-- .../services/auth/auth_masyarakat_service.go | 2 +- .../services/auth/auth_pengelola_service.go | 2 +- .../services/auth/auth_pengepul_service.go | 2 +- internal/services/cart_redis.go | 91 ++---- internal/services/cart_service.go | 259 +++++++++++++++++- internal/services/request_pickup_service.go | 2 - internal/worker/cart_worker.go | 143 ++++++---- model/trashcart_model.go | 8 +- presentation/cart_router.go | 16 +- presentation/collector_route.go | 4 +- presentation/request_pickup_route.go | 4 +- utils/redis_caching.go | 3 +- 17 files changed, 627 insertions(+), 286 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index e99d0be..46df279 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,58 +2,49 @@ package main import ( "log" - "os" "rijig/config" + "rijig/internal/repositories" + "rijig/internal/services" "rijig/internal/worker" "rijig/router" + "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/robfig/cron" ) func main() { config.SetupConfig() - logFile, _ := os.OpenFile("logs/cart_commit.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - log.SetOutput(logFile) + cartRepo := repositories.NewCartRepository() + trashRepo := repositories.NewTrashRepository(config.DB) + cartService := services.NewCartService(cartRepo, trashRepo) + worker := worker.NewCartWorker(cartService, cartRepo, trashRepo) go func() { - c := cron.New() - c.AddFunc("@every 1m", func() { - _ = worker.CommitExpiredCartsToDB() - }) - c.Start() + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for range ticker.C { + if err := worker.AutoCommitExpiringCarts(); err != nil { + log.Printf("Auto-commit error: %v", err) + } + } }() - app := fiber.New() - app.Use(cors.New(cors.Config{ - AllowOrigins: "*", - AllowMethods: "GET,POST,PUT,PATCH,DELETE", - AllowHeaders: "Content-Type,x-api-key", - })) + app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + return c.Status(code).JSON(fiber.Map{ + "error": err.Error(), + }) + }, + }) - // app.Use(cors.New(cors.Config{ - // AllowOrigins: "http://localhost:3000", - // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", - // AllowHeaders: "Origin, Content-Type, Accept, Authorization, x-api-key", - // AllowCredentials: true, - // })) + app.Use(cors.New()) - // app.Use(func(c *fiber.Ctx) error { - // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - // c.Set("Access-Control-Allow-Credentials", "true") - // return c.Next() - // }) - - // app.Options("*", func(c *fiber.Ctx) error { - // c.Set("Access-Control-Allow-Origin", "http://localhost:3000") - // c.Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") - // c.Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, x-api-key") - // c.Set("Access-Control-Allow-Credentials", "true") - // return c.SendStatus(fiber.StatusNoContent) - // }) router.SetupRoutes(app) config.StartServer(app) diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go index 2815a9f..d195a00 100644 --- a/dto/trashcart_dto.go +++ b/dto/trashcart_dto.go @@ -7,44 +7,38 @@ import ( type RequestCartItemDTO struct { TrashID string `json:"trash_id"` - Amount float32 `json:"amount"` + Amount float64 `json:"amount"` } type RequestCartDTO struct { CartItems []RequestCartItemDTO `json:"cart_items"` } +type ResponseCartDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TotalAmount float64 `json:"total_amount"` + EstimatedTotalPrice float64 `json:"estimated_total_price"` + CartItems []ResponseCartItemDTO `json:"cart_items"` +} + type ResponseCartItemDTO struct { ID string `json:"id"` TrashID string `json:"trash_id"` TrashName string `json:"trash_name"` TrashIcon string `json:"trash_icon"` - Amount float32 `json:"amount"` - SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` -} - -type ResponseCartDTO struct { - ID string `json:"id"` - UserID string `json:"user_id"` - TotalAmount float32 `json:"total_amount"` - EstimatedTotalPrice float32 `json:"estimated_total_price"` - CartItems []ResponseCartItemDTO `json:"cart_items"` + TrashPrice float64 `json:"trash_price"` + Amount float64 `json:"amount"` + SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"` } func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { errors := make(map[string][]string) - if len(r.CartItems) == 0 { - errors["cart_items"] = append(errors["cart_items"], "minimal satu item harus dimasukkan") - } - for i, item := range r.CartItems { if strings.TrimSpace(item.TrashID) == "" { errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong") } - if item.Amount <= 0 { - errors[fmt.Sprintf("cart_items[%d].amount", i)] = append(errors[fmt.Sprintf("cart_items[%d].amount", i)], "amount harus lebih dari 0") - } } if len(errors) > 0 { diff --git a/go.mod b/go.mod index 93ed566..687dde0 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect - github.com/robfig/cron v1.2.0 github.com/rs/zerolog v1.33.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect diff --git a/internal/handler/cart_handler.go b/internal/handler/cart_handler.go index ca1f3c8..4dd7c25 100644 --- a/internal/handler/cart_handler.go +++ b/internal/handler/cart_handler.go @@ -1,7 +1,6 @@ package handler import ( - "context" "rijig/dto" "rijig/internal/services" "rijig/utils" @@ -9,100 +8,86 @@ import ( "github.com/gofiber/fiber/v2" ) -type CartHandler interface { - GetCart(c *fiber.Ctx) error - AddOrUpdateCartItem(c *fiber.Ctx) error - AddMultipleCartItems(c *fiber.Ctx) error - DeleteCartItem(c *fiber.Ctx) error - ClearCart(c *fiber.Ctx) error +type CartHandler struct { + cartService services.CartService } -type cartHandler struct { - service services.CartService +func NewCartHandler(cartService services.CartService) *CartHandler { + return &CartHandler{cartService: cartService} } -func NewCartHandler(service services.CartService) CartHandler { - return &cartHandler{service: service} -} - -func (h *cartHandler) GetCart(c *fiber.Ctx) error { +func (h *CartHandler) AddOrUpdateItem(c *fiber.Ctx) error { userID := c.Locals("userID").(string) + var req dto.RequestCartItemDTO - cart, err := h.service.GetCart(context.Background(), userID) - if err != nil { - return utils.ErrorResponse(c, "Cart belum dibuat atau sudah kadaluarsa") - } - - return utils.SuccessResponse(c, cart, "Data cart berhasil diambil") -} - -func (h *cartHandler) AddOrUpdateCartItem(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - var item dto.RequestCartItemDTO - if err := c.BodyParser(&item); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"format tidak valid"}}) - } - - if item.TrashID == "" || item.Amount <= 0 { + if err := c.BodyParser(&req); err != nil { return utils.ValidationErrorResponse(c, map[string][]string{ - "trash_id": {"harus diisi"}, - "amount": {"harus lebih dari 0"}, + "request": {"Payload tidak valid"}, }) } - if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Item berhasil ditambahkan/diupdate di cart") -} - -func (h *cartHandler) AddMultipleCartItems(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - var payload dto.RequestCartDTO - if err := c.BodyParser(&payload); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format tidak valid"}, - }) - } - - if errs, ok := payload.ValidateRequestCartDTO(); !ok { + hasErrors, _ := req.Amount > 0 && req.TrashID != "", true + if !hasErrors { + errs := make(map[string][]string) + if req.Amount <= 0 { + errs["amount"] = append(errs["amount"], "Amount harus lebih dari 0") + } + if req.TrashID == "" { + errs["trash_id"] = append(errs["trash_id"], "Trash ID tidak boleh kosong") + } return utils.ValidationErrorResponse(c, errs) } - for _, item := range payload.CartItems { - if err := h.service.AddOrUpdateItem(context.Background(), userID, item); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } + if err := h.cartService.AddOrUpdateItem(c.Context(), userID, req); err != nil { + return utils.InternalServerErrorResponse(c, "Gagal menambahkan item ke keranjang") } - return utils.SuccessResponse(c, nil, "Semua item berhasil ditambahkan/diupdate ke cart") + return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil ditambahkan ke keranjang") } -func (h *cartHandler) DeleteCartItem(c *fiber.Ctx) error { +func (h *CartHandler) GetCart(c *fiber.Ctx) error { userID := c.Locals("userID").(string) - trashID := c.Params("trashID") + + cart, err := h.cartService.GetCart(c.Context(), userID) + if err != nil { + return utils.ErrorResponse(c, "Gagal mengambil data keranjang") + } + + return utils.SuccessResponse(c, cart, "Berhasil mengambil data keranjang") +} + +func (h *CartHandler) DeleteItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + trashID := c.Params("trash_id") if trashID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{"trash_id": {"tidak boleh kosong"}}) + return utils.GenericResponse(c, fiber.StatusBadRequest, "Trash ID tidak boleh kosong") } - err := h.service.DeleteItem(context.Background(), userID, trashID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) + if err := h.cartService.DeleteItem(c.Context(), userID, trashID); err != nil { + return utils.InternalServerErrorResponse(c, "Gagal menghapus item dari keranjang") } - return utils.SuccessResponse(c, nil, "Item berhasil dihapus dari cart") + return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil dihapus dari keranjang") } -func (h *cartHandler) ClearCart(c *fiber.Ctx) error { +func (h *CartHandler) Checkout(c *fiber.Ctx) error { userID := c.Locals("userID").(string) - if err := h.service.ClearCart(context.Background(), userID); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) + if err := h.cartService.Checkout(c.Context(), userID); err != nil { + return utils.InternalServerErrorResponse(c, "Gagal melakukan checkout keranjang") } - return utils.SuccessResponse(c, nil, "Seluruh cart berhasil dihapus") + return utils.GenericResponse(c, fiber.StatusOK, "Checkout berhasil. Permintaan pickup telah dibuat.") } + +func (h *CartHandler) ClearCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + err := h.cartService.ClearCart(c.Context(), userID) + if err != nil { + return utils.InternalServerErrorResponse(c, "Gagal menghapus keranjang") + } + + return utils.GenericResponse(c, fiber.StatusOK, "Keranjang berhasil dikosongkan") +} \ No newline at end of file diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go index 59e37ff..81c7e20 100644 --- a/internal/repositories/trashcart_repo.go +++ b/internal/repositories/trashcart_repo.go @@ -1,15 +1,27 @@ package repositories import ( + "context" + "errors" + "fmt" + "rijig/config" "rijig/model" + + "gorm.io/gorm" ) type CartRepository interface { - CreateCart(cart *model.Cart) error - GetTrashCategoryByID(id string) (*model.TrashCategory, error) - GetCartByUserID(userID string) (*model.Cart, error) - DeleteCartByUserID(userID string) error + FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) + AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error + DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error + GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) + UpdateCartTotals(ctx context.Context, cartID string) error + DeleteCart(ctx context.Context, userID string) error + // New method for batch cart creation + CreateCartWithItems(ctx context.Context, cart *model.Cart) error + // Check if user already has a cart + HasExistingCart(ctx context.Context, userID string) (bool, error) } type cartRepository struct{} @@ -18,29 +30,137 @@ func NewCartRepository() CartRepository { return &cartRepository{} } -func (r *cartRepository) CreateCart(cart *model.Cart) error { - return config.DB.Create(cart).Error -} - -func (r *cartRepository) DeleteCartByUserID(userID string) error { - return config.DB.Where("user_id = ?", userID).Delete(&model.Cart{}).Error -} - -func (r *cartRepository) GetTrashCategoryByID(id string) (*model.TrashCategory, error) { - var trash model.TrashCategory - if err := config.DB.First(&trash, "id = ?", id).Error; err != nil { - return nil, err - } - return &trash, nil -} - -func (r *cartRepository) GetCartByUserID(userID string) (*model.Cart, error) { +func (r *cartRepository) FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) { var cart model.Cart - err := config.DB.Preload("CartItems.TrashCategory"). + db := config.DB.WithContext(ctx) + + err := db. + Preload("CartItems.TrashCategory"). Where("user_id = ?", userID). First(&cart).Error + + if err == nil { + return &cart, nil + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + newCart := model.Cart{ + UserID: userID, + TotalAmount: 0, + EstimatedTotalPrice: 0, + } + if err := db.Create(&newCart).Error; err != nil { + return nil, err + } + return &newCart, nil + } + + return nil, err +} + +func (r *cartRepository) AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error { + db := config.DB.WithContext(ctx) + + var item model.CartItem + err := db. + Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). + First(&item).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + newItem := model.CartItem{ + CartID: cartID, + TrashCategoryID: trashCategoryID, + Amount: amount, + SubTotalEstimatedPrice: amount * estimatedPrice, + } + return db.Create(&newItem).Error + } + + if err != nil { + return err + } + + item.Amount = amount + item.SubTotalEstimatedPrice = amount * estimatedPrice + return db.Save(&item).Error +} + +func (r *cartRepository) DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error { + db := config.DB.WithContext(ctx) + return db.Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). + Delete(&model.CartItem{}).Error +} + +func (r *cartRepository) GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) { + var cart model.Cart + db := config.DB.WithContext(ctx) + + err := db. + Preload("CartItems.TrashCategory"). + Where("user_id = ?", userID). + First(&cart).Error + if err != nil { return nil, err } return &cart, nil } + +func (r *cartRepository) UpdateCartTotals(ctx context.Context, cartID string) error { + db := config.DB.WithContext(ctx) + + var items []model.CartItem + if err := db.Where("cart_id = ?", cartID).Find(&items).Error; err != nil { + return err + } + + var totalAmount float64 + var totalPrice float64 + + for _, item := range items { + totalAmount += item.Amount + totalPrice += item.SubTotalEstimatedPrice + } + + return db.Model(&model.Cart{}). + Where("id = ?", cartID). + Updates(map[string]interface{}{ + "total_amount": totalAmount, + "estimated_total_price": totalPrice, + }).Error +} + +func (r *cartRepository) DeleteCart(ctx context.Context, userID string) error { + db := config.DB.WithContext(ctx) + var cart model.Cart + if err := db.Where("user_id = ?", userID).First(&cart).Error; err != nil { + return err + } + return db.Delete(&cart).Error +} + +// New method for batch cart creation with transaction +func (r *cartRepository) CreateCartWithItems(ctx context.Context, cart *model.Cart) error { + db := config.DB.WithContext(ctx) + + // Use transaction to ensure data consistency + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(cart).Error; err != nil { + return fmt.Errorf("failed to create cart: %w", err) + } + return nil + }) +} + +// Check if user already has a cart +func (r *cartRepository) HasExistingCart(ctx context.Context, userID string) (bool, error) { + db := config.DB.WithContext(ctx) + + var count int64 + err := db.Model(&model.Cart{}).Where("user_id = ?", userID).Count(&count).Error + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/internal/services/auth/auth_masyarakat_service.go b/internal/services/auth/auth_masyarakat_service.go index 407ba9b..b20e930 100644 --- a/internal/services/auth/auth_masyarakat_service.go +++ b/internal/services/auth/auth_masyarakat_service.go @@ -31,7 +31,7 @@ func NewAuthMasyarakatService(userRepo repositories.UserRepository, roleRepo rep func (s *authMasyarakatService) generateJWTToken(userID string, deviceID string) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) + expirationTime := time.Now().Add(672 * time.Hour) claims := jwt.MapClaims{ "sub": userID, diff --git a/internal/services/auth/auth_pengelola_service.go b/internal/services/auth/auth_pengelola_service.go index 840f22c..2cc1ad9 100644 --- a/internal/services/auth/auth_pengelola_service.go +++ b/internal/services/auth/auth_pengelola_service.go @@ -31,7 +31,7 @@ func NewAuthPengelolaService(userRepo repositories.UserRepository, roleRepo repo func (s *authPengelolaService) generateJWTToken(userID string, deviceID string) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) + expirationTime := time.Now().Add(168 * time.Hour) claims := jwt.MapClaims{ "sub": userID, diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go index 3ce5246..c4e6b73 100644 --- a/internal/services/auth/auth_pengepul_service.go +++ b/internal/services/auth/auth_pengepul_service.go @@ -31,7 +31,7 @@ func NewAuthPengepulService(userRepo repositories.UserRepository, roleRepo repos func (s *authPengepulService) generateJWTToken(userID string, deviceID string) (string, error) { - expirationTime := time.Now().Add(24 * time.Hour) + expirationTime := time.Now().Add(480 * time.Hour) claims := jwt.MapClaims{ "sub": userID, diff --git a/internal/services/cart_redis.go b/internal/services/cart_redis.go index aff0ce9..80b0aae 100644 --- a/internal/services/cart_redis.go +++ b/internal/services/cart_redis.go @@ -8,97 +8,66 @@ import ( "rijig/config" "rijig/dto" + + "github.com/go-redis/redis/v8" ) -var cartTTL = 30 * time.Minute +const CartTTL = 30 * time.Minute +const CartKeyPrefix = "cart:" -func getCartKey(userID string) string { - return fmt.Sprintf("cart:user:%s", userID) +func buildCartKey(userID string) string { + return fmt.Sprintf("%s%s", CartKeyPrefix, userID) } func SetCartToRedis(ctx context.Context, userID string, cart dto.RequestCartDTO) error { - key := getCartKey(userID) - data, err := json.Marshal(cart) if err != nil { - return fmt.Errorf("failed to marshal cart: %w", err) + return err } - err = config.RedisClient.Set(ctx, key, data, cartTTL).Err() - if err != nil { - return fmt.Errorf("failed to save cart to redis: %w", err) - } + return config.RedisClient.Set(ctx, buildCartKey(userID), data, CartTTL).Err() +} - return nil +func RefreshCartTTL(ctx context.Context, userID string) error { + return config.RedisClient.Expire(ctx, buildCartKey(userID), CartTTL).Err() } func GetCartFromRedis(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { - key := getCartKey(userID) - val, err := config.RedisClient.Get(ctx, key).Result() - if err != nil { + val, err := config.RedisClient.Get(ctx, buildCartKey(userID)).Result() + if err == redis.Nil { + return nil, nil + } else if err != nil { return nil, err } var cart dto.RequestCartDTO if err := json.Unmarshal([]byte(val), &cart); err != nil { - return nil, fmt.Errorf("failed to unmarshal cart data: %w", err) + return nil, err } - return &cart, nil } func DeleteCartFromRedis(ctx context.Context, userID string) error { - key := getCartKey(userID) - return config.RedisClient.Del(ctx, key).Err() + return config.RedisClient.Del(ctx, buildCartKey(userID)).Err() } -func GetCartTTL(ctx context.Context, userID string) (time.Duration, error) { - key := getCartKey(userID) - return config.RedisClient.TTL(ctx, key).Result() -} - -func UpdateOrAddCartItemToRedis(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { - cart, err := GetCartFromRedis(ctx, userID) +func GetExpiringCartKeys(ctx context.Context, threshold time.Duration) ([]string, error) { + keys, err := config.RedisClient.Keys(ctx, CartKeyPrefix+"*").Result() if err != nil { - - cart = &dto.RequestCartDTO{ - CartItems: []dto.RequestCartItemDTO{item}, - } - return SetCartToRedis(ctx, userID, *cart) + return nil, err } - updated := false - for i, ci := range cart.CartItems { - if ci.TrashID == item.TrashID { - cart.CartItems[i].Amount = item.Amount - updated = true - break + var expiringKeys []string + for _, key := range keys { + ttl, err := config.RedisClient.TTL(ctx, key).Result() + if err != nil { + continue + } + + if ttl > 0 && ttl <= threshold { + expiringKeys = append(expiringKeys, key) } } - if !updated { - cart.CartItems = append(cart.CartItems, item) - } - return SetCartToRedis(ctx, userID, *cart) -} - -func RemoveCartItemFromRedis(ctx context.Context, userID, trashID string) error { - cart, err := GetCartFromRedis(ctx, userID) - if err != nil { - return err - } - - updatedItems := make([]dto.RequestCartItemDTO, 0) - for _, ci := range cart.CartItems { - if ci.TrashID != trashID { - updatedItems = append(updatedItems, ci) - } - } - - if len(updatedItems) == 0 { - return DeleteCartFromRedis(ctx, userID) - } - - cart.CartItems = updatedItems - return SetCartToRedis(ctx, userID, *cart) + return expiringKeys, nil } diff --git a/internal/services/cart_service.go b/internal/services/cart_service.go index d98cd8d..3a905dc 100644 --- a/internal/services/cart_service.go +++ b/internal/services/cart_service.go @@ -1,35 +1,266 @@ package services import ( - "rijig/dto" "context" + "errors" + "log" + + "rijig/dto" + "rijig/internal/repositories" + "rijig/model" ) type CartService interface { - GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) - AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error + AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error + GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error) DeleteItem(ctx context.Context, userID string, trashID string) error ClearCart(ctx context.Context, userID string) error + Checkout(ctx context.Context, userID string) error } -type cartService struct{} - -func NewCartService() CartService { - return &cartService{} +type cartService struct { + repo repositories.CartRepository + trashRepo repositories.TrashRepository } -func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { - return GetCartFromRedis(ctx, userID) +func NewCartService(repo repositories.CartRepository, trashRepo repositories.TrashRepository) CartService { + return &cartService{repo: repo, trashRepo: trashRepo} } -func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, item dto.RequestCartItemDTO) error { - return UpdateOrAddCartItemToRedis(ctx, userID, item) +func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error { + if req.Amount <= 0 { + return errors.New("amount harus lebih dari 0") + } + + _, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID) + if err != nil { + return err + } + + existingCart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + + if existingCart == nil { + existingCart = &dto.RequestCartDTO{ + CartItems: []dto.RequestCartItemDTO{}, + } + } + + updated := false + for i, item := range existingCart.CartItems { + if item.TrashID == req.TrashID { + existingCart.CartItems[i].Amount = req.Amount + updated = true + break + } + } + + if !updated { + existingCart.CartItems = append(existingCart.CartItems, dto.RequestCartItemDTO{ + TrashID: req.TrashID, + Amount: req.Amount, + }) + } + + return SetCartToRedis(ctx, userID, *existingCart) +} + +func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error) { + + cached, err := GetCartFromRedis(ctx, userID) + if err != nil { + return nil, err + } + + if cached != nil { + + if err := RefreshCartTTL(ctx, userID); err != nil { + log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err) + } + + return s.buildResponseFromCache(ctx, userID, cached) + } + + cart, err := s.repo.GetCartByUser(ctx, userID) + if err != nil { + + return &dto.ResponseCartDTO{ + ID: "", + UserID: userID, + TotalAmount: 0, + EstimatedTotalPrice: 0, + CartItems: []dto.ResponseCartItemDTO{}, + }, nil + + } + + response := s.buildResponseFromDB(cart) + + cacheData := dto.RequestCartDTO{CartItems: []dto.RequestCartItemDTO{}} + for _, item := range cart.CartItems { + cacheData.CartItems = append(cacheData.CartItems, dto.RequestCartItemDTO{ + TrashID: item.TrashCategoryID, + Amount: item.Amount, + }) + } + + if err := SetCartToRedis(ctx, userID, cacheData); err != nil { + log.Printf("Warning: Failed to cache cart for user %s: %v", userID, err) + } + + return response, nil } func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error { - return RemoveCartItemFromRedis(ctx, userID, trashID) + + existingCart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + if existingCart == nil { + return errors.New("keranjang tidak ditemukan") + } + + filtered := []dto.RequestCartItemDTO{} + for _, item := range existingCart.CartItems { + if item.TrashID != trashID { + filtered = append(filtered, item) + } + } + existingCart.CartItems = filtered + + return SetCartToRedis(ctx, userID, *existingCart) } func (s *cartService) ClearCart(ctx context.Context, userID string) error { - return DeleteCartFromRedis(ctx, userID) -} \ No newline at end of file + + if err := DeleteCartFromRedis(ctx, userID); err != nil { + return err + } + + return s.repo.DeleteCart(ctx, userID) +} + +func (s *cartService) Checkout(ctx context.Context, userID string) error { + + cachedCart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + + if cachedCart != nil { + if err := s.commitCartFromRedis(ctx, userID, cachedCart); err != nil { + return err + } + } + + _, err = s.repo.GetCartByUser(ctx, userID) + if err != nil { + return err + } + + DeleteCartFromRedis(ctx, userID) + return s.repo.DeleteCart(ctx, userID) +} + +func (s *cartService) buildResponseFromCache(ctx context.Context, userID string, cached *dto.RequestCartDTO) (*dto.ResponseCartDTO, error) { + totalQty := 0.0 + totalPrice := 0.0 + items := []dto.ResponseCartItemDTO{} + + for _, item := range cached.CartItems { + trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + log.Printf("Warning: Trash category %s not found for cached cart item", item.TrashID) + continue + } + + subtotal := item.Amount * trash.EstimatedPrice + totalQty += item.Amount + totalPrice += subtotal + + items = append(items, dto.ResponseCartItemDTO{ + ID: "", + TrashID: item.TrashID, + TrashName: trash.Name, + TrashIcon: trash.Icon, + TrashPrice: trash.EstimatedPrice, + Amount: item.Amount, + SubTotalEstimatedPrice: subtotal, + }) + } + + return &dto.ResponseCartDTO{ + ID: "-", + UserID: userID, + TotalAmount: totalQty, + EstimatedTotalPrice: totalPrice, + CartItems: items, + }, nil +} + +func (s *cartService) buildResponseFromDB(cart *model.Cart) *dto.ResponseCartDTO { + var items []dto.ResponseCartItemDTO + for _, item := range cart.CartItems { + items = append(items, dto.ResponseCartItemDTO{ + ID: item.ID, + TrashID: item.TrashCategoryID, + TrashName: item.TrashCategory.Name, + TrashIcon: item.TrashCategory.Icon, + TrashPrice: item.TrashCategory.EstimatedPrice, + Amount: item.Amount, + SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, + }) + } + + return &dto.ResponseCartDTO{ + ID: cart.ID, + UserID: cart.UserID, + TotalAmount: cart.TotalAmount, + EstimatedTotalPrice: cart.EstimatedTotalPrice, + CartItems: items, + } +} + +func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *dto.RequestCartDTO) error { + if len(cachedCart.CartItems) == 0 { + return nil + } + + totalAmount := 0.0 + totalPrice := 0.0 + var cartItems []model.CartItem + + for _, item := range cachedCart.CartItems { + trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + log.Printf("Warning: Skipping invalid trash category %s during commit", item.TrashID) + continue + } + + subtotal := item.Amount * trash.EstimatedPrice + totalAmount += item.Amount + totalPrice += subtotal + + cartItems = append(cartItems, model.CartItem{ + TrashCategoryID: item.TrashID, + Amount: item.Amount, + SubTotalEstimatedPrice: subtotal, + }) + } + + if len(cartItems) == 0 { + return nil + } + + newCart := &model.Cart{ + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: cartItems, + } + + return s.repo.CreateCartWithItems(ctx, newCart) +} diff --git a/internal/services/request_pickup_service.go b/internal/services/request_pickup_service.go index a5eb89f..363d6a4 100644 --- a/internal/services/request_pickup_service.go +++ b/internal/services/request_pickup_service.go @@ -67,8 +67,6 @@ func (s *requestPickupService) ConvertCartToRequestPickup(ctx context.Context, u Notes: req.Notes, StatusPickup: "waiting_collector", RequestItems: requestItems, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), } if err := s.pickupRepo.CreateRequestPickup(ctx, &pickup); err != nil { diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go index 07449fb..5c292da 100644 --- a/internal/worker/cart_worker.go +++ b/internal/worker/cart_worker.go @@ -3,109 +3,156 @@ package worker import ( "context" "encoding/json" - "fmt" + "log" "strings" "time" "rijig/config" "rijig/dto" + "rijig/internal/repositories" + "rijig/internal/services" "rijig/model" ) -func CommitExpiredCartsToDB() error { +type CartWorker struct { + cartService services.CartService + cartRepo repositories.CartRepository + trashRepo repositories.TrashRepository +} + +func NewCartWorker(cartService services.CartService, cartRepo repositories.CartRepository, trashRepo repositories.TrashRepository) *CartWorker { + return &CartWorker{ + cartService: cartService, + cartRepo: cartRepo, + trashRepo: trashRepo, + } +} + +func (w *CartWorker) AutoCommitExpiringCarts() error { ctx := context.Background() + threshold := 1 * time.Minute - keys, err := config.RedisClient.Keys(ctx, "cart:user:*").Result() + keys, err := services.GetExpiringCartKeys(ctx, threshold) if err != nil { - return fmt.Errorf("error fetching cart keys: %w", err) + return err } + if len(keys) == 0 { + return nil + } + + log.Printf("[CART-WORKER] Found %d carts expiring within 1 minute", len(keys)) + + successCount := 0 for _, key := range keys { - ttl, err := config.RedisClient.TTL(ctx, key).Result() - if err != nil || ttl > 30*time.Second { + userID := w.extractUserIDFromKey(key) + if userID == "" { + log.Printf("[CART-WORKER] Invalid key format: %s", key) continue } - val, err := config.RedisClient.Get(ctx, key).Result() + hasCart, err := w.cartRepo.HasExistingCart(ctx, userID) if err != nil { + log.Printf("[CART-WORKER] Error checking existing cart for user %s: %v", userID, err) continue } - var cart dto.RequestCartDTO - if err := json.Unmarshal([]byte(val), &cart); err != nil { + if hasCart { + + if err := services.DeleteCartFromRedis(ctx, userID); err != nil { + log.Printf("[CART-WORKER] Failed to delete Redis cache for user %s: %v", userID, err) + } else { + log.Printf("[CART-WORKER] Deleted Redis cache for user %s (already has DB cart)", userID) + } continue } - userID := extractUserIDFromKey(key) + cartData, err := w.getCartFromRedis(ctx, key) + if err != nil { + log.Printf("[CART-WORKER] Failed to get cart data for key %s: %v", key, err) + continue + } - cartID := SaveCartToDB(ctx, userID, &cart) + if err := w.commitCartToDB(ctx, userID, cartData); err != nil { + log.Printf("[CART-WORKER] Failed to commit cart for user %s: %v", userID, err) + continue + } - _ = config.RedisClient.Del(ctx, key).Err() - - fmt.Printf( - "[AUTO-COMMIT] UserID: %s | CartID: %s | TotalItem: %d | EstimatedTotalPrice: %.2f | Committed at: %s\n", - userID, cartID, len(cart.CartItems), calculateTotalEstimated(&cart), time.Now().Format(time.RFC3339), - ) + if err := services.DeleteCartFromRedis(ctx, userID); err != nil { + log.Printf("[CART-WORKER] Warning: Failed to delete Redis key after commit for user %s: %v", userID, err) + } + successCount++ + log.Printf("[CART-WORKER] Successfully auto-committed cart for user %s", userID) } + log.Printf("[CART-WORKER] Auto-commit completed: %d successful commits", successCount) return nil } -func extractUserIDFromKey(key string) string { - +func (w *CartWorker) extractUserIDFromKey(key string) string { parts := strings.Split(key, ":") - if len(parts) == 3 { - return parts[2] + if len(parts) >= 2 { + return parts[len(parts)-1] } return "" } -func SaveCartToDB(ctx context.Context, userID string, cart *dto.RequestCartDTO) string { - totalAmount := float32(0) - totalPrice := float32(0) +func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.RequestCartDTO, error) { + val, err := config.RedisClient.Get(ctx, key).Result() + if err != nil { + return nil, err + } + var cart dto.RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + return nil, err + } + + return &cart, nil +} + +func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *dto.RequestCartDTO) error { + if len(cartData.CartItems) == 0 { + return nil + } + + totalAmount := 0.0 + totalPrice := 0.0 var cartItems []model.CartItem - for _, item := range cart.CartItems { - var trash model.TrashCategory - if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { + for _, item := range cartData.CartItems { + if item.Amount <= 0 { continue } - subtotal := trash.EstimatedPrice * float64(item.Amount) + trash, err := w.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + log.Printf("[CART-WORKER] Warning: Skipping invalid trash category %s", item.TrashID) + continue + } + + subtotal := item.Amount * trash.EstimatedPrice totalAmount += item.Amount - totalPrice += float32(subtotal) + totalPrice += subtotal cartItems = append(cartItems, model.CartItem{ TrashCategoryID: item.TrashID, Amount: item.Amount, - SubTotalEstimatedPrice: float32(subtotal), + SubTotalEstimatedPrice: subtotal, }) } - newCart := model.Cart{ + if len(cartItems) == 0 { + return nil + } + + newCart := &model.Cart{ UserID: userID, TotalAmount: totalAmount, EstimatedTotalPrice: totalPrice, CartItems: cartItems, } - if err := config.DB.WithContext(ctx).Create(&newCart).Error; err != nil { - fmt.Printf("Error committing cart: %v\n", err) - } - - return newCart.ID -} - -func calculateTotalEstimated(cart *dto.RequestCartDTO) float32 { - var total float32 - for _, item := range cart.CartItems { - var trash model.TrashCategory - if err := config.DB.First(&trash, "id = ?", item.TrashID).Error; err != nil { - continue - } - total += item.Amount * float32(trash.EstimatedPrice) - } - return total + return w.cartRepo.CreateCartWithItems(ctx, newCart) } diff --git a/model/trashcart_model.go b/model/trashcart_model.go index 49d530a..0b4d05f 100644 --- a/model/trashcart_model.go +++ b/model/trashcart_model.go @@ -9,8 +9,8 @@ type Cart struct { UserID string `gorm:"not null" json:"user_id"` User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;" json:"-"` CartItems []CartItem `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"cart_items"` - TotalAmount float32 `json:"total_amount"` - EstimatedTotalPrice float32 `json:"estimated_total_price"` + TotalAmount float64 `json:"total_amount"` + EstimatedTotalPrice float64 `json:"estimated_total_price"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } @@ -21,8 +21,8 @@ type CartItem struct { Cart *Cart `gorm:"foreignKey:CartID;constraint:OnDelete:CASCADE;" json:"-"` TrashCategoryID string `gorm:"not null" json:"trash_id"` TrashCategory TrashCategory `gorm:"foreignKey:TrashCategoryID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"trash_category"` - Amount float32 `json:"amount"` - SubTotalEstimatedPrice float32 `json:"subtotal_estimated_price"` + Amount float64 `json:"amount"` + SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"` CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` } diff --git a/presentation/cart_router.go b/presentation/cart_router.go index 30c6725..2066cca 100644 --- a/presentation/cart_router.go +++ b/presentation/cart_router.go @@ -1,7 +1,9 @@ package presentation import ( + "rijig/config" "rijig/internal/handler" + "rijig/internal/repositories" "rijig/internal/services" "rijig/middleware" @@ -9,16 +11,18 @@ import ( ) func TrashCartRouter(api fiber.Router) { - cartService := services.NewCartService() + repo := repositories.NewCartRepository() + trashRepo := repositories.NewTrashRepository(config.DB) + cartService := services.NewCartService(repo, trashRepo) cartHandler := handler.NewCartHandler(cartService) cart := api.Group("/cart") cart.Use(middleware.AuthMiddleware) cart.Get("/", cartHandler.GetCart) - cart.Post("/item", cartHandler.AddOrUpdateCartItem) - cart.Post("/items", cartHandler.AddMultipleCartItems) - cart.Delete("/item/:trashID", cartHandler.DeleteCartItem) - cart.Delete("/", cartHandler.ClearCart) - + cart.Post("/item", cartHandler.AddOrUpdateItem) + cart.Delete("/item/:trash_id", cartHandler.DeleteItem) + cart.Delete("/clear", cartHandler.ClearCart) } + +// cart.Post("/items", cartHandler.AddMultipleCartItems) diff --git a/presentation/collector_route.go b/presentation/collector_route.go index 7d59807..d31f741 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -11,11 +11,13 @@ import ( ) func CollectorRouter(api fiber.Router) { + cartRepo := repositories.NewCartRepository() + // trashRepo repositories.TrashRepository pickupRepo := repositories.NewRequestPickupRepository() trashRepo := repositories.NewTrashRepository(config.DB) historyRepo := repositories.NewPickupStatusHistoryRepository() - cartService := services.NewCartService() + cartService := services.NewCartService(cartRepo, trashRepo) pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) pickupHandler := handler.NewRequestPickupHandler(pickupService) diff --git a/presentation/request_pickup_route.go b/presentation/request_pickup_route.go index cb968e6..1facfff 100644 --- a/presentation/request_pickup_route.go +++ b/presentation/request_pickup_route.go @@ -11,10 +11,12 @@ import ( ) func RequestPickupRouter(api fiber.Router) { + cartRepo := repositories.NewCartRepository() pickupRepo := repositories.NewRequestPickupRepository() historyRepo := repositories.NewPickupStatusHistoryRepository() trashRepo := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService() + + cartService := services.NewCartService(cartRepo, trashRepo) historyService := services.NewPickupStatusHistoryService(historyRepo) pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) diff --git a/utils/redis_caching.go b/utils/redis_caching.go index 99083c7..7b343f3 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -5,9 +5,8 @@ import ( "encoding/json" "fmt" "log" - "time" - "rijig/config" + "time" "github.com/go-redis/redis/v8" ) From e7c0675f8a0b386c53fc126b6db1ecc947305fda Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sat, 7 Jun 2025 18:22:14 +0700 Subject: [PATCH 41/48] refact: modularization folder struct and fixing auth --- .env.example | 5 + dto/company_profile_dto.go | 9 +- dto/trash_dto.go | 3 +- internal/about/about_dto.go | 66 ++ internal/about/about_handler.go | 177 +++++ internal/about/about_repository.go | 113 +++ internal/about/about_route.go | 1 + internal/about/about_service.go | 497 ++++++++++++ internal/address/address_dto.go | 73 ++ internal/address/address_handler.go | 1 + internal/address/address_repository.go | 62 ++ internal/address/address_route.go | 1 + internal/address/address_service.go | 250 ++++++ internal/article/article_dto.go | 48 ++ internal/article/article_handler.go | 141 ++++ internal/article/article_repository.go | 148 ++++ internal/article/article_route.go | 26 + internal/article/article_service.go | 337 ++++++++ internal/authentication/authentication_dto.go | 362 +++++++++ .../authentication/authentication_handler.go | 225 ++++++ .../authentication_repository.go | 86 ++ .../authentication/authentication_route.go | 40 + .../authentication/authentication_service.go | 382 +++++++++ internal/cart/cart_dto.go | 1 + internal/cart/cart_handler.go | 1 + internal/cart/cart_repository.go | 1 + internal/cart/cart_route.go | 1 + internal/cart/cart_service.go | 1 + internal/collector/collector_dto.go | 1 + internal/collector/collector_handler.go | 1 + internal/collector/collector_repository.go | 1 + internal/collector/collector_route.go | 1 + internal/collector/collector_service.go | 1 + internal/company/company_dto.go | 62 ++ internal/company/company_handler.go | 111 +++ internal/company/company_repository.go | 89 +++ internal/company/company_route.go | 23 + internal/company/company_service.go | 136 ++++ internal/handler/about_handler.go | 5 +- internal/handler/auth/auth_admin_handler.go | 3 +- .../handler/auth/auth_masyarakat_handler.go | 3 +- .../handler/auth/auth_pengepul_handler.go | 3 +- .../handler/auth/auth_pnegelola_handler.go | 3 +- internal/handler/role_handler.go | 4 +- internal/identitycart/identitycart_dto.go | 148 ++++ internal/identitycart/identitycart_handler.go | 73 ++ internal/identitycart/identitycart_repo.go | 64 ++ internal/identitycart/identitycart_route.go | 35 + internal/identitycart/identitycart_service.go | 321 ++++++++ internal/repositories/role_repo.go | 37 +- internal/repositories/trash_repo.go | 1 - internal/requestpickup/requestpickup_dto.go | 1 + .../requestpickup/requestpickup_handler.go | 1 + .../requestpickup/requestpickup_repository.go | 1 + internal/requestpickup/requestpickup_route.go | 1 + .../requestpickup/requestpickup_service.go | 1 + internal/role/role_dto.go | 8 + internal/role/role_handler.go | 52 ++ internal/role/role_repo.go | 49 ++ internal/role/role_route.go | 18 + internal/role/role_service.go | 89 +++ internal/services/auth/auth_admin_service.go | 3 +- .../services/auth/auth_masyarakat_service.go | 3 +- .../services/auth/auth_pengelola_service.go | 3 +- .../services/auth/auth_pengepul_service.go | 3 +- internal/services/role_service.go | 14 +- internal/trash/trash_dto.go | 75 ++ internal/trash/trash_handler.go | 521 ++++++++++++ internal/trash/trash_repository.go | 326 ++++++++ internal/trash/trash_route.go | 1 + internal/trash/trash_service.go | 750 ++++++++++++++++++ internal/userpin/userpin_dto.go | 48 ++ internal/userpin/userpin_handler.go | 77 ++ internal/userpin/userpin_repo.go | 50 ++ internal/userpin/userpin_route.go | 23 + internal/userpin/userpin_service.go | 97 +++ internal/userprofile/userprofile_dto.go | 67 ++ internal/userprofile/userprofile_handler.go | 1 + internal/userprofile/userprofile_repo.go | 35 + internal/userprofile/userprofile_route.go | 1 + internal/userprofile/userprofile_service.go | 1 + internal/whatsapp/whatsapp_handler.go | 24 + internal/whatsapp/whatsapp_route.go | 11 + internal/wilayahindo/wilayahindo_dto.go | 27 + internal/wilayahindo/wilayahindo_handler.go | 1 + .../wilayahindo/wilayahindo_repository.go | 310 ++++++++ internal/wilayahindo/wilayahindo_route.go | 1 + internal/wilayahindo/wilayahindo_service.go | 455 +++++++++++ middleware/additional_middleware.go | 199 +++++ middleware/api_key.go | 4 +- middleware/auth_middleware.go | 65 -- middleware/middleware.go | 564 +++++++++++++ middleware/role_middleware.go | 29 - model/company_profile_model.go | 2 +- model/identitycard_model.go | 4 + model/trash_model.go | 20 +- model/user_model.go | 31 +- model/userpin_model.go | 1 + presentation/about_route.go | 14 +- presentation/address_route.go | 10 +- presentation/article_route.go | 6 +- presentation/auth/auth_admin_route.go | 3 +- presentation/auth/auth_masyarakat_route.go | 3 +- presentation/auth/auth_pengelola_route.go | 3 +- presentation/auth/auth_pengepul_route.go | 3 +- presentation/banner_route.go | 26 - presentation/cart_router.go | 2 +- presentation/collector_route.go | 2 +- presentation/company_profile_route.go | 2 +- presentation/identitycard_route.go | 3 +- presentation/initialcoint_route.go | 28 - presentation/pickup_matching_route.go | 4 +- presentation/product_route.go | 36 - presentation/rating_route.go | 2 +- presentation/request_pickup_route.go | 2 +- presentation/role_route.go | 28 +- presentation/store_route.go | 26 - presentation/trash_route.go | 14 +- presentation/user_route.go | 10 +- presentation/userpin_route.go | 18 +- presentation/whatsapp_route.go | 13 - presentation/wilayahindonesia_route.go | 2 +- router/setup_routes.go.go | 39 +- utils/api_response.go | 102 +++ utils/identity_number_validator.go | 95 +++ utils/redis_utility.go | 231 ++++++ utils/regexp_formatter.go | 63 -- utils/response.go | 190 ++--- utils/role_permission.go | 11 +- utils/todo_validation.go | 95 +++ utils/token_management.go | 654 +++++++++++++++ 131 files changed, 9461 insertions(+), 531 deletions(-) create mode 100644 internal/about/about_dto.go create mode 100644 internal/about/about_handler.go create mode 100644 internal/about/about_repository.go create mode 100644 internal/about/about_route.go create mode 100644 internal/about/about_service.go create mode 100644 internal/address/address_dto.go create mode 100644 internal/address/address_handler.go create mode 100644 internal/address/address_repository.go create mode 100644 internal/address/address_route.go create mode 100644 internal/address/address_service.go create mode 100644 internal/article/article_dto.go create mode 100644 internal/article/article_handler.go create mode 100644 internal/article/article_repository.go create mode 100644 internal/article/article_route.go create mode 100644 internal/article/article_service.go create mode 100644 internal/authentication/authentication_dto.go create mode 100644 internal/authentication/authentication_handler.go create mode 100644 internal/authentication/authentication_repository.go create mode 100644 internal/authentication/authentication_route.go create mode 100644 internal/authentication/authentication_service.go create mode 100644 internal/cart/cart_dto.go create mode 100644 internal/cart/cart_handler.go create mode 100644 internal/cart/cart_repository.go create mode 100644 internal/cart/cart_route.go create mode 100644 internal/cart/cart_service.go create mode 100644 internal/collector/collector_dto.go create mode 100644 internal/collector/collector_handler.go create mode 100644 internal/collector/collector_repository.go create mode 100644 internal/collector/collector_route.go create mode 100644 internal/collector/collector_service.go create mode 100644 internal/company/company_dto.go create mode 100644 internal/company/company_handler.go create mode 100644 internal/company/company_repository.go create mode 100644 internal/company/company_route.go create mode 100644 internal/company/company_service.go create mode 100644 internal/identitycart/identitycart_dto.go create mode 100644 internal/identitycart/identitycart_handler.go create mode 100644 internal/identitycart/identitycart_repo.go create mode 100644 internal/identitycart/identitycart_route.go create mode 100644 internal/identitycart/identitycart_service.go create mode 100644 internal/requestpickup/requestpickup_dto.go create mode 100644 internal/requestpickup/requestpickup_handler.go create mode 100644 internal/requestpickup/requestpickup_repository.go create mode 100644 internal/requestpickup/requestpickup_route.go create mode 100644 internal/requestpickup/requestpickup_service.go create mode 100644 internal/role/role_dto.go create mode 100644 internal/role/role_handler.go create mode 100644 internal/role/role_repo.go create mode 100644 internal/role/role_route.go create mode 100644 internal/role/role_service.go create mode 100644 internal/trash/trash_dto.go create mode 100644 internal/trash/trash_handler.go create mode 100644 internal/trash/trash_repository.go create mode 100644 internal/trash/trash_route.go create mode 100644 internal/trash/trash_service.go create mode 100644 internal/userpin/userpin_dto.go create mode 100644 internal/userpin/userpin_handler.go create mode 100644 internal/userpin/userpin_repo.go create mode 100644 internal/userpin/userpin_route.go create mode 100644 internal/userpin/userpin_service.go create mode 100644 internal/userprofile/userprofile_dto.go create mode 100644 internal/userprofile/userprofile_handler.go create mode 100644 internal/userprofile/userprofile_repo.go create mode 100644 internal/userprofile/userprofile_route.go create mode 100644 internal/userprofile/userprofile_service.go create mode 100644 internal/whatsapp/whatsapp_handler.go create mode 100644 internal/whatsapp/whatsapp_route.go create mode 100644 internal/wilayahindo/wilayahindo_dto.go create mode 100644 internal/wilayahindo/wilayahindo_handler.go create mode 100644 internal/wilayahindo/wilayahindo_repository.go create mode 100644 internal/wilayahindo/wilayahindo_route.go create mode 100644 internal/wilayahindo/wilayahindo_service.go create mode 100644 middleware/additional_middleware.go delete mode 100644 middleware/auth_middleware.go create mode 100644 middleware/middleware.go delete mode 100644 middleware/role_middleware.go delete mode 100644 presentation/banner_route.go delete mode 100644 presentation/initialcoint_route.go delete mode 100644 presentation/product_route.go delete mode 100644 presentation/store_route.go delete mode 100644 presentation/whatsapp_route.go create mode 100644 utils/api_response.go create mode 100644 utils/identity_number_validator.go create mode 100644 utils/redis_utility.go delete mode 100644 utils/regexp_formatter.go create mode 100644 utils/todo_validation.go create mode 100644 utils/token_management.go diff --git a/.env.example b/.env.example index 2f50fe6..21d988c 100644 --- a/.env.example +++ b/.env.example @@ -23,3 +23,8 @@ API_KEY= #SECRET_KEY SECRET_KEY= + +# TTL +ACCESS_TOKEN_EXPIRY= +REFRESH_TOKEN_EXPIRY= +PARTIAL_TOKEN_EXPIRY= diff --git a/dto/company_profile_dto.go b/dto/company_profile_dto.go index dacd91d..f3cedd8 100644 --- a/dto/company_profile_dto.go +++ b/dto/company_profile_dto.go @@ -1,6 +1,7 @@ package dto import ( + "rijig/utils" "strings" ) @@ -45,12 +46,8 @@ func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]s errors["company_Address"] = append(errors["company_address"], "Company address is required") } - if strings.TrimSpace(r.CompanyPhone) == "" { - errors["company_Phone"] = append(errors["company_phone"], "Company phone is required") - } - - if strings.TrimSpace(r.CompanyEmail) == "" { - errors["company_Email"] = append(errors["company_email"], "Company email is required") + if !utils.IsValidPhoneNumber(r.CompanyPhone) { + errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit") } if strings.TrimSpace(r.CompanyDescription) == "" { diff --git a/dto/trash_dto.go b/dto/trash_dto.go index d1f5fd9..15d8fcc 100644 --- a/dto/trash_dto.go +++ b/dto/trash_dto.go @@ -1,5 +1,5 @@ package dto - +/* import ( "strings" ) @@ -62,3 +62,4 @@ func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, return nil, true } + */ \ No newline at end of file diff --git a/internal/about/about_dto.go b/internal/about/about_dto.go new file mode 100644 index 0000000..17f2917 --- /dev/null +++ b/internal/about/about_dto.go @@ -0,0 +1,66 @@ +package about + +import ( + "strings" +) + +type RequestAboutDTO struct { + Title string `json:"title"` + CoverImage string `json:"cover_image"` +} + +func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Title) == "" { + errors["title"] = append(errors["title"], "Title is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +type ResponseAboutDTO struct { + ID string `json:"id"` + Title string `json:"title"` + CoverImage string `json:"cover_image"` + AboutDetail *[]ResponseAboutDetailDTO `json:"about_detail"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type RequestAboutDetailDTO struct { + AboutId string `json:"about_id"` + ImageDetail string `json:"image_detail"` + Description string `json:"description"` +} + +func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.AboutId) == "" { + errors["about_id"] = append(errors["about_id"], "about_id is required") + } + + if strings.TrimSpace(r.Description) == "" { + errors["description"] = append(errors["description"], "Description is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +type ResponseAboutDetailDTO struct { + ID string `json:"id"` + AboutID string `json:"about_id"` + ImageDetail string `json:"image_detail"` + Description string `json:"description"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/about/about_handler.go b/internal/about/about_handler.go new file mode 100644 index 0000000..4ff94ae --- /dev/null +++ b/internal/about/about_handler.go @@ -0,0 +1,177 @@ +package about + +import ( + "fmt" + "log" + "rijig/dto" + "rijig/internal/services" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AboutHandler struct { + AboutService services.AboutService +} + +func NewAboutHandler(aboutService services.AboutService) *AboutHandler { + return &AboutHandler{ + AboutService: aboutService, + } +} + +func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { + var request dto.RequestAboutDTO + if err := c.BodyParser(&request); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateAbout() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + aboutCoverImage, err := c.FormFile("cover_image") + if err != nil { + return utils.BadRequest(c, "Cover image is required") + } + + response, err := h.AboutService.CreateAbout(request, aboutCoverImage) + if err != nil { + log.Printf("Error creating About: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to create About: %v", err)) + } + + return utils.CreateSuccessWithData(c, "Successfully created About", response) +} + +func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { + id := c.Params("id") + + var request dto.RequestAboutDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.BadRequest(c, "Invalid input data") + } + + aboutCoverImage, err := c.FormFile("cover_image") + if err != nil { + log.Printf("Error retrieving cover image about from request: %v", err) + return utils.BadRequest(c, "cover_image is required") + } + + response, err := h.AboutService.UpdateAbout(id, request, aboutCoverImage) + if err != nil { + log.Printf("Error updating About: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to update About: %v", err)) + } + + return utils.SuccessWithData(c, "Successfully updated About", response) +} + +func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error { + response, err := h.AboutService.GetAllAbout() + if err != nil { + log.Printf("Error fetching all About: %v", err) + return utils.InternalServerError(c, "Failed to fetch About list") + } + + return utils.SuccessWithPagination(c, "Successfully fetched About list", response, 1, len(response)) +} + +func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.AboutService.GetAboutByID(id) + if err != nil { + log.Printf("Error fetching About by ID: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) + } + + return utils.SuccessWithData(c, "Successfully fetched About", response) +} + +func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error { + id := c.Params("id") + + response, err := h.AboutService.GetAboutDetailById(id) + if err != nil { + log.Printf("Error fetching About detail by ID: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) + } + + return utils.SuccessWithData(c, "Successfully fetched About", response) +} + +func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error { + id := c.Params("id") + + if err := h.AboutService.DeleteAbout(id); err != nil { + log.Printf("Error deleting About: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to delete About: %v", err)) + } + + return utils.Success(c, "Successfully deleted About") +} + +func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { + var request dto.RequestAboutDetailDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.BadRequest(c, "Invalid input data") + } + + errors, valid := request.ValidateAboutDetail() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + aboutDetailImage, err := c.FormFile("image_detail") + if err != nil { + log.Printf("Error retrieving image detail from request: %v", err) + return utils.BadRequest(c, "image_detail is required") + } + + response, err := h.AboutService.CreateAboutDetail(request, aboutDetailImage) + if err != nil { + log.Printf("Error creating AboutDetail: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to create AboutDetail: %v", err)) + } + + return utils.CreateSuccessWithData(c, "Successfully created AboutDetail", response) +} + +func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { + id := c.Params("id") + + var request dto.RequestAboutDetailDTO + if err := c.BodyParser(&request); err != nil { + log.Printf("Error parsing request body: %v", err) + return utils.BadRequest(c, "Invalid input data") + } + + aboutDetailImage, err := c.FormFile("image_detail") + if err != nil { + log.Printf("Error retrieving image detail from request: %v", err) + return utils.BadRequest(c, "image_detail is required") + } + + response, err := h.AboutService.UpdateAboutDetail(id, request, aboutDetailImage) + if err != nil { + log.Printf("Error updating AboutDetail: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to update AboutDetail: %v", err)) + } + + return utils.SuccessWithData(c, "Successfully updated AboutDetail", response) +} + +func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error { + id := c.Params("id") + + if err := h.AboutService.DeleteAboutDetail(id); err != nil { + log.Printf("Error deleting AboutDetail: %v", err) + return utils.InternalServerError(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err)) + } + + return utils.Success(c, "Successfully deleted AboutDetail") +} diff --git a/internal/about/about_repository.go b/internal/about/about_repository.go new file mode 100644 index 0000000..ab06e14 --- /dev/null +++ b/internal/about/about_repository.go @@ -0,0 +1,113 @@ +package about + +import ( + "context" + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type AboutRepository interface { + CreateAbout(ctx context.Context, about *model.About) error + CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error + GetAllAbout(ctx context.Context) ([]model.About, error) + GetAboutByID(ctx context.Context, id string) (*model.About, error) + GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error) + GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error) + UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error) + UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) + DeleteAbout(ctx context.Context, id string) error + DeleteAboutDetail(ctx context.Context, id string) error +} + +type aboutRepository struct { + db *gorm.DB +} + +func NewAboutRepository(db *gorm.DB) AboutRepository { + return &aboutRepository{db} +} + +func (r *aboutRepository) CreateAbout(ctx context.Context, about *model.About) error { + if err := r.db.WithContext(ctx).Create(&about).Error; err != nil { + return fmt.Errorf("failed to create About: %v", err) + } + return nil +} + +func (r *aboutRepository) CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error { + if err := r.db.WithContext(ctx).Create(&aboutDetail).Error; err != nil { + return fmt.Errorf("failed to create AboutDetail: %v", err) + } + return nil +} + +func (r *aboutRepository) GetAllAbout(ctx context.Context) ([]model.About, error) { + var abouts []model.About + if err := r.db.WithContext(ctx).Find(&abouts).Error; err != nil { + return nil, fmt.Errorf("failed to fetch all About records: %v", err) + } + return abouts, nil +} + +func (r *aboutRepository) GetAboutByID(ctx context.Context, id string) (*model.About, error) { + var about model.About + if err := r.db.WithContext(ctx).Preload("AboutDetail").Where("id = ?", id).First(&about).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("about with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch About by ID: %v", err) + } + return &about, nil +} + +func (r *aboutRepository) GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error) { + var about model.About + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&about).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("about with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch About by ID: %v", err) + } + return &about, nil +} + +func (r *aboutRepository) GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error) { + var aboutDetail model.AboutDetail + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&aboutDetail).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("aboutdetail with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch AboutDetail by ID: %v", err) + } + return &aboutDetail, nil +} + +func (r *aboutRepository) UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error) { + if err := r.db.WithContext(ctx).Model(&about).Where("id = ?", id).Updates(about).Error; err != nil { + return nil, fmt.Errorf("failed to update About: %v", err) + } + return about, nil +} + +func (r *aboutRepository) UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) { + if err := r.db.WithContext(ctx).Model(&aboutDetail).Where("id = ?", id).Updates(aboutDetail).Error; err != nil { + return nil, fmt.Errorf("failed to update AboutDetail: %v", err) + } + return aboutDetail, nil +} + +func (r *aboutRepository) DeleteAbout(ctx context.Context, id string) error { + if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.About{}).Error; err != nil { + return fmt.Errorf("failed to delete About: %v", err) + } + return nil +} + +func (r *aboutRepository) DeleteAboutDetail(ctx context.Context, id string) error { + if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.AboutDetail{}).Error; err != nil { + return fmt.Errorf("failed to delete AboutDetail: %v", err) + } + return nil +} diff --git a/internal/about/about_route.go b/internal/about/about_route.go new file mode 100644 index 0000000..5f6c9a3 --- /dev/null +++ b/internal/about/about_route.go @@ -0,0 +1 @@ +package about \ No newline at end of file diff --git a/internal/about/about_service.go b/internal/about/about_service.go new file mode 100644 index 0000000..4cec901 --- /dev/null +++ b/internal/about/about_service.go @@ -0,0 +1,497 @@ +package about + +import ( + "context" + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "rijig/dto" + "rijig/model" + "rijig/utils" + "time" + + "github.com/google/uuid" +) + +const ( + cacheKeyAllAbout = "about:all" + cacheKeyAboutByID = "about:id:%s" + cacheKeyAboutDetail = "about_detail:id:%s" + + cacheTTL = 30 * time.Minute +) + +type AboutService interface { + CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) + UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) + GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error) + GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error) + GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error) + DeleteAbout(ctx context.Context, id string) error + + CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) + UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) + DeleteAboutDetail(ctx context.Context, id string) error +} + +type aboutService struct { + aboutRepo AboutRepository +} + +func NewAboutService(aboutRepo AboutRepository) AboutService { + return &aboutService{aboutRepo: aboutRepo} +} + +func (s *aboutService) invalidateAboutCaches(aboutID string) { + + if err := utils.DeleteCache(cacheKeyAllAbout); err != nil { + log.Printf("Failed to invalidate all about cache: %v", err) + } + + aboutCacheKey := fmt.Sprintf(cacheKeyAboutByID, aboutID) + if err := utils.DeleteCache(aboutCacheKey); err != nil { + log.Printf("Failed to invalidate about cache for ID %s: %v", aboutID, err) + } +} + +func (s *aboutService) invalidateAboutDetailCaches(aboutDetailID, aboutID string) { + + detailCacheKey := fmt.Sprintf(cacheKeyAboutDetail, aboutDetailID) + if err := utils.DeleteCache(detailCacheKey); err != nil { + log.Printf("Failed to invalidate about detail cache for ID %s: %v", aboutDetailID, err) + } + + s.invalidateAboutCaches(aboutID) +} + +func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutDetailDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) + + response := &dto.ResponseAboutDetailDTO{ + ID: about.ID, + AboutID: about.AboutID, + ImageDetail: about.ImageDetail, + Description: about.Description, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func formatResponseAboutDTO(about *model.About) (*dto.ResponseAboutDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) + + response := &dto.ResponseAboutDTO{ + ID: about.ID, + Title: about.Title, + CoverImage: about.CoverImage, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + return response, nil +} + +func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader) (string, error) { + pathImage := "/uploads/coverabout/" + coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { + if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { + return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverImageAbout.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_coverabout%s", uuid.New().String(), extension) + coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) + + src, err := coverImageAbout.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return "", fmt.Errorf("failed to create cover image about file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save cover image about: %v", err) + } + + coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) + return coverImageAboutUrl, nil +} + +func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) { + pathImage := "/uploads/coverabout/coveraboutdetail/" + coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { + if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { + return "", fmt.Errorf("gagal membuat direktori untuk cover image about detail: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverImageAbout.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed") + } + + coverImageFileName := fmt.Sprintf("%s_coveraboutdetail%s", uuid.New().String(), extension) + coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) + + src, err := coverImageAbout.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(coverImagePath) + if err != nil { + return "", fmt.Errorf("failed to create cover image about detail file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save cover image about detail: %v", err) + } + + coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) + return coverImageAboutUrl, nil +} + +func deleteCoverImageAbout(coverimageAboutPath string) error { + if coverimageAboutPath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + coverimageAboutPath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *aboutService) CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { + errors, valid := request.ValidateAbout() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + coverImageAboutPath, err := s.saveCoverImageAbout(coverImageAbout) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan cover image about: %v", err) + } + + about := model.About{ + Title: request.Title, + CoverImage: coverImageAboutPath, + } + + if err := s.aboutRepo.CreateAbout(ctx, &about); err != nil { + return nil, fmt.Errorf("failed to create About: %v", err) + } + + response, err := formatResponseAboutDTO(&about) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + s.invalidateAboutCaches("") + + return response, nil +} + +func (s *aboutService) UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { + errors, valid := request.ValidateAbout() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + about, err := s.aboutRepo.GetAboutByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("about not found: %v", err) + } + + oldCoverImage := about.CoverImage + + var coverImageAboutPath string + if coverImageAbout != nil { + coverImageAboutPath, err = s.saveCoverImageAbout(coverImageAbout) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) + } + } + + about.Title = request.Title + if coverImageAboutPath != "" { + about.CoverImage = coverImageAboutPath + } + + updatedAbout, err := s.aboutRepo.UpdateAbout(ctx, id, about) + if err != nil { + return nil, fmt.Errorf("failed to update About: %v", err) + } + + if oldCoverImage != "" && coverImageAboutPath != "" { + if err := deleteCoverImageAbout(oldCoverImage); err != nil { + log.Printf("Warning: failed to delete old image: %v", err) + } + } + + response, err := formatResponseAboutDTO(updatedAbout) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + s.invalidateAboutCaches(id) + + return response, nil +} + +func (s *aboutService) GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error) { + + var cachedAbouts []dto.ResponseAboutDTO + if err := utils.GetCache(cacheKeyAllAbout, &cachedAbouts); err == nil { + return cachedAbouts, nil + } + + aboutList, err := s.aboutRepo.GetAllAbout(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get About list: %v", err) + } + + var aboutDTOList []dto.ResponseAboutDTO + for _, about := range aboutList { + response, err := formatResponseAboutDTO(&about) + if err != nil { + log.Printf("Error formatting About response: %v", err) + continue + } + aboutDTOList = append(aboutDTOList, *response) + } + + if err := utils.SetCache(cacheKeyAllAbout, aboutDTOList, cacheTTL); err != nil { + log.Printf("Failed to cache all about data: %v", err) + } + + return aboutDTOList, nil +} + +func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error) { + cacheKey := fmt.Sprintf(cacheKeyAboutByID, id) + + var cachedAbout dto.ResponseAboutDTO + if err := utils.GetCache(cacheKey, &cachedAbout); err == nil { + return &cachedAbout, nil + } + + about, err := s.aboutRepo.GetAboutByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("about not found: %v", err) + } + + response, err := formatResponseAboutDTO(about) + if err != nil { + return nil, fmt.Errorf("error formatting About response: %v", err) + } + + var responseDetails []dto.ResponseAboutDetailDTO + for _, detail := range about.AboutDetail { + formattedDetail, err := formatResponseAboutDetailDTO(&detail) + if err != nil { + return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) + } + responseDetails = append(responseDetails, *formattedDetail) + } + + response.AboutDetail = &responseDetails + + if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil { + log.Printf("Failed to cache about data for ID %s: %v", id, err) + } + + return response, nil +} + +func (s *aboutService) GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error) { + cacheKey := fmt.Sprintf(cacheKeyAboutDetail, id) + + var cachedDetail dto.ResponseAboutDetailDTO + if err := utils.GetCache(cacheKey, &cachedDetail); err == nil { + return &cachedDetail, nil + } + + aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("about detail not found: %v", err) + } + + response, err := formatResponseAboutDetailDTO(aboutDetail) + if err != nil { + return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) + } + + if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil { + log.Printf("Failed to cache about detail data for ID %s: %v", id, err) + } + + return response, nil +} + +func (s *aboutService) DeleteAbout(ctx context.Context, id string) error { + about, err := s.aboutRepo.GetAboutByID(ctx, id) + if err != nil { + return fmt.Errorf("about not found: %v", err) + } + + if about.CoverImage != "" { + if err := deleteCoverImageAbout(about.CoverImage); err != nil { + log.Printf("Warning: failed to delete cover image: %v", err) + } + } + + for _, detail := range about.AboutDetail { + if detail.ImageDetail != "" { + if err := deleteCoverImageAbout(detail.ImageDetail); err != nil { + log.Printf("Warning: failed to delete detail image: %v", err) + } + } + } + + if err := s.aboutRepo.DeleteAbout(ctx, id); err != nil { + return fmt.Errorf("failed to delete About: %v", err) + } + + s.invalidateAboutCaches(id) + + return nil +} + +func (s *aboutService) CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { + errors, valid := request.ValidateAboutDetail() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + _, err := s.aboutRepo.GetAboutByIDWithoutPrel(ctx, request.AboutId) + if err != nil { + return nil, fmt.Errorf("about_id tidak ditemukan: %v", err) + } + + coverImageAboutDetailPath, err := s.saveCoverImageAboutDetail(coverImageAboutDetail) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan cover image about detail: %v", err) + } + + aboutDetail := model.AboutDetail{ + AboutID: request.AboutId, + ImageDetail: coverImageAboutDetailPath, + Description: request.Description, + } + + if err := s.aboutRepo.CreateAboutDetail(ctx, &aboutDetail); err != nil { + return nil, fmt.Errorf("failed to create AboutDetail: %v", err) + } + + response, err := formatResponseAboutDetailDTO(&aboutDetail) + if err != nil { + return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) + } + + s.invalidateAboutDetailCaches("", request.AboutId) + + return response, nil +} + +func (s *aboutService) UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { + errors, valid := request.ValidateAboutDetail() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("about detail tidak ditemukan: %v", err) + } + + oldImageDetail := aboutDetail.ImageDetail + + var coverImageAboutDetailPath string + if imageDetail != nil { + coverImageAboutDetailPath, err = s.saveCoverImageAboutDetail(imageDetail) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) + } + } + + aboutDetail.Description = request.Description + if coverImageAboutDetailPath != "" { + aboutDetail.ImageDetail = coverImageAboutDetailPath + } + + updatedAboutDetail, err := s.aboutRepo.UpdateAboutDetail(ctx, id, aboutDetail) + if err != nil { + return nil, fmt.Errorf("failed to update AboutDetail: %v", err) + } + + if oldImageDetail != "" && coverImageAboutDetailPath != "" { + if err := deleteCoverImageAbout(oldImageDetail); err != nil { + log.Printf("Warning: failed to delete old detail image: %v", err) + } + } + + response, err := formatResponseAboutDetailDTO(updatedAboutDetail) + if err != nil { + return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) + } + + s.invalidateAboutDetailCaches(id, aboutDetail.AboutID) + + return response, nil +} + +func (s *aboutService) DeleteAboutDetail(ctx context.Context, id string) error { + aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id) + if err != nil { + return fmt.Errorf("about detail tidak ditemukan: %v", err) + } + + aboutID := aboutDetail.AboutID + + if aboutDetail.ImageDetail != "" { + if err := deleteCoverImageAbout(aboutDetail.ImageDetail); err != nil { + log.Printf("Warning: failed to delete detail image: %v", err) + } + } + + if err := s.aboutRepo.DeleteAboutDetail(ctx, id); err != nil { + return fmt.Errorf("failed to delete AboutDetail: %v", err) + } + + s.invalidateAboutDetailCaches(id, aboutID) + + return nil +} diff --git a/internal/address/address_dto.go b/internal/address/address_dto.go new file mode 100644 index 0000000..7877f4d --- /dev/null +++ b/internal/address/address_dto.go @@ -0,0 +1,73 @@ +package address + +import "strings" + +type AddressResponseDTO struct { + UserID string `json:"user_id,omitempty"` + ID string `json:"address_id,omitempty"` + Province string `json:"province,omitempty"` + Regency string `json:"regency,omitempty"` + District string `json:"district,omitempty"` + Village string `json:"village,omitempty"` + PostalCode string `json:"postalCode,omitempty"` + Detail string `json:"detail,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` +} + +type CreateAddressDTO struct { + Province string `json:"province_id"` + Regency string `json:"regency_id"` + District string `json:"district_id"` + Village string `json:"village_id"` + PostalCode string `json:"postalCode"` + Detail string `json:"detail"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Province) == "" { + errors["province_id"] = append(errors["province_id"], "Province ID is required") + } + + if strings.TrimSpace(r.Regency) == "" { + errors["regency_id"] = append(errors["regency_id"], "Regency ID is required") + } + + if strings.TrimSpace(r.District) == "" { + errors["district_id"] = append(errors["district_id"], "District ID is required") + } + + if strings.TrimSpace(r.Village) == "" { + errors["village_id"] = append(errors["village_id"], "Village ID is required") + } + + if strings.TrimSpace(r.PostalCode) == "" { + errors["postalCode"] = append(errors["postalCode"], "PostalCode is required") + } else if len(r.PostalCode) < 5 { + errors["postalCode"] = append(errors["postalCode"], "PostalCode must be at least 5 characters") + } + + if strings.TrimSpace(r.Detail) == "" { + errors["detail"] = append(errors["detail"], "Detail address is required") + } + + if r.Latitude == 0 { + errors["latitude"] = append(errors["latitude"], "Latitude is required") + } + + if r.Longitude == 0 { + errors["longitude"] = append(errors["longitude"], "Longitude is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/address/address_handler.go b/internal/address/address_handler.go new file mode 100644 index 0000000..0d5cc40 --- /dev/null +++ b/internal/address/address_handler.go @@ -0,0 +1 @@ +package address \ No newline at end of file diff --git a/internal/address/address_repository.go b/internal/address/address_repository.go new file mode 100644 index 0000000..fb72457 --- /dev/null +++ b/internal/address/address_repository.go @@ -0,0 +1,62 @@ +package address + +import ( + "context" + "rijig/model" + + "gorm.io/gorm" +) + +type AddressRepository interface { + CreateAddress(ctx context.Context, address *model.Address) error + FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error) + FindAddressByID(ctx context.Context, id string) (*model.Address, error) + UpdateAddress(ctx context.Context, address *model.Address) error + DeleteAddress(ctx context.Context, id string) error +} + +type addressRepository struct { + db *gorm.DB +} + +func NewAddressRepository(db *gorm.DB) AddressRepository { + return &addressRepository{db} +} + +func (r *addressRepository) CreateAddress(ctx context.Context, address *model.Address) error { + return r.db.WithContext(ctx).Create(address).Error +} + +func (r *addressRepository) FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error) { + var addresses []model.Address + err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&addresses).Error + if err != nil { + return nil, err + } + return addresses, nil +} + +func (r *addressRepository) FindAddressByID(ctx context.Context, id string) (*model.Address, error) { + var address model.Address + err := r.db.WithContext(ctx).Where("id = ?", id).First(&address).Error + if err != nil { + return nil, err + } + return &address, nil +} + +func (r *addressRepository) UpdateAddress(ctx context.Context, address *model.Address) error { + err := r.db.WithContext(ctx).Save(address).Error + if err != nil { + return err + } + return nil +} + +func (r *addressRepository) DeleteAddress(ctx context.Context, id string) error { + err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Address{}).Error + if err != nil { + return err + } + return nil +} diff --git a/internal/address/address_route.go b/internal/address/address_route.go new file mode 100644 index 0000000..0d5cc40 --- /dev/null +++ b/internal/address/address_route.go @@ -0,0 +1 @@ +package address \ No newline at end of file diff --git a/internal/address/address_service.go b/internal/address/address_service.go new file mode 100644 index 0000000..12b9625 --- /dev/null +++ b/internal/address/address_service.go @@ -0,0 +1,250 @@ +package address + +import ( + "context" + "errors" + "fmt" + "time" + + "rijig/dto" + "rijig/internal/wilayahindo" + "rijig/model" + "rijig/utils" +) + +const ( + cacheTTL = time.Hour * 24 + + userAddressesCacheKeyPattern = "user:%s:addresses" + addressCacheKeyPattern = "address:%s" +) + +type AddressService interface { + CreateAddress(ctx context.Context, userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) + GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) + GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error) + UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) + DeleteAddress(ctx context.Context, userID, id string) error +} + +type addressService struct { + addressRepo AddressRepository + wilayahRepo wilayahindo.WilayahIndonesiaRepository +} + +func NewAddressService(addressRepo AddressRepository, wilayahRepo wilayahindo.WilayahIndonesiaRepository) AddressService { + return &addressService{ + addressRepo: addressRepo, + wilayahRepo: wilayahRepo, + } +} + +func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO dto.CreateAddressDTO) (string, string, string, string, error) { + + province, _, err := s.wilayahRepo.FindProvinceByID(ctx, addressDTO.Province, 0, 0) + if err != nil { + return "", "", "", "", fmt.Errorf("invalid province_id: %w", err) + } + + regency, _, err := s.wilayahRepo.FindRegencyByID(ctx, addressDTO.Regency, 0, 0) + if err != nil { + return "", "", "", "", fmt.Errorf("invalid regency_id: %w", err) + } + + district, _, err := s.wilayahRepo.FindDistrictByID(ctx, addressDTO.District, 0, 0) + if err != nil { + return "", "", "", "", fmt.Errorf("invalid district_id: %w", err) + } + + village, err := s.wilayahRepo.FindVillageByID(ctx, addressDTO.Village) + if err != nil { + return "", "", "", "", fmt.Errorf("invalid village_id: %w", err) + } + + return province.Name, regency.Name, district.Name, village.Name, nil +} + +func (s *addressService) mapToResponseDTO(address *model.Address) *dto.AddressResponseDTO { + createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) + + return &dto.AddressResponseDTO{ + UserID: address.UserID, + ID: address.ID, + Province: address.Province, + Regency: address.Regency, + District: address.District, + Village: address.Village, + PostalCode: address.PostalCode, + Detail: address.Detail, + Latitude: address.Latitude, + Longitude: address.Longitude, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} + +func (s *addressService) invalidateAddressCaches(userID, addressID string) { + if addressID != "" { + addressCacheKey := fmt.Sprintf(addressCacheKeyPattern, addressID) + if err := utils.DeleteCache(addressCacheKey); err != nil { + fmt.Printf("Error deleting address cache: %v\n", err) + } + } + + userCacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID) + if err := utils.DeleteCache(userCacheKey); err != nil { + fmt.Printf("Error deleting user addresses cache: %v\n", err) + } +} + +func (s *addressService) cacheAddress(addressDTO *dto.AddressResponseDTO) { + cacheKey := fmt.Sprintf(addressCacheKeyPattern, addressDTO.ID) + if err := utils.SetCache(cacheKey, addressDTO, cacheTTL); err != nil { + fmt.Printf("Error caching address to Redis: %v\n", err) + } +} + +func (s *addressService) cacheUserAddresses(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) { + addresses, err := s.addressRepo.FindAddressByUserID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to fetch addresses: %w", err) + } + + var addressDTOs []dto.AddressResponseDTO + for _, address := range addresses { + addressDTOs = append(addressDTOs, *s.mapToResponseDTO(&address)) + } + + cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID) + if err := utils.SetCache(cacheKey, addressDTOs, cacheTTL); err != nil { + fmt.Printf("Error caching addresses to Redis: %v\n", err) + } + + return addressDTOs, nil +} + +func (s *addressService) checkAddressOwnership(ctx context.Context, userID, addressID string) (*model.Address, error) { + address, err := s.addressRepo.FindAddressByID(ctx, addressID) + if err != nil { + return nil, fmt.Errorf("address not found: %w", err) + } + + if address.UserID != userID { + return nil, errors.New("you are not authorized to access this address") + } + + return address, nil +} + +func (s *addressService) CreateAddress(ctx context.Context, userID string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { + + provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO) + if err != nil { + return nil, err + } + + address := model.Address{ + UserID: userID, + Province: provinceName, + Regency: regencyName, + District: districtName, + Village: villageName, + PostalCode: addressDTO.PostalCode, + Detail: addressDTO.Detail, + Latitude: addressDTO.Latitude, + Longitude: addressDTO.Longitude, + } + + if err := s.addressRepo.CreateAddress(ctx, &address); err != nil { + return nil, fmt.Errorf("failed to create address: %w", err) + } + + responseDTO := s.mapToResponseDTO(&address) + + s.cacheAddress(responseDTO) + s.invalidateAddressCaches(userID, "") + + return responseDTO, nil +} + +func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) { + + cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID) + var cachedAddresses []dto.AddressResponseDTO + + if err := utils.GetCache(cacheKey, &cachedAddresses); err == nil { + return cachedAddresses, nil + } + + return s.cacheUserAddresses(ctx, userID) +} + +func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error) { + + address, err := s.checkAddressOwnership(ctx, userID, id) + if err != nil { + return nil, err + } + + cacheKey := fmt.Sprintf(addressCacheKeyPattern, id) + var cachedAddress dto.AddressResponseDTO + + if err := utils.GetCache(cacheKey, &cachedAddress); err == nil { + return &cachedAddress, nil + } + + responseDTO := s.mapToResponseDTO(address) + s.cacheAddress(responseDTO) + + return responseDTO, nil +} + +func (s *addressService) UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { + + address, err := s.checkAddressOwnership(ctx, userID, id) + if err != nil { + return nil, err + } + + provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO) + if err != nil { + return nil, err + } + + address.Province = provinceName + address.Regency = regencyName + address.District = districtName + address.Village = villageName + address.PostalCode = addressDTO.PostalCode + address.Detail = addressDTO.Detail + address.Latitude = addressDTO.Latitude + address.Longitude = addressDTO.Longitude + + if err := s.addressRepo.UpdateAddress(ctx, address); err != nil { + return nil, fmt.Errorf("failed to update address: %w", err) + } + + responseDTO := s.mapToResponseDTO(address) + + s.cacheAddress(responseDTO) + s.invalidateAddressCaches(userID, "") + + return responseDTO, nil +} + +func (s *addressService) DeleteAddress(ctx context.Context, userID, addressID string) error { + + address, err := s.checkAddressOwnership(ctx, userID, addressID) + if err != nil { + return err + } + + if err := s.addressRepo.DeleteAddress(ctx, addressID); err != nil { + return fmt.Errorf("failed to delete address: %w", err) + } + + s.invalidateAddressCaches(address.UserID, addressID) + + return nil +} diff --git a/internal/article/article_dto.go b/internal/article/article_dto.go new file mode 100644 index 0000000..1db8a16 --- /dev/null +++ b/internal/article/article_dto.go @@ -0,0 +1,48 @@ +package article + +import ( + "strings" +) + +type ArticleResponseDTO struct { + ID string `json:"article_id"` + Title string `json:"title"` + CoverImage string `json:"coverImage"` + Author string `json:"author"` + Heading string `json:"heading"` + Content string `json:"content"` + PublishedAt string `json:"publishedAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestArticleDTO struct { + Title string `json:"title"` + CoverImage string `json:"coverImage"` + Author string `json:"author"` + Heading string `json:"heading"` + Content string `json:"content"` +} + +func (r *RequestArticleDTO) ValidateRequestArticleDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Title) == "" { + errors["title"] = append(errors["title"], "Title is required") + } + + if strings.TrimSpace(r.Author) == "" { + errors["author"] = append(errors["author"], "Author is required") + } + if strings.TrimSpace(r.Heading) == "" { + errors["heading"] = append(errors["heading"], "Heading is required") + } + if strings.TrimSpace(r.Content) == "" { + errors["content"] = append(errors["content"], "Content is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/article/article_handler.go b/internal/article/article_handler.go new file mode 100644 index 0000000..3159b20 --- /dev/null +++ b/internal/article/article_handler.go @@ -0,0 +1,141 @@ +package article + +import ( + "mime/multipart" + "rijig/utils" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +type ArticleHandler struct { + articleService ArticleService +} + +func NewArticleHandler(articleService ArticleService) *ArticleHandler { + return &ArticleHandler{articleService} +} + +func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { + var request RequestArticleDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateRequestArticleDTO() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + coverImage, err := c.FormFile("coverImage") + if err != nil { + return utils.BadRequest(c, "Cover image is required") + } + + articleResponse, err := h.articleService.CreateArticle(c.Context(), request, coverImage) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "Article created successfully", articleResponse) +} + +func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { + page, err := strconv.Atoi(c.Query("page", "0")) + if err != nil || page < 0 { + page = 0 + } + + limit, err := strconv.Atoi(c.Query("limit", "0")) + if err != nil || limit < 0 { + limit = 0 + } + + articles, totalArticles, err := h.articleService.GetAllArticles(c.Context(), page, limit) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch articles") + } + + responseData := map[string]interface{}{ + "articles": articles, + "total": int(totalArticles), + } + + if page == 0 && limit == 0 { + return utils.SuccessWithData(c, "Articles fetched successfully", responseData) + } + + return utils.SuccessWithPagination(c, "Articles fetched successfully", responseData, page, limit) +} + +func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error { + id := c.Params("article_id") + if id == "" { + return utils.BadRequest(c, "Article ID is required") + } + + article, err := h.articleService.GetArticleByID(c.Context(), id) + if err != nil { + return utils.NotFound(c, "Article not found") + } + + return utils.SuccessWithData(c, "Article fetched successfully", article) +} + +func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error { + id := c.Params("article_id") + if id == "" { + return utils.BadRequest(c, "Article ID is required") + } + + var request RequestArticleDTO + if err := c.BodyParser(&request); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateRequestArticleDTO() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + var coverImage *multipart.FileHeader + coverImage, err := c.FormFile("coverImage") + + if err != nil && err.Error() != "no such file" && err.Error() != "there is no uploaded file associated with the given key" { + return utils.BadRequest(c, "Invalid cover image") + } + + articleResponse, err := h.articleService.UpdateArticle(c.Context(), id, request, coverImage) + if err != nil { + if isNotFoundError(err) { + return utils.NotFound(c, err.Error()) + } + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "Article updated successfully", articleResponse) +} + +func (h *ArticleHandler) DeleteArticle(c *fiber.Ctx) error { + id := c.Params("article_id") + if id == "" { + return utils.BadRequest(c, "Article ID is required") + } + + err := h.articleService.DeleteArticle(c.Context(), id) + if err != nil { + if isNotFoundError(err) { + return utils.NotFound(c, err.Error()) + } + return utils.InternalServerError(c, err.Error()) + } + + return utils.Success(c, "Article deleted successfully") +} + +func isNotFoundError(err error) bool { + return err != nil && (err.Error() == "article not found" || + err.Error() == "failed to find article: record not found" || + false) +} diff --git a/internal/article/article_repository.go b/internal/article/article_repository.go new file mode 100644 index 0000000..eb8c367 --- /dev/null +++ b/internal/article/article_repository.go @@ -0,0 +1,148 @@ +package article + +import ( + "context" + "errors" + "fmt" + + "rijig/model" + + "gorm.io/gorm" +) + +type ArticleRepository interface { + CreateArticle(ctx context.Context, article *model.Article) error + FindArticleByID(ctx context.Context, id string) (*model.Article, error) + FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error) + UpdateArticle(ctx context.Context, id string, article *model.Article) error + DeleteArticle(ctx context.Context, id string) error + ArticleExists(ctx context.Context, id string) (bool, error) +} + +type articleRepository struct { + db *gorm.DB +} + +func NewArticleRepository(db *gorm.DB) ArticleRepository { + return &articleRepository{db: db} +} + +func (r *articleRepository) CreateArticle(ctx context.Context, article *model.Article) error { + if article == nil { + return errors.New("article cannot be nil") + } + + if err := r.db.WithContext(ctx).Create(article).Error; err != nil { + return fmt.Errorf("failed to create article: %w", err) + } + return nil +} + +func (r *articleRepository) FindArticleByID(ctx context.Context, id string) (*model.Article, error) { + if id == "" { + return nil, errors.New("article ID cannot be empty") + } + + var article model.Article + err := r.db.WithContext(ctx).Where("id = ?", id).First(&article).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("article with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch article: %w", err) + } + return &article, nil +} + +func (r *articleRepository) FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error) { + var articles []model.Article + var total int64 + + if page < 0 || limit < 0 { + return nil, 0, errors.New("page and limit must be non-negative") + } + + if err := r.db.WithContext(ctx).Model(&model.Article{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count articles: %w", err) + } + + query := r.db.WithContext(ctx).Model(&model.Article{}) + + if page > 0 && limit > 0 { + offset := (page - 1) * limit + query = query.Offset(offset).Limit(limit) + } + + if err := query.Find(&articles).Error; err != nil { + return nil, 0, fmt.Errorf("failed to fetch articles: %w", err) + } + + return articles, total, nil +} + +func (r *articleRepository) UpdateArticle(ctx context.Context, id string, article *model.Article) error { + if id == "" { + return errors.New("article ID cannot be empty") + } + if article == nil { + return errors.New("article cannot be nil") + } + + exists, err := r.ArticleExists(ctx, id) + if err != nil { + return fmt.Errorf("failed to check article existence: %w", err) + } + if !exists { + return fmt.Errorf("article with ID %s not found", id) + } + + result := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Updates(article) + if result.Error != nil { + return fmt.Errorf("failed to update article: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("no rows affected when updating article with ID %s", id) + } + + return nil +} + +func (r *articleRepository) DeleteArticle(ctx context.Context, id string) error { + if id == "" { + return errors.New("article ID cannot be empty") + } + + exists, err := r.ArticleExists(ctx, id) + if err != nil { + return fmt.Errorf("failed to check article existence: %w", err) + } + if !exists { + return fmt.Errorf("article with ID %s not found", id) + } + + result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Article{}) + if result.Error != nil { + return fmt.Errorf("failed to delete article: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("no rows affected when deleting article with ID %s", id) + } + + return nil +} + +func (r *articleRepository) ArticleExists(ctx context.Context, id string) (bool, error) { + if id == "" { + return false, errors.New("article ID cannot be empty") + } + + var count int64 + err := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to check article existence: %w", err) + } + + return count > 0, nil +} diff --git a/internal/article/article_route.go b/internal/article/article_route.go new file mode 100644 index 0000000..e2928e0 --- /dev/null +++ b/internal/article/article_route.go @@ -0,0 +1,26 @@ +package article + +import ( + "rijig/config" + "rijig/internal/handler" + "rijig/internal/repositories" + "rijig/internal/services" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func ArticleRouter(api fiber.Router) { + articleRepo := repositories.NewArticleRepository(config.DB) + articleService := services.NewArticleService(articleRepo) + articleHandler := handler.NewArticleHandler(articleService) + + articleAPI := api.Group("/article") + + articleAPI.Post("/create", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.CreateArticle) + articleAPI.Get("/view", articleHandler.GetAllArticles) + articleAPI.Get("/view/:article_id", articleHandler.GetArticleByID) + articleAPI.Put("/update/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.UpdateArticle) + articleAPI.Delete("/delete/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.DeleteArticle) +} diff --git a/internal/article/article_service.go b/internal/article/article_service.go new file mode 100644 index 0000000..04f1ef9 --- /dev/null +++ b/internal/article/article_service.go @@ -0,0 +1,337 @@ +package article + +import ( + "context" + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "time" + + "rijig/model" + "rijig/utils" + + "github.com/google/uuid" +) + +type ArticleService interface { + CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) + GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error) + GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error) + UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) + DeleteArticle(ctx context.Context, id string) error +} + +type articleService struct { + articleRepo ArticleRepository +} + +func NewArticleService(articleRepo ArticleRepository) ArticleService { + return &articleService{articleRepo} +} + +func (s *articleService) transformToDTO(article *model.Article) (*ArticleResponseDTO, error) { + publishedAt, err := utils.FormatDateToIndonesianFormat(article.PublishedAt) + if err != nil { + publishedAt = "" + } + + updatedAt, err := utils.FormatDateToIndonesianFormat(article.UpdatedAt) + if err != nil { + updatedAt = "" + } + + return &ArticleResponseDTO{ + ID: article.ID, + Title: article.Title, + CoverImage: article.CoverImage, + Author: article.Author, + Heading: article.Heading, + Content: article.Content, + PublishedAt: publishedAt, + UpdatedAt: updatedAt, + }, nil +} + +func (s *articleService) transformToDTOs(articles []model.Article) ([]ArticleResponseDTO, error) { + var articleDTOs []ArticleResponseDTO + + for _, article := range articles { + dto, err := s.transformToDTO(&article) + if err != nil { + return nil, fmt.Errorf("failed to transform article %s: %w", article.ID, err) + } + articleDTOs = append(articleDTOs, *dto) + } + + return articleDTOs, nil +} + +func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) { + if coverArticle == nil { + return "", fmt.Errorf("cover image is required") + } + + pathImage := "/uploads/articles/" + coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage + + if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for cover article: %w", err) + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := filepath.Ext(coverArticle.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed") + } + + coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension) + coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName) + + src, err := coverArticle.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %w", err) + } + defer src.Close() + + dst, err := os.Create(coverArticlePath) + if err != nil { + return "", fmt.Errorf("failed to create cover article file: %w", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save cover article: %w", err) + } + + return fmt.Sprintf("%s%s", pathImage, coverArticleFileName), nil +} + +func (s *articleService) deleteCoverArticle(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + log.Printf("Image file not found (already deleted?): %s", absolutePath) + return nil + } + + if err := os.Remove(absolutePath); err != nil { + return fmt.Errorf("failed to delete image: %w", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *articleService) invalidateArticleCache(articleID string) { + + articleCacheKey := fmt.Sprintf("article:%s", articleID) + if err := utils.DeleteCache(articleCacheKey); err != nil { + log.Printf("Error deleting article cache: %v", err) + } + + if err := utils.ScanAndDelete("articles:*"); err != nil { + log.Printf("Error deleting articles cache: %v", err) + } +} + +func (s *articleService) CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) { + coverArticlePath, err := s.saveCoverArticle(coverImage) + if err != nil { + return nil, fmt.Errorf("failed to save cover image: %w", err) + } + + article := model.Article{ + Title: request.Title, + CoverImage: coverArticlePath, + Author: request.Author, + Heading: request.Heading, + Content: request.Content, + } + + if err := s.articleRepo.CreateArticle(ctx, &article); err != nil { + + if deleteErr := s.deleteCoverArticle(coverArticlePath); deleteErr != nil { + log.Printf("Failed to clean up image after create failure: %v", deleteErr) + } + return nil, fmt.Errorf("failed to create article: %w", err) + } + + articleDTO, err := s.transformToDTO(&article) + if err != nil { + return nil, fmt.Errorf("failed to transform article: %w", err) + } + + cacheKey := fmt.Sprintf("article:%s", article.ID) + if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil { + log.Printf("Error caching article: %v", err) + } + + s.invalidateArticleCache("") + + return articleDTO, nil +} + +func (s *articleService) GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error) { + + var cacheKey string + if page <= 0 || limit <= 0 { + cacheKey = "articles:all" + } else { + cacheKey = fmt.Sprintf("articles:page:%d:limit:%d", page, limit) + } + + type CachedArticlesData struct { + Articles []ArticleResponseDTO `json:"articles"` + Total int64 `json:"total"` + } + + var cachedData CachedArticlesData + if err := utils.GetCache(cacheKey, &cachedData); err == nil { + return cachedData.Articles, cachedData.Total, nil + } + + articles, total, err := s.articleRepo.FindAllArticles(ctx, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch articles: %w", err) + } + + articleDTOs, err := s.transformToDTOs(articles) + if err != nil { + return nil, 0, fmt.Errorf("failed to transform articles: %w", err) + } + + cacheData := CachedArticlesData{ + Articles: articleDTOs, + Total: total, + } + if err := utils.SetCache(cacheKey, cacheData, time.Hour*24); err != nil { + log.Printf("Error caching articles: %v", err) + } + + return articleDTOs, total, nil +} + +func (s *articleService) GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error) { + if id == "" { + return nil, fmt.Errorf("article ID cannot be empty") + } + + cacheKey := fmt.Sprintf("article:%s", id) + + var cachedArticle ArticleResponseDTO + if err := utils.GetCache(cacheKey, &cachedArticle); err == nil { + return &cachedArticle, nil + } + + article, err := s.articleRepo.FindArticleByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to fetch article: %w", err) + } + + articleDTO, err := s.transformToDTO(article) + if err != nil { + return nil, fmt.Errorf("failed to transform article: %w", err) + } + + if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil { + log.Printf("Error caching article: %v", err) + } + + return articleDTO, nil +} + +func (s *articleService) UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) { + if id == "" { + return nil, fmt.Errorf("article ID cannot be empty") + } + + existingArticle, err := s.articleRepo.FindArticleByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("article not found: %w", err) + } + + oldCoverImage := existingArticle.CoverImage + var newCoverPath string + + if coverImage != nil { + newCoverPath, err = s.saveCoverArticle(coverImage) + if err != nil { + return nil, fmt.Errorf("failed to save new cover image: %w", err) + } + } + + updatedArticle := &model.Article{ + Title: request.Title, + Author: request.Author, + Heading: request.Heading, + Content: request.Content, + CoverImage: existingArticle.CoverImage, + } + + if newCoverPath != "" { + updatedArticle.CoverImage = newCoverPath + } + + if err := s.articleRepo.UpdateArticle(ctx, id, updatedArticle); err != nil { + + if newCoverPath != "" { + s.deleteCoverArticle(newCoverPath) + } + return nil, fmt.Errorf("failed to update article: %w", err) + } + + if newCoverPath != "" && oldCoverImage != "" { + if err := s.deleteCoverArticle(oldCoverImage); err != nil { + log.Printf("Warning: failed to delete old cover image: %v", err) + } + } + + article, err := s.articleRepo.FindArticleByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to fetch updated article: %w", err) + } + + articleDTO, err := s.transformToDTO(article) + if err != nil { + return nil, fmt.Errorf("failed to transform updated article: %w", err) + } + + cacheKey := fmt.Sprintf("article:%s", id) + if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil { + log.Printf("Error caching updated article: %v", err) + } + + s.invalidateArticleCache(id) + + return articleDTO, nil +} + +func (s *articleService) DeleteArticle(ctx context.Context, id string) error { + if id == "" { + return fmt.Errorf("article ID cannot be empty") + } + + article, err := s.articleRepo.FindArticleByID(ctx, id) + if err != nil { + return fmt.Errorf("failed to find article: %w", err) + } + + if err := s.articleRepo.DeleteArticle(ctx, id); err != nil { + return fmt.Errorf("failed to delete article: %w", err) + } + + if err := s.deleteCoverArticle(article.CoverImage); err != nil { + log.Printf("Warning: failed to delete cover image: %v", err) + } + + s.invalidateArticleCache(id) + + return nil +} diff --git a/internal/authentication/authentication_dto.go b/internal/authentication/authentication_dto.go new file mode 100644 index 0000000..5e2608a --- /dev/null +++ b/internal/authentication/authentication_dto.go @@ -0,0 +1,362 @@ +package authentication + +import ( + "rijig/utils" + "strings" + "time" +) + +type LoginorRegistRequest struct { + Phone string `json:"phone" validate:"required,min=10,max=15"` + RoleName string `json:"role_name"` +} + +type VerifyOtpRequest struct { + DeviceID string `json:"device_id" validate:"required"` + RoleName string `json:"role_name" validate:"required,oneof=masyarakat pengepul pengelola"` + Phone string `json:"phone" validate:"required"` + Otp string `json:"otp" validate:"required,len=6"` +} + +type CreatePINRequest struct { + PIN string `json:"pin" validate:"required,len=6,numeric"` + ConfirmPIN string `json:"confirm_pin" validate:"required,len=6,numeric"` +} + +type VerifyPINRequest struct { + PIN string `json:"pin" validate:"required,len=6,numeric"` + DeviceID string `json:"device_id" validate:"required"` +} + +type RefreshTokenRequest struct { + RefreshToken string `json:"refresh_token" validate:"required"` + DeviceID string `json:"device_id" validate:"required"` + UserID string `json:"user_id" validate:"required"` +} + +type LogoutRequest struct { + DeviceID string `json:"device_id" validate:"required"` +} + +type OTPResponse struct { + Message string `json:"message"` + ExpiresIn int `json:"expires_in"` + Phone string `json:"phone"` +} + +type AuthResponse struct { + Message string `json:"message"` + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + TokenType string `json:"token_type,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` + User *UserResponse `json:"user,omitempty"` + RegistrationStatus string `json:"registration_status,omitempty"` + NextStep string `json:"next_step,omitempty"` + SessionID string `json:"session_id,omitempty"` +} + +type UserResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Phone string `json:"phone"` + Email string `json:"email,omitempty"` + Role string `json:"role"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int8 `json:"registration_progress"` + PhoneVerified bool `json:"phone_verified"` + Avatar *string `json:"avatar,omitempty"` + Gender string `json:"gender,omitempty"` + DateOfBirth string `json:"date_of_birth,omitempty"` + PlaceOfBirth string `json:"place_of_birth,omitempty"` +} + +type RegistrationStatusResponse struct { + CurrentStep int `json:"current_step"` + TotalSteps int `json:"total_steps"` + CompletedSteps []RegistrationStep `json:"completed_steps"` + NextStep *RegistrationStep `json:"next_step,omitempty"` + RegistrationStatus string `json:"registration_status"` + IsComplete bool `json:"is_complete"` + RequiresApproval bool `json:"requires_approval"` + ApprovalMessage string `json:"approval_message,omitempty"` +} + +type RegistrationStep struct { + StepNumber int `json:"step_number"` + Title string `json:"title"` + Description string `json:"description"` + IsRequired bool `json:"is_required"` + IsCompleted bool `json:"is_completed"` + IsActive bool `json:"is_active"` +} + +type OTPData struct { + Phone string `json:"phone"` + OTP string `json:"otp"` + UserID string `json:"user_id,omitempty"` + Role string `json:"role"` + RoleID string `json:"role_id,omitempty"` + Type string `json:"type"` + ExpiresAt time.Time `json:"expires_at"` + Attempts int `json:"attempts"` +} + +type SessionData struct { + UserID string `json:"user_id"` + DeviceID string `json:"device_id"` + Role string `json:"role"` + CreatedAt time.Time `json:"created_at"` + LastSeen time.Time `json:"last_seen"` + IsActive bool `json:"is_active"` +} + +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code string `json:"code,omitempty"` + Details interface{} `json:"details,omitempty"` +} + +type ValidationErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Fields map[string]string `json:"fields"` +} + +type ApproveRegistrationRequest struct { + UserID string `json:"user_id" validate:"required"` + Message string `json:"message,omitempty"` +} + +type RejectRegistrationRequest struct { + UserID string `json:"user_id" validate:"required"` + Reason string `json:"reason" validate:"required"` +} + +type PendingRegistrationResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Phone string `json:"phone"` + Role string `json:"role"` + RegistrationData RegistrationData `json:"registration_data"` + SubmittedAt time.Time `json:"submitted_at"` + DocumentsUploaded []DocumentInfo `json:"documents_uploaded"` +} + +type RegistrationData struct { + KTPNumber string `json:"ktp_number,omitempty"` + KTPImage string `json:"ktp_image,omitempty"` + FullName string `json:"full_name,omitempty"` + Address string `json:"address,omitempty"` + BusinessName string `json:"business_name,omitempty"` + BusinessType string `json:"business_type,omitempty"` + BusinessAddress string `json:"business_address,omitempty"` + BusinessPhone string `json:"business_phone,omitempty"` + TaxNumber string `json:"tax_number,omitempty"` + BusinessLicense string `json:"business_license,omitempty"` +} + +type DocumentInfo struct { + Type string `json:"type"` + FileName string `json:"file_name"` + UploadedAt time.Time `json:"uploaded_at"` + Status string `json:"status"` + FileSize int64 `json:"file_size"` + ContentType string `json:"content_type"` +} + +type AuthStatsResponse struct { + TotalUsers int64 `json:"total_users"` + ActiveUsers int64 `json:"active_users"` + PendingRegistrations int64 `json:"pending_registrations"` + UsersByRole map[string]int64 `json:"users_by_role"` + RegistrationStats RegistrationStatsData `json:"registration_stats"` + LoginStats LoginStatsData `json:"login_stats"` +} + +type RegistrationStatsData struct { + TotalRegistrations int64 `json:"total_registrations"` + CompletedToday int64 `json:"completed_today"` + CompletedThisWeek int64 `json:"completed_this_week"` + CompletedThisMonth int64 `json:"completed_this_month"` + PendingApproval int64 `json:"pending_approval"` + RejectedRegistrations int64 `json:"rejected_registrations"` +} + +type LoginStatsData struct { + TotalLogins int64 `json:"total_logins"` + LoginsToday int64 `json:"logins_today"` + LoginsThisWeek int64 `json:"logins_this_week"` + LoginsThisMonth int64 `json:"logins_this_month"` + UniqueUsersToday int64 `json:"unique_users_today"` + UniqueUsersWeek int64 `json:"unique_users_week"` + UniqueUsersMonth int64 `json:"unique_users_month"` +} + +type PaginationRequest struct { + Page int `json:"page" query:"page" validate:"min=1"` + Limit int `json:"limit" query:"limit" validate:"min=1,max=100"` + Sort string `json:"sort" query:"sort"` + Order string `json:"order" query:"order" validate:"oneof=asc desc"` + Search string `json:"search" query:"search"` + Filter string `json:"filter" query:"filter"` +} + +type PaginationResponse struct { + Page int `json:"page"` + Limit int `json:"limit"` + Total int64 `json:"total"` + TotalPages int `json:"total_pages"` + HasNext bool `json:"has_next"` + HasPrev bool `json:"has_prev"` +} + +type PaginatedResponse struct { + Data interface{} `json:"data"` + Pagination PaginationResponse `json:"pagination"` +} + +type SMSWebhookRequest struct { + MessageID string `json:"message_id"` + Phone string `json:"phone"` + Status string `json:"status"` + Timestamp string `json:"timestamp"` +} + +type RateLimitInfo struct { + Limit int `json:"limit"` + Remaining int `json:"remaining"` + ResetTime time.Time `json:"reset_time"` + RetryAfter time.Duration `json:"retry_after,omitempty"` +} + +type StepResponse struct { + UserID string `json:"user_id"` + Role string `json:"role"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int `json:"registration_progress"` + NextStep string `json:"next_step"` +} + +type RegisterAdminRequest struct { + Name string `json:"name"` + Gender string `json:"gender"` + DateOfBirth string `json:"dateofbirth"` + PlaceOfBirth string `json:"placeofbirth"` + Phone string `json:"phone"` + Email string `json:"email"` + Password string `json:"password"` + PasswordConfirm string `json:"password_confirm"` +} + +type LoginAdminRequest struct { + Email string `json:"email"` + Password string `json:"password"` + DeviceID string `json:"device_id"` +} + +func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func (r *VerifyOtpRequest) ValidateVerifyOtpRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + if len(strings.TrimSpace(r.DeviceID)) < 10 { + errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters") + } + + validRoles := map[string]bool{"masyarakat": true, "pengepul": true, "pengelola": true} + if _, ok := validRoles[r.RoleName]; !ok { + errors["role"] = append(errors["role"], "Role tidak valid, hanya masyarakat, pengepul, atau pengelola") + } + + if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit") + } + + if len(r.Otp) != 4 || !utils.IsNumeric(r.Otp) { + errors["otp"] = append(errors["otp"], "OTP must be 4-digit number") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func (r *LoginAdminRequest) ValidateLoginAdminRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if !utils.IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + + if strings.TrimSpace(r.Password) == "" { + errors["password"] = append(errors["password"], "Password is required") + } + + if len(strings.TrimSpace(r.DeviceID)) < 10 { + errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func (r *RegisterAdminRequest) ValidateRegisterAdminRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + + if r.Gender != "male" && r.Gender != "female" { + errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'") + } + + if strings.TrimSpace(r.DateOfBirth) == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") + } else { + _, err := time.Parse("02-01-2006", r.DateOfBirth) + if err != nil { + errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format") + } + } + + if strings.TrimSpace(r.PlaceOfBirth) == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required") + } + + if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Phone must be valid, has 8-14 digit and start with '62..'") + } + + if !utils.IsValidEmail(r.Email) { + errors["email"] = append(errors["email"], "Invalid email format") + } + + if !utils.IsValidPassword(r.Password) { + errors["password"] = append(errors["password"], "Password must be at least 8 characters, with uppercase, number, and special character") + } + + if r.Password != r.PasswordConfirm { + errors["password_confirm"] = append(errors["password_confirm"], "Passwords do not match") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go new file mode 100644 index 0000000..cc27327 --- /dev/null +++ b/internal/authentication/authentication_handler.go @@ -0,0 +1,225 @@ +package authentication + +import ( + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AuthenticationHandler struct { + service AuthenticationService +} + +func NewAuthenticationHandler(service AuthenticationService) *AuthenticationHandler { + return &AuthenticationHandler{service} +} + +func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error { + deviceID := c.Get("X-Device-ID") + if deviceID == "" { + return utils.BadRequest(c, "Device ID is required") + } + + var body struct { + RefreshToken string `json:"refresh_token"` + } + if err := c.BodyParser(&body); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + if body.RefreshToken == "" { + return utils.BadRequest(c, "Refresh token is required") + } + + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "Unauthorized or missing user ID") + } + + tokenData, err := utils.RefreshAccessToken(userID, deviceID, body.RefreshToken) + if err != nil { + return utils.Unauthorized(c, err.Error()) + } + + return utils.SuccessWithData(c, "Token refreshed successfully", tokenData) + +} + +func (h *AuthenticationHandler) GetMe(c *fiber.Ctx) error { + userID, _ := c.Locals("user_id").(string) + role, _ := c.Locals("role").(string) + deviceID, _ := c.Locals("device_id").(string) + regStatus, _ := c.Locals("registration_status").(string) + + data := fiber.Map{ + "user_id": userID, + "role": role, + "device_id": deviceID, + "registration_status": regStatus, + } + + return utils.SuccessWithData(c, "User session data retrieved", data) + +} + +func (h *AuthenticationHandler) Login(c *fiber.Ctx) error { + + var req LoginAdminRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if errs, ok := req.ValidateLoginAdminRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": fiber.StatusBadRequest, + "message": "Validation failed", + }, + "errors": errs, + }) + } + + res, err := h.service.LoginAdmin(c.Context(), &req) + if err != nil { + return utils.Unauthorized(c, err.Error()) + } + + return utils.SuccessWithData(c, "Login successful", res) + +} + +func (h *AuthenticationHandler) Register(c *fiber.Ctx) error { + + var req RegisterAdminRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if errs, ok := req.ValidateRegisterAdminRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": fiber.StatusBadRequest, + "message": "periksa lagi inputan", + }, + "errors": errs, + }) + } + + err := h.service.RegisterAdmin(c.Context(), &req) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.Success(c, "Registration successful, Please login") +} + +func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error { + var req LoginorRegistRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if errs, ok := req.ValidateLoginorRegistRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": fiber.StatusBadRequest, + "message": "Input tidak valid", + }, + "errors": errs, + }) + } + + _, err := h.service.SendLoginOTP(c.Context(), &req) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.Success(c, "OTP sent successfully") +} + +func (h *AuthenticationHandler) VerifyOtpHandler(c *fiber.Ctx) error { + var req VerifyOtpRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + if errs, ok := req.ValidateVerifyOtpRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"}, + "errors": errs, + }) + } + + stepResp, err := h.service.VerifyLoginOTP(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "OTP verified successfully", stepResp) +} + +func (h *AuthenticationHandler) RequestOtpRegistHandler(c *fiber.Ctx) error { + var req LoginorRegistRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if errs, ok := req.ValidateLoginorRegistRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{ + "status": fiber.StatusBadRequest, + "message": "Input tidak valid", + }, + "errors": errs, + }) + } + + _, err := h.service.SendRegistrationOTP(c.Context(), &req) + if err != nil { + return utils.Forbidden(c, err.Error()) + } + + return utils.Success(c, "OTP sent successfully") +} + +func (h *AuthenticationHandler) VerifyOtpRegistHandler(c *fiber.Ctx) error { + var req VerifyOtpRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + if errs, ok := req.ValidateVerifyOtpRequest(); !ok { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"}, + "errors": errs, + }) + } + + stepResp, err := h.service.VerifyRegistrationOTP(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "OTP verified successfully", stepResp) +} + +func (h *AuthenticationHandler) LogoutAuthentication(c *fiber.Ctx) error { + + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + // deviceID := c.Get("Device-ID") + // if deviceID == "" { + // return utils.BadRequest(c, "Device ID is required") + // } + + err = h.service.LogoutAuthentication(c.Context(), claims.UserID, claims.DeviceID) + if err != nil { + + return utils.InternalServerError(c, "Failed to logout") + } + + return utils.Success(c, "Logout successful") +} diff --git a/internal/authentication/authentication_repository.go b/internal/authentication/authentication_repository.go new file mode 100644 index 0000000..4993150 --- /dev/null +++ b/internal/authentication/authentication_repository.go @@ -0,0 +1,86 @@ +package authentication + +import ( + "context" + "rijig/model" + + "gorm.io/gorm" +) + +type AuthenticationRepository interface { + FindUserByPhone(ctx context.Context, phone string) (*model.User, error) + FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error) + FindUserByEmail(ctx context.Context, email string) (*model.User, error) + FindUserByID(ctx context.Context, userID string) (*model.User, error) + CreateUser(ctx context.Context, user *model.User) error + UpdateUser(ctx context.Context, user *model.User) error + PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error +} + +type authenticationRepository struct { + db *gorm.DB +} + +func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository { + return &authenticationRepository{db} +} + +func (r *authenticationRepository) FindUserByPhone(ctx context.Context, phone string) (*model.User, error) { + var user model.User + if err := r.db.WithContext(ctx). + Preload("Role"). + Where("phone = ?", phone).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authenticationRepository) FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error) { + var user model.User + if err := r.db.WithContext(ctx). + Preload("Role"). + Joins("JOIN roles AS role ON role.id = users.role_id"). + Where("users.phone = ? AND role.role_name = ?", phone, rolename). + First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authenticationRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) { + var user model.User + if err := r.db.WithContext(ctx). + Preload("Role"). + Where("email = ?", email).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authenticationRepository) FindUserByID(ctx context.Context, userID string) (*model.User, error) { + var user model.User + if err := r.db.WithContext(ctx). + Preload("Role"). + First(&user, "id = ?", userID).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (r *authenticationRepository) CreateUser(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx).Create(user).Error +} + +func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", user.ID). + Updates(user).Error +} + +func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error { + return r.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", userID). + Updates(updates).Error +} diff --git a/internal/authentication/authentication_route.go b/internal/authentication/authentication_route.go new file mode 100644 index 0000000..38b489e --- /dev/null +++ b/internal/authentication/authentication_route.go @@ -0,0 +1,40 @@ +package authentication + +import ( + "rijig/config" + "rijig/internal/role" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AuthenticationRouter(api fiber.Router) { + repoAuth := NewAuthenticationRepository(config.DB) + repoRole := role.NewRoleRepository(config.DB) + + authService := NewAuthenticationService(repoAuth, repoRole) + authHandler := NewAuthenticationHandler(authService) + + authRoute := api.Group("/auth") + + authRoute.Post("/refresh-token", + middleware.AuthMiddleware(), + middleware.DeviceValidation(), + authHandler.RefreshToken, + ) + + // authRoute.Get("/me", + // middleware.AuthMiddleware(), + // middleware.CheckRefreshTokenTTL(30*time.Second), + // middleware.RequireApprovedRegistration(), + // authHandler.GetMe, + // ) + + authRoute.Post("/login/admin", authHandler.Login) + authRoute.Post("/register/admin", authHandler.Register) + authRoute.Post("/request-otp", authHandler.RequestOtpHandler) + authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler) + authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler) + authRoute.Post("/verif-otp/register", authHandler.VerifyOtpRegistHandler) + authRoute.Post("/logout", middleware.AuthMiddleware(), authHandler.LogoutAuthentication) +} diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go new file mode 100644 index 0000000..8a9dff8 --- /dev/null +++ b/internal/authentication/authentication_service.go @@ -0,0 +1,382 @@ +package authentication + +import ( + "context" + "fmt" + "strings" + "time" + + "rijig/internal/role" + "rijig/model" + "rijig/utils" +) + +type AuthenticationService interface { + LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) + RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error + + SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) + VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) + + SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) + VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) + + LogoutAuthentication(ctx context.Context, userID, deviceID string) error +} + +type authenticationService struct { + authRepo AuthenticationRepository + roleRepo role.RoleRepository +} + +func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService { + return &authenticationService{authRepo, roleRepo} +} + +func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) { + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + if user.Role == nil || user.Role.RoleName != "administrator" { + return nil, fmt.Errorf("user not found: %w", err) + } + + if user.RegistrationStatus != "completed" { + return nil, fmt.Errorf("user not found: %w", err) + } + + if !utils.CompareHashAndPlainText(user.Password, req.Password) { + return nil, fmt.Errorf("user not found: %w", err) + } + + token, err := utils.GenerateTokenPair(user.ID, user.Role.RoleName, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress)) + if err != nil { + return nil, fmt.Errorf("failed to generate token: %w", err) + } + + return &AuthResponse{ + Message: "login berhasil", + AccessToken: token.AccessToken, + RefreshToken: token.RefreshToken, + RegistrationStatus: user.RegistrationStatus, + SessionID: token.SessionID, + }, nil +} + +func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error { + + existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email) + if existingUser != nil { + return fmt.Errorf("email already in use") + } + + hashedPassword, err := utils.HashingPlainText(req.Password) + if err != nil { + return fmt.Errorf("failed to hash password: %w", err) + } + + role, err := s.roleRepo.FindRoleByName(ctx, "administrator") + if err != nil { + return fmt.Errorf("role name not found: %w", err) + } + + user := &model.User{ + Name: req.Name, + Phone: req.Phone, + Email: req.Email, + Gender: req.Gender, + Dateofbirth: req.DateOfBirth, + Placeofbirth: req.PlaceOfBirth, + Password: hashedPassword, + RoleID: role.ID, + RegistrationStatus: "completed", + } + + if err := s.authRepo.CreateUser(ctx, user); err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + return nil +} + +func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) { + + existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, strings.ToLower(req.RoleName)) + if err == nil && existingUser != nil { + return nil, fmt.Errorf("nomor telepon dengan role %s sudah terdaftar", req.RoleName) + } + + roleData, err := s.roleRepo.FindRoleByName(ctx, req.RoleName) + if err != nil { + return nil, fmt.Errorf("role tidak valid") + } + + rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone) + if isRateLimited(rateLimitKey, 3, 5*time.Minute) { + return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit") + } + + otp, err := utils.GenerateOTP() + if err != nil { + return nil, fmt.Errorf("gagal generate OTP: %v", err) + } + + otpKey := fmt.Sprintf("otp:%s:register", req.Phone) + otpData := OTPData{ + Phone: req.Phone, + OTP: otp, + Role: req.RoleName, + RoleID: roleData.ID, + Type: "register", + + Attempts: 0, + } + + err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) + } + + err = sendOTPViaSMS(req.Phone, otp) + if err != nil { + return nil, fmt.Errorf("gagal mengirim OTP: %v", err) + } + + return &OTPResponse{ + Message: "OTP berhasil dikirim", + ExpiresIn: 60, + Phone: maskPhoneNumber(req.Phone), + }, nil +} + +func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) { + otpKey := fmt.Sprintf("otp:%s:register", req.Phone) + var otpData OTPData + err := utils.GetCache(otpKey, &otpData) + if err != nil { + return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa") + } + + if otpData.Attempts >= 3 { + utils.DeleteCache(otpKey) + return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru") + } + + if otpData.OTP != req.Otp { + otpData.Attempts++ + utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt)) + return nil, fmt.Errorf("kode OTP salah") + } + + if otpData.Role != req.RoleName { + return nil, fmt.Errorf("role tidak sesuai") + } + + user := &model.User{ + Phone: req.Phone, + PhoneVerified: true, + RoleID: otpData.RoleID, + RegistrationStatus: utils.RegStatusIncomplete, + RegistrationProgress: 0, + Name: "", + Gender: "", + Dateofbirth: "", + Placeofbirth: "", + } + + err = s.authRepo.CreateUser(ctx, user) + if err != nil { + return nil, fmt.Errorf("gagal membuat user: %v", err) + } + + utils.DeleteCache(otpKey) + + tokenResponse, err := utils.GenerateTokenPair( + user.ID, + req.RoleName, + req.DeviceID, + user.RegistrationStatus, + int(user.RegistrationProgress), + ) + + if err != nil { + return nil, fmt.Errorf("gagal generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep(req.RoleName, int(user.RegistrationProgress),user.RegistrationStatus) + + return &AuthResponse{ + Message: "Registrasi berhasil", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + + RegistrationStatus: user.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil +} + +func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) { + + user, err := s.authRepo.FindUserByPhone(ctx, req.Phone) + if err != nil { + return nil, fmt.Errorf("nomor telepon tidak terdaftar") + } + + if !user.PhoneVerified { + return nil, fmt.Errorf("nomor telepon belum diverifikasi") + } + + rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone) + if isRateLimited(rateLimitKey, 3, 5*time.Minute) { + return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit") + } + + otp, err := utils.GenerateOTP() + if err != nil { + return nil, fmt.Errorf("gagal generate OTP: %v", err) + } + + otpKey := fmt.Sprintf("otp:%s:login", req.Phone) + otpData := OTPData{ + Phone: req.Phone, + OTP: otp, + UserID: user.ID, + Role: user.Role.RoleName, + Type: "login", + Attempts: 0, + } + + err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute) + if err != nil { + return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) + } + + err = sendOTPViaSMS(req.Phone, otp) + if err != nil { + return nil, fmt.Errorf("gagal mengirim OTP: %v", err) + } + + return &OTPResponse{ + Message: "OTP berhasil dikirim", + ExpiresIn: 300, + Phone: maskPhoneNumber(req.Phone), + }, nil +} + +func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) { + + otpKey := fmt.Sprintf("otp:%s:login", req.Phone) + var otpData OTPData + err := utils.GetCache(otpKey, &otpData) + if err != nil { + return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa") + } + + if otpData.Attempts >= 3 { + utils.DeleteCache(otpKey) + return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru") + } + + if otpData.OTP != req.Otp { + otpData.Attempts++ + utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt)) + return nil, fmt.Errorf("kode OTP salah") + } + + user, err := s.authRepo.FindUserByID(ctx, otpData.UserID) + if err != nil { + return nil, fmt.Errorf("user tidak ditemukan") + } + + utils.DeleteCache(otpKey) + + tokenResponse, err := utils.GenerateTokenPair( + user.ID, + user.Role.RoleName, + req.DeviceID, + "pin_verification_required", + int(user.RegistrationProgress), + ) + if err != nil { + return nil, fmt.Errorf("gagal generate token: %v", err) + } + + return &AuthResponse{ + Message: "OTP berhasil diverifikasi, silakan masukkan PIN", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + User: convertUserToResponse(user), + RegistrationStatus: user.RegistrationStatus, + NextStep: "Masukkan PIN", + SessionID: tokenResponse.SessionID, + }, nil +} + +func (s *authenticationService) LogoutAuthentication(ctx context.Context, userID, deviceID string) error { + if err := utils.RevokeRefreshToken(userID, deviceID); err != nil { + return fmt.Errorf("failed to revoke token: %w", err) + } + return nil +} + +func maskPhoneNumber(phone string) string { + if len(phone) < 4 { + return phone + } + return phone[:4] + strings.Repeat("*", len(phone)-8) + phone[len(phone)-4:] +} + +func isRateLimited(key string, maxAttempts int, duration time.Duration) bool { + var count int + err := utils.GetCache(key, &count) + if err != nil { + count = 0 + } + + if count >= maxAttempts { + return true + } + + count++ + utils.SetCache(key, count, duration) + return false +} + +func sendOTPViaSMS(phone, otp string) error { + + fmt.Printf("Sending OTP %s to %s\n", otp, phone) + return nil +} + +func convertUserToResponse(user *model.User) *UserResponse { + return &UserResponse{ + ID: user.ID, + Name: user.Name, + Phone: user.Phone, + Email: user.Email, + Role: user.Role.RoleName, + RegistrationStatus: user.RegistrationStatus, + RegistrationProgress: user.RegistrationProgress, + PhoneVerified: user.PhoneVerified, + Avatar: user.Avatar, + } +} + +func IsRegistrationComplete(role string, progress int) bool { + switch role { + case "masyarakat": + return progress >= 1 + case "pengepul": + return progress >= 2 + case "pengelola": + return progress >= 3 + } + return false +} diff --git a/internal/cart/cart_dto.go b/internal/cart/cart_dto.go new file mode 100644 index 0000000..795236c --- /dev/null +++ b/internal/cart/cart_dto.go @@ -0,0 +1 @@ +package cart \ No newline at end of file diff --git a/internal/cart/cart_handler.go b/internal/cart/cart_handler.go new file mode 100644 index 0000000..795236c --- /dev/null +++ b/internal/cart/cart_handler.go @@ -0,0 +1 @@ +package cart \ No newline at end of file diff --git a/internal/cart/cart_repository.go b/internal/cart/cart_repository.go new file mode 100644 index 0000000..795236c --- /dev/null +++ b/internal/cart/cart_repository.go @@ -0,0 +1 @@ +package cart \ No newline at end of file diff --git a/internal/cart/cart_route.go b/internal/cart/cart_route.go new file mode 100644 index 0000000..795236c --- /dev/null +++ b/internal/cart/cart_route.go @@ -0,0 +1 @@ +package cart \ No newline at end of file diff --git a/internal/cart/cart_service.go b/internal/cart/cart_service.go new file mode 100644 index 0000000..795236c --- /dev/null +++ b/internal/cart/cart_service.go @@ -0,0 +1 @@ +package cart \ No newline at end of file diff --git a/internal/collector/collector_dto.go b/internal/collector/collector_dto.go new file mode 100644 index 0000000..c87b2bd --- /dev/null +++ b/internal/collector/collector_dto.go @@ -0,0 +1 @@ +package collector \ No newline at end of file diff --git a/internal/collector/collector_handler.go b/internal/collector/collector_handler.go new file mode 100644 index 0000000..c87b2bd --- /dev/null +++ b/internal/collector/collector_handler.go @@ -0,0 +1 @@ +package collector \ No newline at end of file diff --git a/internal/collector/collector_repository.go b/internal/collector/collector_repository.go new file mode 100644 index 0000000..c87b2bd --- /dev/null +++ b/internal/collector/collector_repository.go @@ -0,0 +1 @@ +package collector \ No newline at end of file diff --git a/internal/collector/collector_route.go b/internal/collector/collector_route.go new file mode 100644 index 0000000..c87b2bd --- /dev/null +++ b/internal/collector/collector_route.go @@ -0,0 +1 @@ +package collector \ No newline at end of file diff --git a/internal/collector/collector_service.go b/internal/collector/collector_service.go new file mode 100644 index 0000000..c87b2bd --- /dev/null +++ b/internal/collector/collector_service.go @@ -0,0 +1 @@ +package collector \ No newline at end of file diff --git a/internal/company/company_dto.go b/internal/company/company_dto.go new file mode 100644 index 0000000..eed6e4d --- /dev/null +++ b/internal/company/company_dto.go @@ -0,0 +1,62 @@ +package company + +import ( + "rijig/utils" + "strings" +) + +type ResponseCompanyProfileDTO struct { + ID string `json:"id"` + UserID string `json:"userId"` + CompanyName string `json:"company_name"` + CompanyAddress string `json:"company_address"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` + CompanyLogo string `json:"company_logo,omitempty"` + CompanyWebsite string `json:"company_website,omitempty"` + TaxID string `json:"taxId,omitempty"` + FoundedDate string `json:"founded_date,omitempty"` + CompanyType string `json:"company_type,omitempty"` + CompanyDescription string `json:"company_description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestCompanyProfileDTO struct { + CompanyName string `json:"company_name"` + CompanyAddress string `json:"company_address"` + CompanyPhone string `json:"company_phone"` + CompanyEmail string `json:"company_email"` + CompanyLogo string `json:"company_logo,omitempty"` + CompanyWebsite string `json:"company_website,omitempty"` + TaxID string `json:"taxId,omitempty"` + FoundedDate string `json:"founded_date,omitempty"` + CompanyType string `json:"company_type,omitempty"` + CompanyDescription string `json:"company_description"` +} + +func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CompanyName) == "" { + errors["company_Name"] = append(errors["company_name"], "Company name is required") + } + + if strings.TrimSpace(r.CompanyAddress) == "" { + errors["company_Address"] = append(errors["company_address"], "Company address is required") + } + + if !utils.IsValidPhoneNumber(r.CompanyPhone) { + errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit") + } + + if strings.TrimSpace(r.CompanyDescription) == "" { + errors["company_Description"] = append(errors["company_description"], "Company description is required") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/company/company_handler.go b/internal/company/company_handler.go new file mode 100644 index 0000000..dcdd13e --- /dev/null +++ b/internal/company/company_handler.go @@ -0,0 +1,111 @@ +package company + +import ( + "context" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CompanyProfileHandler struct { + service CompanyProfileService +} + +func NewCompanyProfileHandler(service CompanyProfileService) *CompanyProfileHandler { + return &CompanyProfileHandler{ + service: service, + } +} + +func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User not authenticated") + } + + var req RequestCompanyProfileDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "invalid request body") + } + + if errors, valid := req.ValidateCompanyProfileInput(); !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors) + } + + res, err := h.service.CreateCompanyProfile(context.Background(), userID, &req) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "company profile created successfully", res) +} + +func (h *CompanyProfileHandler) GetCompanyProfileByID(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User not authenticated") + } + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "id is required") + } + + res, err := h.service.GetCompanyProfileByID(context.Background(), id) + if err != nil { + return utils.NotFound(c, err.Error()) + } + + return utils.SuccessWithData(c, "company profile retrieved", res) +} + +func (h *CompanyProfileHandler) GetCompanyProfilesByUserID(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User not authenticated") + } + + res, err := h.service.GetCompanyProfilesByUserID(context.Background(), userID) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "company profiles retrieved", res) +} + +func (h *CompanyProfileHandler) UpdateCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User not authenticated") + } + + var req RequestCompanyProfileDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "invalid request body") + } + + if errors, valid := req.ValidateCompanyProfileInput(); !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors) + } + + res, err := h.service.UpdateCompanyProfile(context.Background(), userID, &req) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "company profile updated", res) +} + +func (h *CompanyProfileHandler) DeleteCompanyProfile(c *fiber.Ctx) error { + userID, ok := c.Locals("user_id").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User not authenticated") + } + + err := h.service.DeleteCompanyProfile(context.Background(), userID) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.Success(c, "company profile deleted") +} diff --git a/internal/company/company_repository.go b/internal/company/company_repository.go new file mode 100644 index 0000000..cbea2a1 --- /dev/null +++ b/internal/company/company_repository.go @@ -0,0 +1,89 @@ +package company + +import ( + "context" + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type CompanyProfileRepository interface { + CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) + GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error) + GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error) + UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error + DeleteCompanyProfileByUserID(ctx context.Context, userID string) error + ExistsByUserID(ctx context.Context, userID string) (bool, error) +} + +type companyProfileRepository struct { + db *gorm.DB +} + +func NewCompanyProfileRepository(db *gorm.DB) CompanyProfileRepository { + return &companyProfileRepository{db} +} + +func (r *companyProfileRepository) CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) { + err := r.db.WithContext(ctx).Create(companyProfile).Error + if err != nil { + return nil, fmt.Errorf("failed to create company profile: %v", err) + } + return companyProfile, nil +} + +func (r *companyProfileRepository) GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error) { + var companyProfile model.CompanyProfile + err := r.db.WithContext(ctx).Preload("User").First(&companyProfile, "id = ?", id).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("company profile with ID %s not found", id) + } + return nil, fmt.Errorf("error fetching company profile: %v", err) + } + return &companyProfile, nil +} + +func (r *companyProfileRepository) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error) { + var companyProfiles []model.CompanyProfile + err := r.db.WithContext(ctx).Preload("User").Where("user_id = ?", userID).Find(&companyProfiles).Error + if err != nil { + return nil, fmt.Errorf("error fetching company profiles for userID %s: %v", userID, err) + } + return companyProfiles, nil +} + +func (r *companyProfileRepository) UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error { + var existing model.CompanyProfile + if err := r.db.WithContext(ctx).First(&existing, "user_id = ?", company.UserID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return fmt.Errorf("company profile not found for user_id %s", company.UserID) + } + return fmt.Errorf("failed to fetch company profile: %v", err) + } + + err := r.db.WithContext(ctx).Model(&existing).Updates(company).Error + if err != nil { + return fmt.Errorf("failed to update company profile: %v", err) + } + return nil +} + +func (r *companyProfileRepository) DeleteCompanyProfileByUserID(ctx context.Context, userID string) error { + err := r.db.WithContext(ctx).Delete(&model.CompanyProfile{}, "user_id = ?", userID).Error + if err != nil { + return fmt.Errorf("failed to delete company profile: %v", err) + } + return nil +} + +func (r *companyProfileRepository) ExistsByUserID(ctx context.Context, userID string) (bool, error) { + var count int64 + err := r.db.WithContext(ctx).Model(&model.CompanyProfile{}). + Where("user_id = ?", userID).Count(&count).Error + if err != nil { + return false, fmt.Errorf("failed to check existence: %v", err) + } + return count > 0, nil +} diff --git a/internal/company/company_route.go b/internal/company/company_route.go new file mode 100644 index 0000000..78012aa --- /dev/null +++ b/internal/company/company_route.go @@ -0,0 +1,23 @@ +package company + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func CompanyRouter(api fiber.Router) { + companyProfileRepo := NewCompanyProfileRepository(config.DB) + companyProfileService := NewCompanyProfileService(companyProfileRepo) + companyProfileHandler := NewCompanyProfileHandler(companyProfileService) + + companyProfileAPI := api.Group("/companyprofile") + companyProfileAPI.Use(middleware.AuthMiddleware()) + + companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile) + companyProfileAPI.Get("/get/:id", companyProfileHandler.GetCompanyProfileByID) + companyProfileAPI.Get("/get", companyProfileHandler.GetCompanyProfilesByUserID) + companyProfileAPI.Put("/update", companyProfileHandler.UpdateCompanyProfile) + companyProfileAPI.Delete("/delete", companyProfileHandler.DeleteCompanyProfile) +} diff --git a/internal/company/company_service.go b/internal/company/company_service.go new file mode 100644 index 0000000..ebef795 --- /dev/null +++ b/internal/company/company_service.go @@ -0,0 +1,136 @@ +package company + +import ( + "context" + "fmt" + "rijig/model" + "rijig/utils" +) + +type CompanyProfileService interface { + CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) + GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) + GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) + UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) + DeleteCompanyProfile(ctx context.Context, userID string) error +} + +type companyProfileService struct { + companyRepo CompanyProfileRepository +} + +func NewCompanyProfileService(companyRepo CompanyProfileRepository) CompanyProfileService { + return &companyProfileService{ + companyRepo: companyRepo, + } +} + +func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*ResponseCompanyProfileDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.UpdatedAt) + + return &ResponseCompanyProfileDTO{ + ID: companyProfile.ID, + UserID: companyProfile.UserID, + CompanyName: companyProfile.CompanyName, + CompanyAddress: companyProfile.CompanyAddress, + CompanyPhone: companyProfile.CompanyPhone, + CompanyEmail: companyProfile.CompanyEmail, + CompanyLogo: companyProfile.CompanyLogo, + CompanyWebsite: companyProfile.CompanyWebsite, + TaxID: companyProfile.TaxID, + FoundedDate: companyProfile.FoundedDate, + CompanyType: companyProfile.CompanyType, + CompanyDescription: companyProfile.CompanyDescription, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil +} + +func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) { + if errors, valid := request.ValidateCompanyProfileInput(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + companyProfile := &model.CompanyProfile{ + UserID: userID, + CompanyName: request.CompanyName, + CompanyAddress: request.CompanyAddress, + CompanyPhone: request.CompanyPhone, + CompanyEmail: request.CompanyEmail, + CompanyLogo: request.CompanyLogo, + CompanyWebsite: request.CompanyWebsite, + TaxID: request.TaxID, + FoundedDate: request.FoundedDate, + CompanyType: request.CompanyType, + CompanyDescription: request.CompanyDescription, + } + + created, err := s.companyRepo.CreateCompanyProfile(ctx, companyProfile) + if err != nil { + return nil, err + } + + return FormatResponseCompanyProfile(created) +} + +func (s *companyProfileService) GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) { + profile, err := s.companyRepo.GetCompanyProfileByID(ctx, id) + if err != nil { + return nil, err + } + return FormatResponseCompanyProfile(profile) +} + +func (s *companyProfileService) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) { + profiles, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID) + if err != nil { + return nil, err + } + + var responses []ResponseCompanyProfileDTO + for _, p := range profiles { + dto, err := FormatResponseCompanyProfile(&p) + if err != nil { + continue + } + responses = append(responses, *dto) + } + + return responses, nil +} + +func (s *companyProfileService) UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) { + if errors, valid := request.ValidateCompanyProfileInput(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + company := &model.CompanyProfile{ + UserID: userID, + CompanyName: request.CompanyName, + CompanyAddress: request.CompanyAddress, + CompanyPhone: request.CompanyPhone, + CompanyEmail: request.CompanyEmail, + CompanyLogo: request.CompanyLogo, + CompanyWebsite: request.CompanyWebsite, + TaxID: request.TaxID, + FoundedDate: request.FoundedDate, + CompanyType: request.CompanyType, + CompanyDescription: request.CompanyDescription, + } + + if err := s.companyRepo.UpdateCompanyProfile(ctx, company); err != nil { + return nil, err + } + + updated, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID) + if err != nil || len(updated) == 0 { + return nil, fmt.Errorf("failed to retrieve updated company profile") + } + + return FormatResponseCompanyProfile(&updated[0]) +} + +func (s *companyProfileService) DeleteCompanyProfile(ctx context.Context, userID string) error { + return s.companyRepo.DeleteCompanyProfileByUserID(ctx, userID) +} diff --git a/internal/handler/about_handler.go b/internal/handler/about_handler.go index ebe895b..92fca2c 100644 --- a/internal/handler/about_handler.go +++ b/internal/handler/about_handler.go @@ -1,5 +1,5 @@ package handler - +/* import ( "fmt" "log" @@ -24,7 +24,7 @@ func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { var request dto.RequestAboutDTO if err := c.BodyParser(&request); err != nil { log.Printf("Error parsing request body: %v", err) - return utils.ErrorResponse(c, "Invalid input data") + return utils.ResponseErrorData(c, "Invalid input data") } aboutCoverImage, err := c.FormFile("cover_image") @@ -173,3 +173,4 @@ func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error { return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail") } + */ \ No newline at end of file diff --git a/internal/handler/auth/auth_admin_handler.go b/internal/handler/auth/auth_admin_handler.go index 98341b4..3f3d78f 100644 --- a/internal/handler/auth/auth_admin_handler.go +++ b/internal/handler/auth/auth_admin_handler.go @@ -1,5 +1,5 @@ package handler - +/* import ( "log" dto "rijig/dto/auth" @@ -78,3 +78,4 @@ func (h *AuthAdminHandler) LogoutAdmin(c *fiber.Ctx) error { return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out") } + */ \ No newline at end of file diff --git a/internal/handler/auth/auth_masyarakat_handler.go b/internal/handler/auth/auth_masyarakat_handler.go index 426e689..cf3750d 100644 --- a/internal/handler/auth/auth_masyarakat_handler.go +++ b/internal/handler/auth/auth_masyarakat_handler.go @@ -1,5 +1,5 @@ package handler - +/* import ( "log" "rijig/dto" @@ -79,3 +79,4 @@ func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Logged out successfully") } + */ \ No newline at end of file diff --git a/internal/handler/auth/auth_pengepul_handler.go b/internal/handler/auth/auth_pengepul_handler.go index 50da9ab..f034ab2 100644 --- a/internal/handler/auth/auth_pengepul_handler.go +++ b/internal/handler/auth/auth_pengepul_handler.go @@ -1,5 +1,5 @@ package handler - +/* import ( "log" "rijig/dto" @@ -79,3 +79,4 @@ func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Logged out successfully") } + */ \ No newline at end of file diff --git a/internal/handler/auth/auth_pnegelola_handler.go b/internal/handler/auth/auth_pnegelola_handler.go index 1491f74..5382cf6 100644 --- a/internal/handler/auth/auth_pnegelola_handler.go +++ b/internal/handler/auth/auth_pnegelola_handler.go @@ -1,5 +1,5 @@ package handler - +/* import ( "log" "rijig/dto" @@ -79,3 +79,4 @@ func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error { return utils.SuccessResponse(c, nil, "Logged out successfully") } + */ \ No newline at end of file diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go index f0bb07f..b399aa1 100644 --- a/internal/handler/role_handler.go +++ b/internal/handler/role_handler.go @@ -22,7 +22,7 @@ func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") // } - roles, err := h.RoleService.GetRoles() + roles, err := h.RoleService.GetRoles(c.Context()) if err != nil { return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) } @@ -38,7 +38,7 @@ func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") // } - role, err := h.RoleService.GetRoleByID(roleID) + role, err := h.RoleService.GetRoleByID(c.Context(), roleID) if err != nil { return utils.GenericResponse(c, fiber.StatusNotFound, "role id tidak ditemukan") } diff --git a/internal/identitycart/identitycart_dto.go b/internal/identitycart/identitycart_dto.go new file mode 100644 index 0000000..78dc475 --- /dev/null +++ b/internal/identitycart/identitycart_dto.go @@ -0,0 +1,148 @@ +package identitycart + +import ( + "rijig/utils" + "strings" +) + +type ResponseIdentityCardDTO struct { + ID string `json:"id"` + UserID string `json:"userId"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + Province string `json:"province"` + District string `json:"district"` + SubDistrict string `json:"subdistrict"` + Hamlet string `json:"hamlet"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + PostalCode string `json:"postalcode"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` + Cardphoto string `json:"cardphoto"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestIdentityCardDTO struct { + DeviceID string `json:"device_id"` + UserID string `json:"userId"` + Identificationumber string `json:"identificationumber"` + Placeofbirth string `json:"placeofbirth"` + Dateofbirth string `json:"dateofbirth"` + Gender string `json:"gender"` + BloodType string `json:"bloodtype"` + Province string `json:"province"` + District string `json:"district"` + SubDistrict string `json:"subdistrict"` + Hamlet string `json:"hamlet"` + Village string `json:"village"` + Neighbourhood string `json:"neighbourhood"` + PostalCode string `json:"postalcode"` + Religion string `json:"religion"` + Maritalstatus string `json:"maritalstatus"` + Job string `json:"job"` + Citizenship string `json:"citizenship"` + Validuntil string `json:"validuntil"` + Cardphoto string `json:"cardphoto"` +} + +func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + r.Placeofbirth = strings.ToLower(r.Placeofbirth) + r.Dateofbirth = strings.ToLower(r.Dateofbirth) + r.Gender = strings.ToLower(r.Gender) + r.BloodType = strings.ToUpper(r.BloodType) + r.Province = strings.ToLower(r.Province) + r.District = strings.ToLower(r.District) + r.SubDistrict = strings.ToLower(r.SubDistrict) + r.Hamlet = strings.ToLower(r.Hamlet) + r.Village = strings.ToLower(r.Village) + r.Neighbourhood = strings.ToLower(r.Neighbourhood) + r.PostalCode = strings.ToLower(r.PostalCode) + r.Religion = strings.ToLower(r.Religion) + r.Maritalstatus = strings.ToLower(r.Maritalstatus) + r.Job = strings.ToLower(r.Job) + r.Citizenship = strings.ToLower(r.Citizenship) + r.Validuntil = strings.ToLower(r.Validuntil) + + nikData := utils.FetchNIKData(r.Identificationumber) + if strings.ToLower(nikData.Status) != "sukses" { + errors["identificationumber"] = append(errors["identificationumber"], "NIK yang anda masukkan tidak valid") + } else { + + if r.Dateofbirth != strings.ToLower(nikData.Ttl) { + errors["dateofbirth"] = append(errors["dateofbirth"], "Tanggal lahir tidak sesuai dengan NIK") + } + + if r.Gender != strings.ToLower(nikData.Sex) { + errors["gender"] = append(errors["gender"], "Jenis kelamin tidak sesuai dengan NIK") + } + + if r.Province != strings.ToLower(nikData.Provinsi) { + errors["province"] = append(errors["province"], "Provinsi tidak sesuai dengan NIK") + } + + if r.District != strings.ToLower(nikData.Kabkot) { + errors["district"] = append(errors["district"], "Kabupaten/Kota tidak sesuai dengan NIK") + } + + if r.SubDistrict != strings.ToLower(nikData.Kecamatan) { + errors["subdistrict"] = append(errors["subdistrict"], "Kecamatan tidak sesuai dengan NIK") + } + + if r.PostalCode != strings.ToLower(nikData.KodPos) { + errors["postalcode"] = append(errors["postalcode"], "Kode pos tidak sesuai dengan NIK") + } + } + + if r.Placeofbirth == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Tempat lahir wajib diisi") + } + if r.Hamlet == "" { + errors["hamlet"] = append(errors["hamlet"], "Dusun/RW wajib diisi") + } + if r.Village == "" { + errors["village"] = append(errors["village"], "Desa/Kelurahan wajib diisi") + } + if r.Neighbourhood == "" { + errors["neighbourhood"] = append(errors["neighbourhood"], "RT wajib diisi") + } + if r.Job == "" { + errors["job"] = append(errors["job"], "Pekerjaan wajib diisi") + } + if r.Citizenship == "" { + errors["citizenship"] = append(errors["citizenship"], "Kewarganegaraan wajib diisi") + } + if r.Validuntil == "" { + errors["validuntil"] = append(errors["validuntil"], "Berlaku hingga wajib diisi") + } + + validBloodTypes := map[string]bool{"A": true, "B": true, "O": true, "AB": true} + if _, ok := validBloodTypes[r.BloodType]; !ok { + errors["bloodtype"] = append(errors["bloodtype"], "Golongan darah harus A, B, O, atau AB") + } + + validReligions := map[string]bool{ + "islam": true, "kristen": true, "katolik": true, "hindu": true, "buddha": true, "konghucu": true, + } + if _, ok := validReligions[r.Religion]; !ok { + errors["religion"] = append(errors["religion"], "Agama harus salah satu dari Islam, Kristen, Katolik, Hindu, Buddha, atau Konghucu") + } + + if r.Maritalstatus != "kawin" && r.Maritalstatus != "belum kawin" { + errors["maritalstatus"] = append(errors["maritalstatus"], "Status perkawinan harus 'kawin' atau 'belum kawin'") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/internal/identitycart/identitycart_handler.go b/internal/identitycart/identitycart_handler.go new file mode 100644 index 0000000..049b9b1 --- /dev/null +++ b/internal/identitycart/identitycart_handler.go @@ -0,0 +1,73 @@ +package identitycart + +import ( + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type IdentityCardHandler struct { + service IdentityCardService +} + +func NewIdentityCardHandler(service IdentityCardService) *IdentityCardHandler { + return &IdentityCardHandler{service: service} +} + +func (h *IdentityCardHandler) CreateIdentityCardHandler(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + cardPhoto, err := c.FormFile("cardphoto") + if err != nil { + return utils.BadRequest(c, "KTP photo is required") + } + + var input RequestIdentityCardDTO + if err := c.BodyParser(&input); err != nil { + return utils.BadRequest(c, "Invalid input format") + } + + + if errs, valid := input.ValidateIdentityCardInput(); !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs) + } + + response, err := h.service.CreateIdentityCard(c.Context(), claims.UserID, &input, cardPhoto) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "KTP successfully submitted", response) +} + +func (h *IdentityCardHandler) GetIdentityByID(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "id is required") + } + + result, err := h.service.GetIdentityCardByID(c.Context(), id) + if err != nil { + return utils.NotFound(c, "data not found") + } + + return utils.SuccessWithData(c, "success retrieve identity card", result) +} + +func (h *IdentityCardHandler) GetIdentityByUserId(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + result, err := h.service.GetIdentityCardsByUserID(c.Context(), claims.UserID) + if err != nil { + return utils.InternalServerError(c, "failed to fetch your identity card data") + } + + return utils.SuccessWithData(c, "success retrieve your identity card", result) +} diff --git a/internal/identitycart/identitycart_repo.go b/internal/identitycart/identitycart_repo.go new file mode 100644 index 0000000..e59ee5e --- /dev/null +++ b/internal/identitycart/identitycart_repo.go @@ -0,0 +1,64 @@ +package identitycart + +import ( + "context" + "errors" + "fmt" + "log" + "rijig/model" + + "gorm.io/gorm" +) + +type IdentityCardRepository interface { + CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error) + GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error) + GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error) + UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error +} + +type identityCardRepository struct { + db *gorm.DB +} + +func NewIdentityCardRepository(db *gorm.DB) IdentityCardRepository { + return &identityCardRepository{ + db: db, + } +} + +func (r *identityCardRepository) CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error) { + if err := r.db.WithContext(ctx).Create(identityCard).Error; err != nil { + log.Printf("Error creating identity card: %v", err) + return nil, fmt.Errorf("failed to create identity card: %w", err) + } + return identityCard, nil +} + +func (r *identityCardRepository) GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error) { + var identityCard model.IdentityCard + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&identityCard).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("identity card not found with id %s", id) + } + log.Printf("Error fetching identity card by ID: %v", err) + return nil, fmt.Errorf("error fetching identity card by ID: %w", err) + } + return &identityCard, nil +} + +func (r *identityCardRepository) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error) { + var identityCards []model.IdentityCard + if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&identityCards).Error; err != nil { + log.Printf("Error fetching identity cards by userID: %v", err) + return nil, fmt.Errorf("error fetching identity cards by userID: %w", err) + } + return identityCards, nil +} + +func (r *identityCardRepository) UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error { + return r.db.WithContext(ctx). + Model(&model.IdentityCard{}). + Where("user_id = ?", identity.UserID). + Updates(identity).Error +} diff --git a/internal/identitycart/identitycart_route.go b/internal/identitycart/identitycart_route.go new file mode 100644 index 0000000..f9f0673 --- /dev/null +++ b/internal/identitycart/identitycart_route.go @@ -0,0 +1,35 @@ +package identitycart + +import ( + "rijig/config" + "rijig/internal/authentication" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func UserIdentityCardRoute(api fiber.Router) { + identityRepo := NewIdentityCardRepository(config.DB) + authRepo := authentication.NewAuthenticationRepository(config.DB) + identityService := NewIdentityCardService(identityRepo, authRepo) + identityHandler := NewIdentityCardHandler(identityService) + + identity := api.Group("/identity") + + identity.Post("/create", + middleware.AuthMiddleware(), + middleware.RequireRoles("pengelola", "pengepul"), + identityHandler.CreateIdentityCardHandler, + ) + identity.Get("/:id", + middleware.AuthMiddleware(), + middleware.RequireRoles("pengelola", "pengepul"), + identityHandler.GetIdentityByID, + ) + identity.Get("/", + middleware.AuthMiddleware(), + middleware.RequireRoles("pengelola", "pengepul"), + identityHandler.GetIdentityByUserId, + ) + +} diff --git a/internal/identitycart/identitycart_service.go b/internal/identitycart/identitycart_service.go new file mode 100644 index 0000000..e72e219 --- /dev/null +++ b/internal/identitycart/identitycart_service.go @@ -0,0 +1,321 @@ +package identitycart + +import ( + "context" + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "rijig/internal/authentication" + "rijig/model" + "rijig/utils" + "strings" +) + +type IdentityCardService interface { + CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) + GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error) + GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error) + UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error) +} + +type identityCardService struct { + identityRepo IdentityCardRepository + authRepo authentication.AuthenticationRepository +} + +func NewIdentityCardService(identityRepo IdentityCardRepository, authRepo authentication.AuthenticationRepository) IdentityCardService { + return &identityCardService{ + identityRepo: identityRepo, + authRepo: authRepo, + } +} + +func FormatResponseIdentityCard(identityCard *model.IdentityCard) (*ResponseIdentityCardDTO, error) { + createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt) + + return &ResponseIdentityCardDTO{ + ID: identityCard.ID, + UserID: identityCard.UserID, + Identificationumber: identityCard.Identificationumber, + Placeofbirth: identityCard.Placeofbirth, + Dateofbirth: identityCard.Dateofbirth, + Gender: identityCard.Gender, + BloodType: identityCard.BloodType, + Province: identityCard.Province, + District: identityCard.District, + SubDistrict: identityCard.SubDistrict, + Hamlet: identityCard.Hamlet, + Village: identityCard.Village, + Neighbourhood: identityCard.Neighbourhood, + PostalCode: identityCard.PostalCode, + Religion: identityCard.Religion, + Maritalstatus: identityCard.Maritalstatus, + Job: identityCard.Job, + Citizenship: identityCard.Citizenship, + Validuntil: identityCard.Validuntil, + Cardphoto: identityCard.Cardphoto, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, nil +} + +func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *multipart.FileHeader) (string, error) { + pathImage := "/uploads/identitycards/" + cardPhotoDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(cardPhotoDir); os.IsNotExist(err) { + + if err := os.MkdirAll(cardPhotoDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for identity card photo: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(cardPhoto.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + cardPhotoFileName := fmt.Sprintf("%s_cardphoto%s", userID, extension) + cardPhotoPath := filepath.Join(cardPhotoDir, cardPhotoFileName) + + src, err := cardPhoto.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(cardPhotoPath) + if err != nil { + return "", fmt.Errorf("failed to create card photo file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save card photo: %v", err) + } + + cardPhotoURL := fmt.Sprintf("%s%s", pathImage, cardPhotoFileName) + + return cardPhotoURL, nil +} + +func deleteIdentityCardImage(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) { + + // Validate essential parameters + if userID == "" { + return nil, fmt.Errorf("userID cannot be empty") + } + + if request.DeviceID == "" { + return nil, fmt.Errorf("deviceID cannot be empty") + } + + cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + + identityCard := &model.IdentityCard{ + UserID: userID, + Identificationumber: request.Identificationumber, + Placeofbirth: request.Placeofbirth, + Dateofbirth: request.Dateofbirth, + Gender: request.Gender, + BloodType: request.BloodType, + Province: request.Province, + District: request.District, + SubDistrict: request.SubDistrict, + Hamlet: request.Hamlet, + Village: request.Village, + Neighbourhood: request.Neighbourhood, + PostalCode: request.PostalCode, + Religion: request.Religion, + Maritalstatus: request.Maritalstatus, + Job: request.Job, + Citizenship: request.Citizenship, + Validuntil: request.Validuntil, + Cardphoto: cardPhotoPath, + } + + _, err = s.identityRepo.CreateIdentityCard(ctx, identityCard) + if err != nil { + log.Printf("Error creating identity card: %v", err) + return nil, fmt.Errorf("failed to create identity card: %v", err) + } + + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to find user: %v", err) + } + + // Validate user data + if user.Role.RoleName == "" { + return nil, fmt.Errorf("user role not found") + } + + roleName := strings.ToLower(user.Role.RoleName) + + // Determine new registration status and progress + var newRegistrationStatus string + var newRegistrationProgress int + + switch roleName { + case "pengepul": + newRegistrationProgress = 2 + newRegistrationStatus = utils.RegStatusPending + case "pengelola": + newRegistrationProgress = 2 + newRegistrationStatus = user.RegistrationStatus + default: + newRegistrationProgress = int(user.RegistrationProgress) + newRegistrationStatus = user.RegistrationStatus + } + + // Update user registration progress and status + updates := map[string]interface{}{ + "registration_progress": newRegistrationProgress, + "registration_status": newRegistrationStatus, + } + + err = s.authRepo.PatchUser(ctx, userID, updates) + if err != nil { + return nil, fmt.Errorf("failed to update user: %v", err) + } + + // Debug logging before token generation + log.Printf("Token Generation Parameters:") + log.Printf("- UserID: '%s'", user.ID) + log.Printf("- Role: '%s'", user.Role.RoleName) + log.Printf("- DeviceID: '%s'", request.DeviceID) + log.Printf("- Registration Status: '%s'", newRegistrationStatus) + + // Generate token pair with updated status + tokenResponse, err := utils.GenerateTokenPair( + user.ID, + user.Role.RoleName, + request.DeviceID, + newRegistrationStatus, + newRegistrationProgress, + ) + if err != nil { + log.Printf("GenerateTokenPair error: %v", err) + return nil, fmt.Errorf("failed to generate token: %v", err) + } + + return &authentication.AuthResponse{ + Message: "identity card berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: newRegistrationStatus, + NextStep: tokenResponse.NextStep, + SessionID: tokenResponse.SessionID, + }, nil +} + +func (s *identityCardService) GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error) { + identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id) + if err != nil { + log.Printf("Error fetching identity card: %v", err) + return nil, fmt.Errorf("failed to fetch identity card") + } + return FormatResponseIdentityCard(identityCard) +} + +func (s *identityCardService) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error) { + identityCards, err := s.identityRepo.GetIdentityCardsByUserID(ctx, userID) + if err != nil { + log.Printf("Error fetching identity cards by userID: %v", err) + return nil, fmt.Errorf("failed to fetch identity cards by userID") + } + + var response []ResponseIdentityCardDTO + for _, card := range identityCards { + dto, _ := FormatResponseIdentityCard(&card) + response = append(response, *dto) + } + return response, nil +} + +func (s *identityCardService) UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error) { + + errors, valid := request.ValidateIdentityCardInput() + if !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("identity card not found: %v", err) + } + + if identityCard.Cardphoto != "" { + err := deleteIdentityCardImage(identityCard.Cardphoto) + if err != nil { + return nil, fmt.Errorf("failed to delete old image: %v", err) + } + } + + var cardPhotoPath string + if cardPhoto != nil { + cardPhotoPath, err = s.saveIdentityCardImage(userID, cardPhoto) + if err != nil { + return nil, fmt.Errorf("failed to save card photo: %v", err) + } + } + + identityCard.Identificationumber = request.Identificationumber + identityCard.Placeofbirth = request.Placeofbirth + identityCard.Dateofbirth = request.Dateofbirth + identityCard.Gender = request.Gender + identityCard.BloodType = request.BloodType + identityCard.Province = request.Province + identityCard.District = request.District + identityCard.SubDistrict = request.SubDistrict + identityCard.Hamlet = request.Hamlet + identityCard.Village = request.Village + identityCard.Neighbourhood = request.Neighbourhood + identityCard.PostalCode = request.PostalCode + identityCard.Religion = request.Religion + identityCard.Maritalstatus = request.Maritalstatus + identityCard.Job = request.Job + identityCard.Citizenship = request.Citizenship + identityCard.Validuntil = request.Validuntil + if cardPhotoPath != "" { + identityCard.Cardphoto = cardPhotoPath + } + + if err != nil { + log.Printf("Error updating identity card: %v", err) + return nil, fmt.Errorf("failed to update identity card: %v", err) + } + + idcardResponseDTO, _ := FormatResponseIdentityCard(identityCard) + + return idcardResponseDTO, nil +} diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go index 27698f3..e048df8 100644 --- a/internal/repositories/role_repo.go +++ b/internal/repositories/role_repo.go @@ -1,48 +1,49 @@ package repositories import ( + "context" "rijig/model" "gorm.io/gorm" ) type RoleRepository interface { - FindByID(id string) (*model.Role, error) - FindRoleByName(roleName string) (*model.Role, error) - FindAll() ([]model.Role, error) + FindByID(ctx context.Context, id string) (*model.Role, error) + FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) + FindAll(ctx context.Context) ([]model.Role, error) } type roleRepository struct { - DB *gorm.DB + db *gorm.DB } func NewRoleRepository(db *gorm.DB) RoleRepository { - return &roleRepository{DB: db} + return &roleRepository{db} } -func (r *roleRepository) FindByID(id string) (*model.Role, error) { +func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) { var role model.Role - err := r.DB.Where("id = ?", id).First(&role).Error + err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error if err != nil { return nil, err } return &role, nil } -func (r *roleRepository) FindAll() ([]model.Role, error) { +func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) { + var role model.Role + err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} + +func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) { var roles []model.Role - err := r.DB.Find(&roles).Error + err := r.db.WithContext(ctx).Find(&roles).Error if err != nil { return nil, err } return roles, nil } - -func (r *roleRepository) FindRoleByName(roleName string) (*model.Role, error) { - var role model.Role - err := r.DB.Where("role_name = ?", roleName).First(&role).Error - if err != nil { - return nil, err - } - return &role, nil -} diff --git a/internal/repositories/trash_repo.go b/internal/repositories/trash_repo.go index 53815f9..19bee10 100644 --- a/internal/repositories/trash_repo.go +++ b/internal/repositories/trash_repo.go @@ -69,7 +69,6 @@ func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, erro return &category, nil } -// spesial code func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { var trash model.TrashCategory if err := config.DB.WithContext(ctx).First(&trash, "id = ?", id).Error; err != nil { diff --git a/internal/requestpickup/requestpickup_dto.go b/internal/requestpickup/requestpickup_dto.go new file mode 100644 index 0000000..df31b39 --- /dev/null +++ b/internal/requestpickup/requestpickup_dto.go @@ -0,0 +1 @@ +package requestpickup \ No newline at end of file diff --git a/internal/requestpickup/requestpickup_handler.go b/internal/requestpickup/requestpickup_handler.go new file mode 100644 index 0000000..df31b39 --- /dev/null +++ b/internal/requestpickup/requestpickup_handler.go @@ -0,0 +1 @@ +package requestpickup \ No newline at end of file diff --git a/internal/requestpickup/requestpickup_repository.go b/internal/requestpickup/requestpickup_repository.go new file mode 100644 index 0000000..df31b39 --- /dev/null +++ b/internal/requestpickup/requestpickup_repository.go @@ -0,0 +1 @@ +package requestpickup \ No newline at end of file diff --git a/internal/requestpickup/requestpickup_route.go b/internal/requestpickup/requestpickup_route.go new file mode 100644 index 0000000..df31b39 --- /dev/null +++ b/internal/requestpickup/requestpickup_route.go @@ -0,0 +1 @@ +package requestpickup \ No newline at end of file diff --git a/internal/requestpickup/requestpickup_service.go b/internal/requestpickup/requestpickup_service.go new file mode 100644 index 0000000..df31b39 --- /dev/null +++ b/internal/requestpickup/requestpickup_service.go @@ -0,0 +1 @@ +package requestpickup \ No newline at end of file diff --git a/internal/role/role_dto.go b/internal/role/role_dto.go new file mode 100644 index 0000000..66665e0 --- /dev/null +++ b/internal/role/role_dto.go @@ -0,0 +1,8 @@ +package role + +type RoleResponseDTO struct { + ID string `json:"role_id"` + RoleName string `json:"role_name"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/internal/role/role_handler.go b/internal/role/role_handler.go new file mode 100644 index 0000000..bdb7b24 --- /dev/null +++ b/internal/role/role_handler.go @@ -0,0 +1,52 @@ +package role + +import ( + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type RoleHandler struct { + roleService RoleService +} + +func NewRoleHandler(roleService RoleService) *RoleHandler { + return &RoleHandler{ + roleService: roleService, + } +} + +func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { + + if _, err := middleware.GetUserFromContext(c); err != nil { + return utils.Unauthorized(c, "Unauthorized access") + } + + roles, err := h.roleService.GetRoles(c.Context()) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch roles") + } + + return utils.SuccessWithData(c, "Roles fetched successfully", roles) +} + +func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { + + if _, err := middleware.GetUserFromContext(c); err != nil { + return utils.Unauthorized(c, "Unauthorized access") + } + + roleID := c.Params("role_id") + if roleID == "" { + return utils.BadRequest(c, "Role ID is required") + } + + role, err := h.roleService.GetRoleByID(c.Context(), roleID) + if err != nil { + + return utils.NotFound(c, "Role not found") + } + + return utils.SuccessWithData(c, "Role fetched successfully", role) +} diff --git a/internal/role/role_repo.go b/internal/role/role_repo.go new file mode 100644 index 0000000..039e269 --- /dev/null +++ b/internal/role/role_repo.go @@ -0,0 +1,49 @@ +package role + +import ( + "context" + "rijig/model" + + "gorm.io/gorm" +) + +type RoleRepository interface { + FindByID(ctx context.Context, id string) (*model.Role, error) + FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) + FindAll(ctx context.Context) ([]model.Role, error) +} + +type roleRepository struct { + db *gorm.DB +} + +func NewRoleRepository(db *gorm.DB) RoleRepository { + return &roleRepository{db} +} + +func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) { + var role model.Role + err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} + +func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) { + var role model.Role + err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} + +func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) { + var roles []model.Role + err := r.db.WithContext(ctx).Find(&roles).Error + if err != nil { + return nil, err + } + return roles, nil +} diff --git a/internal/role/role_route.go b/internal/role/role_route.go new file mode 100644 index 0000000..5636785 --- /dev/null +++ b/internal/role/role_route.go @@ -0,0 +1,18 @@ +package role + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func UserRoleRouter(api fiber.Router) { + roleRepo := NewRoleRepository(config.DB) + roleService := NewRoleService(roleRepo) + roleHandler := NewRoleHandler(roleService) + roleRoute := api.Group("/role", middleware.AuthMiddleware()) + + roleRoute.Get("/", roleHandler.GetRoles) + roleRoute.Get("/:role_id", roleHandler.GetRoleByID) +} diff --git a/internal/role/role_service.go b/internal/role/role_service.go new file mode 100644 index 0000000..3a17e0c --- /dev/null +++ b/internal/role/role_service.go @@ -0,0 +1,89 @@ +package role + +import ( + "context" + "fmt" + "time" + + "rijig/utils" +) + +type RoleService interface { + GetRoles(ctx context.Context) ([]RoleResponseDTO, error) + GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error) +} + +type roleService struct { + RoleRepo RoleRepository +} + +func NewRoleService(roleRepo RoleRepository) RoleService { + return &roleService{roleRepo} +} + +func (s *roleService) GetRoles(ctx context.Context) ([]RoleResponseDTO, error) { + cacheKey := "roles_list" + + var cachedRoles []RoleResponseDTO + err := utils.GetCache(cacheKey, &cachedRoles) + if err == nil { + return cachedRoles, nil + } + + roles, err := s.RoleRepo.FindAll(ctx) + if err != nil { + return nil, fmt.Errorf("failed to fetch roles: %v", err) + } + + var roleDTOs []RoleResponseDTO + for _, role := range roles { + createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt) + + roleDTOs = append(roleDTOs, RoleResponseDTO{ + ID: role.ID, + RoleName: role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + err = utils.SetCache(cacheKey, roleDTOs, time.Hour*24) + if err != nil { + fmt.Printf("Error caching roles data to Redis: %v\n", err) + } + + return roleDTOs, nil +} + +func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error) { + cacheKey := fmt.Sprintf("role:%s", roleID) + + var cachedRole RoleResponseDTO + err := utils.GetCache(cacheKey, &cachedRole) + if err == nil { + return &cachedRole, nil + } + + role, err := s.RoleRepo.FindByID(ctx, roleID) + if err != nil { + return nil, fmt.Errorf("role not found: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt) + + roleDTO := &RoleResponseDTO{ + ID: role.ID, + RoleName: role.RoleName, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + err = utils.SetCache(cacheKey, roleDTO, time.Hour*24) + if err != nil { + fmt.Printf("Error caching role data to Redis: %v\n", err) + } + + return roleDTO, nil +} diff --git a/internal/services/auth/auth_admin_service.go b/internal/services/auth/auth_admin_service.go index 89ac8aa..7adb5af 100644 --- a/internal/services/auth/auth_admin_service.go +++ b/internal/services/auth/auth_admin_service.go @@ -1,5 +1,5 @@ package service - +/* import ( "errors" "fmt" @@ -189,3 +189,4 @@ func (s *authAdminService) LogoutAdmin(userID, deviceID string) error { return nil } + */ \ No newline at end of file diff --git a/internal/services/auth/auth_masyarakat_service.go b/internal/services/auth/auth_masyarakat_service.go index b20e930..11070da 100644 --- a/internal/services/auth/auth_masyarakat_service.go +++ b/internal/services/auth/auth_masyarakat_service.go @@ -1,5 +1,5 @@ package service - +/* import ( "errors" "fmt" @@ -168,3 +168,4 @@ func (s *authMasyarakatService) Logout(userID, deviceID string) error { return nil } + */ \ No newline at end of file diff --git a/internal/services/auth/auth_pengelola_service.go b/internal/services/auth/auth_pengelola_service.go index 2cc1ad9..0c2b33e 100644 --- a/internal/services/auth/auth_pengelola_service.go +++ b/internal/services/auth/auth_pengelola_service.go @@ -1,5 +1,5 @@ package service - +/* import ( "errors" "fmt" @@ -168,3 +168,4 @@ func (s *authPengelolaService) Logout(userID, deviceID string) error { return nil } + */ \ No newline at end of file diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go index c4e6b73..fd91d95 100644 --- a/internal/services/auth/auth_pengepul_service.go +++ b/internal/services/auth/auth_pengepul_service.go @@ -1,5 +1,5 @@ package service - +/* import ( "errors" "fmt" @@ -169,3 +169,4 @@ func (s *authPengepulService) Logout(userID, deviceID string) error { return nil } + */ \ No newline at end of file diff --git a/internal/services/role_service.go b/internal/services/role_service.go index d40d490..5d938bc 100644 --- a/internal/services/role_service.go +++ b/internal/services/role_service.go @@ -1,6 +1,7 @@ package services import ( + "context" "fmt" "time" @@ -10,8 +11,8 @@ import ( ) type RoleService interface { - GetRoles() ([]dto.RoleResponseDTO, error) - GetRoleByID(roleID string) (*dto.RoleResponseDTO, error) + GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) + GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error) } type roleService struct { @@ -22,8 +23,7 @@ func NewRoleService(roleRepo repositories.RoleRepository) RoleService { return &roleService{RoleRepo: roleRepo} } -func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) { - +func (s *roleService) GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) { cacheKey := "roles_list" cachedData, err := utils.GetJSONData(cacheKey) if err == nil && cachedData != nil { @@ -44,7 +44,7 @@ func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) { } } - roles, err := s.RoleRepo.FindAll() + roles, err := s.RoleRepo.FindAll(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch roles: %v", err) } @@ -73,9 +73,9 @@ func (s *roleService) GetRoles() ([]dto.RoleResponseDTO, error) { return roleDTOs, nil } -func (s *roleService) GetRoleByID(roleID string) (*dto.RoleResponseDTO, error) { +func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error) { - role, err := s.RoleRepo.FindByID(roleID) + role, err := s.RoleRepo.FindByID(ctx, roleID) if err != nil { return nil, fmt.Errorf("role not found: %v", err) } diff --git a/internal/trash/trash_dto.go b/internal/trash/trash_dto.go new file mode 100644 index 0000000..a900a5d --- /dev/null +++ b/internal/trash/trash_dto.go @@ -0,0 +1,75 @@ +package trash + +import ( + "strings" +) + +type RequestTrashCategoryDTO struct { + Name string `json:"name"` + EstimatedPrice float64 `json:"estimated_price"` + IconTrash string `json:"icon_trash,omitempty"` + Variety string `json:"variety"` +} + +type RequestTrashDetailDTO struct { + CategoryID string `json:"category_id"` + StepOrder int `json:"step"` + IconTrashDetail string `json:"icon_trash_detail,omitempty"` + Description string `json:"description"` +} + +type ResponseTrashCategoryDTO struct { + ID string `json:"id,omitempty"` + TrashName string `json:"trash_name,omitempty"` + TrashIcon string `json:"trash_icon,omitempty"` + EstimatedPrice float64 `json:"estimated_price"` + Variety string `json:"variety,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` + TrashDetail []ResponseTrashDetailDTO `json:"trash_detail,omitempty"` +} + +type ResponseTrashDetailDTO struct { + ID string `json:"trashdetail_id"` + CategoryID string `json:"category_id"` + IconTrashDetail string `json:"trashdetail_icon,omitempty"` + Description string `json:"description"` + StepOrder int `json:"step_order"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +func (r *RequestTrashCategoryDTO) ValidateRequestTrashCategoryDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "name is required") + } + if r.EstimatedPrice <= 0 { + errors["estimated_price"] = append(errors["estimated_price"], "estimated price must be greater than 0") + } + if strings.TrimSpace(r.Variety) == "" { + errors["variety"] = append(errors["variety"], "variety is required") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +func (r *RequestTrashDetailDTO) ValidateRequestTrashDetailDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Description) == "" { + errors["description"] = append(errors["description"], "description is required") + } + if strings.TrimSpace(r.CategoryID) == "" { + errors["category_id"] = append(errors["category_id"], "category_id is required") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/internal/trash/trash_handler.go b/internal/trash/trash_handler.go new file mode 100644 index 0000000..7d6553c --- /dev/null +++ b/internal/trash/trash_handler.go @@ -0,0 +1,521 @@ +package trash + +import ( + "rijig/utils" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +type TrashHandler struct { + trashService TrashServiceInterface +} + +func NewTrashHandler(trashService TrashServiceInterface) *TrashHandler { + return &TrashHandler{ + trashService: trashService, + } +} + +func (h *TrashHandler) CreateTrashCategory(c *fiber.Ctx) error { + var req RequestTrashCategoryDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.CreateTrashCategory(c.Context(), req) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + return utils.InternalServerError(c, "Failed to create trash category") + } + + return utils.CreateSuccessWithData(c, "Trash category created successfully", response) +} + +func (h *TrashHandler) CreateTrashCategoryWithIcon(c *fiber.Ctx) error { + var req RequestTrashCategoryDTO + + req.Name = c.FormValue("name") + req.Variety = c.FormValue("variety") + + if estimatedPriceStr := c.FormValue("estimated_price"); estimatedPriceStr != "" { + if price, err := strconv.ParseFloat(estimatedPriceStr, 64); err == nil { + req.EstimatedPrice = price + } + } + + iconFile, err := c.FormFile("icon") + if err != nil && err.Error() != "there is no uploaded file associated with the given key" { + return utils.BadRequest(c, "Invalid icon file") + } + + response, err := h.trashService.CreateTrashCategoryWithIcon(c.Context(), req, iconFile) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to create trash category") + } + + return utils.CreateSuccessWithData(c, "Trash category created successfully", response) +} + +func (h *TrashHandler) CreateTrashCategoryWithDetails(c *fiber.Ctx) error { + var req struct { + Category RequestTrashCategoryDTO `json:"category"` + Details []RequestTrashDetailDTO `json:"details"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.CreateTrashCategoryWithDetails(c.Context(), req.Category, req.Details) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + return utils.InternalServerError(c, "Failed to create trash category with details") + } + + return utils.CreateSuccessWithData(c, "Trash category with details created successfully", response) +} + +func (h *TrashHandler) UpdateTrashCategory(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req RequestTrashCategoryDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.UpdateTrashCategory(c.Context(), id, req) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to update trash category") + } + + return utils.SuccessWithData(c, "Trash category updated successfully", response) +} + +func (h *TrashHandler) UpdateTrashCategoryWithIcon(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req RequestTrashCategoryDTO + + req.Name = c.FormValue("name") + req.Variety = c.FormValue("variety") + + if estimatedPriceStr := c.FormValue("estimated_price"); estimatedPriceStr != "" { + if price, err := strconv.ParseFloat(estimatedPriceStr, 64); err == nil { + req.EstimatedPrice = price + } + } + + iconFile, _ := c.FormFile("icon") + + response, err := h.trashService.UpdateTrashCategoryWithIcon(c.Context(), id, req, iconFile) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to update trash category") + } + + return utils.SuccessWithData(c, "Trash category updated successfully", response) +} + +func (h *TrashHandler) GetAllTrashCategories(c *fiber.Ctx) error { + withDetails := c.Query("with_details", "false") + + if withDetails == "true" { + response, err := h.trashService.GetAllTrashCategoriesWithDetails(c.Context()) + if err != nil { + return utils.InternalServerError(c, "Failed to get trash categories") + } + return utils.SuccessWithData(c, "Trash categories retrieved successfully", response) + } + + response, err := h.trashService.GetAllTrashCategories(c.Context()) + if err != nil { + return utils.InternalServerError(c, "Failed to get trash categories") + } + + return utils.SuccessWithData(c, "Trash categories retrieved successfully", response) +} + +func (h *TrashHandler) GetTrashCategoryByID(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Category ID is required") + } + + withDetails := c.Query("with_details", "false") + + if withDetails == "true" { + response, err := h.trashService.GetTrashCategoryByIDWithDetails(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to get trash category") + } + return utils.SuccessWithData(c, "Trash category retrieved successfully", response) + } + + response, err := h.trashService.GetTrashCategoryByID(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to get trash category") + } + + return utils.SuccessWithData(c, "Trash category retrieved successfully", response) +} + +func (h *TrashHandler) DeleteTrashCategory(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Category ID is required") + } + + err := h.trashService.DeleteTrashCategory(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to delete trash category") + } + + return utils.Success(c, "Trash category deleted successfully") +} + +func (h *TrashHandler) CreateTrashDetail(c *fiber.Ctx) error { + var req RequestTrashDetailDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.CreateTrashDetail(c.Context(), req) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + return utils.InternalServerError(c, "Failed to create trash detail") + } + + return utils.CreateSuccessWithData(c, "Trash detail created successfully", response) +} + +func (h *TrashHandler) CreateTrashDetailWithIcon(c *fiber.Ctx) error { + var req RequestTrashDetailDTO + + req.CategoryID = c.FormValue("category_id") + req.Description = c.FormValue("description") + + if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" { + if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil { + req.StepOrder = stepOrder + } + } + + iconFile, err := c.FormFile("icon") + if err != nil && err.Error() != "there is no uploaded file associated with the given key" { + return utils.BadRequest(c, "Invalid icon file") + } + + response, err := h.trashService.CreateTrashDetailWithIcon(c.Context(), req, iconFile) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to create trash detail") + } + + return utils.CreateSuccessWithData(c, "Trash detail created successfully", response) +} + +func (h *TrashHandler) AddTrashDetailToCategory(c *fiber.Ctx) error { + categoryID := c.Params("categoryId") + if categoryID == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req RequestTrashDetailDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.AddTrashDetailToCategory(c.Context(), categoryID, req) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + return utils.InternalServerError(c, "Failed to add trash detail to category") + } + + return utils.CreateSuccessWithData(c, "Trash detail added to category successfully", response) +} + +func (h *TrashHandler) AddTrashDetailToCategoryWithIcon(c *fiber.Ctx) error { + categoryID := c.Params("categoryId") + if categoryID == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req RequestTrashDetailDTO + + req.Description = c.FormValue("description") + + if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" { + if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil { + req.StepOrder = stepOrder + } + } + + iconFile, err := c.FormFile("icon") + if err != nil && err.Error() != "there is no uploaded file associated with the given key" { + return utils.BadRequest(c, "Invalid icon file") + } + + response, err := h.trashService.AddTrashDetailToCategoryWithIcon(c.Context(), categoryID, req, iconFile) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to add trash detail to category") + } + + return utils.CreateSuccessWithData(c, "Trash detail added to category successfully", response) +} + +func (h *TrashHandler) UpdateTrashDetail(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Detail ID is required") + } + + var req RequestTrashDetailDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + response, err := h.trashService.UpdateTrashDetail(c.Context(), id, req) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash detail not found") + } + return utils.InternalServerError(c, "Failed to update trash detail") + } + + return utils.SuccessWithData(c, "Trash detail updated successfully", response) +} + +func (h *TrashHandler) UpdateTrashDetailWithIcon(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Detail ID is required") + } + + var req RequestTrashDetailDTO + + req.Description = c.FormValue("description") + + if stepOrderStr := c.FormValue("step_order"); stepOrderStr != "" { + if stepOrder, err := strconv.Atoi(stepOrderStr); err == nil { + req.StepOrder = stepOrder + } + } + + iconFile, _ := c.FormFile("icon") + + response, err := h.trashService.UpdateTrashDetailWithIcon(c.Context(), id, req, iconFile) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash detail not found") + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to update trash detail") + } + + return utils.SuccessWithData(c, "Trash detail updated successfully", response) +} + +func (h *TrashHandler) GetTrashDetailsByCategory(c *fiber.Ctx) error { + categoryID := c.Params("categoryId") + if categoryID == "" { + return utils.BadRequest(c, "Category ID is required") + } + + response, err := h.trashService.GetTrashDetailsByCategory(c.Context(), categoryID) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to get trash details") + } + + return utils.SuccessWithData(c, "Trash details retrieved successfully", response) +} + +func (h *TrashHandler) GetTrashDetailByID(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Detail ID is required") + } + + response, err := h.trashService.GetTrashDetailByID(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash detail not found") + } + return utils.InternalServerError(c, "Failed to get trash detail") + } + + return utils.SuccessWithData(c, "Trash detail retrieved successfully", response) +} + +func (h *TrashHandler) DeleteTrashDetail(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Detail ID is required") + } + + err := h.trashService.DeleteTrashDetail(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash detail not found") + } + return utils.InternalServerError(c, "Failed to delete trash detail") + } + + return utils.Success(c, "Trash detail deleted successfully") +} + +func (h *TrashHandler) BulkCreateTrashDetails(c *fiber.Ctx) error { + categoryID := c.Params("categoryId") + if categoryID == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req struct { + Details []RequestTrashDetailDTO `json:"details"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + if len(req.Details) == 0 { + return utils.BadRequest(c, "At least one detail is required") + } + + response, err := h.trashService.BulkCreateTrashDetails(c.Context(), categoryID, req.Details) + if err != nil { + if strings.Contains(err.Error(), "validation failed") { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", extractValidationErrors(err.Error())) + } + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to bulk create trash details") + } + + return utils.CreateSuccessWithData(c, "Trash details created successfully", response) +} + +func (h *TrashHandler) BulkDeleteTrashDetails(c *fiber.Ctx) error { + var req struct { + DetailIDs []string `json:"detail_ids"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + if len(req.DetailIDs) == 0 { + return utils.BadRequest(c, "At least one detail ID is required") + } + + err := h.trashService.BulkDeleteTrashDetails(c.Context(), req.DetailIDs) + if err != nil { + return utils.InternalServerError(c, "Failed to bulk delete trash details") + } + + return utils.Success(c, "Trash details deleted successfully") +} + +func (h *TrashHandler) ReorderTrashDetails(c *fiber.Ctx) error { + categoryID := c.Params("categoryId") + if categoryID == "" { + return utils.BadRequest(c, "Category ID is required") + } + + var req struct { + OrderedDetailIDs []string `json:"ordered_detail_ids"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + if len(req.OrderedDetailIDs) == 0 { + return utils.BadRequest(c, "At least one detail ID is required") + } + + err := h.trashService.ReorderTrashDetails(c.Context(), categoryID, req.OrderedDetailIDs) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Trash category not found") + } + return utils.InternalServerError(c, "Failed to reorder trash details") + } + + return utils.Success(c, "Trash details reordered successfully") +} + +func extractValidationErrors(errMsg string) interface{} { + + if strings.Contains(errMsg, "validation failed:") { + return strings.TrimSpace(strings.Split(errMsg, "validation failed:")[1]) + } + return errMsg +} diff --git a/internal/trash/trash_repository.go b/internal/trash/trash_repository.go new file mode 100644 index 0000000..8041af3 --- /dev/null +++ b/internal/trash/trash_repository.go @@ -0,0 +1,326 @@ +package trash + +import ( + "context" + "errors" + "fmt" + "rijig/model" + "time" + + "gorm.io/gorm" +) + +type TrashRepositoryInterface interface { + CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error + CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error + UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error + GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) + GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) + GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) + GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) + DeleteTrashCategory(ctx context.Context, id string) error + + CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error + AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error + UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error + GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) + GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) + DeleteTrashDetail(ctx context.Context, id string) error + + CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) + CheckTrashDetailExists(ctx context.Context, id string) (bool, error) + GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) +} + +type TrashRepository struct { + db *gorm.DB +} + +func NewTrashRepository(db *gorm.DB) TrashRepositoryInterface { + return &TrashRepository{ + db: db, + } +} + +func (r *TrashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error { + if err := r.db.WithContext(ctx).Create(category).Error; err != nil { + return fmt.Errorf("failed to create trash category: %w", err) + } + return nil +} + +func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + + if err := tx.Create(category).Error; err != nil { + return fmt.Errorf("failed to create trash category: %w", err) + } + + if len(details) > 0 { + + for i := range details { + details[i].TrashCategoryID = category.ID + + if details[i].StepOrder == 0 { + details[i].StepOrder = i + 1 + } + } + + if err := tx.Create(&details).Error; err != nil { + return fmt.Errorf("failed to create trash details: %w", err) + } + } + + return nil + }) +} + +func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error { + + exists, err := r.CheckTrashCategoryExists(ctx, id) + if err != nil { + return err + } + if !exists { + return errors.New("trash category not found") + } + + updates["updated_at"] = time.Now() + + result := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Updates(updates) + if result.Error != nil { + return fmt.Errorf("failed to update trash category: %w", result.Error) + } + + if result.RowsAffected == 0 { + return errors.New("no rows affected during update") + } + + return nil +} + +func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) { + var categories []model.TrashCategory + + if err := r.db.WithContext(ctx).Find(&categories).Error; err != nil { + return nil, fmt.Errorf("failed to get trash categories: %w", err) + } + + return categories, nil +} + +func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) { + var categories []model.TrashCategory + + if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB { + return db.Order("step_order ASC") + }).Find(&categories).Error; err != nil { + return nil, fmt.Errorf("failed to get trash categories with details: %w", err) + } + + return categories, nil +} + +func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&category).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("trash category not found") + } + return nil, fmt.Errorf("failed to get trash category: %w", err) + } + + return &category, nil +} + +func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) { + var category model.TrashCategory + + if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB { + return db.Order("step_order ASC") + }).Where("id = ?", id).First(&category).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("trash category not found") + } + return nil, fmt.Errorf("failed to get trash category with details: %w", err) + } + + return &category, nil +} + +func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) error { + + exists, err := r.CheckTrashCategoryExists(ctx, id) + if err != nil { + return err + } + if !exists { + return errors.New("trash category not found") + } + + result := r.db.WithContext(ctx).Delete(&model.TrashCategory{ID: id}) + if result.Error != nil { + return fmt.Errorf("failed to delete trash category: %w", result.Error) + } + + if result.RowsAffected == 0 { + return errors.New("no rows affected during deletion") + } + + return nil +} + +func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error { + + exists, err := r.CheckTrashCategoryExists(ctx, detail.TrashCategoryID) + if err != nil { + return err + } + if !exists { + return errors.New("trash category not found") + } + + if detail.StepOrder == 0 { + maxOrder, err := r.GetMaxStepOrderByCategory(ctx, detail.TrashCategoryID) + if err != nil { + return err + } + detail.StepOrder = maxOrder + 1 + } + + if err := r.db.WithContext(ctx).Create(detail).Error; err != nil { + return fmt.Errorf("failed to create trash detail: %w", err) + } + + return nil +} + +func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error { + + exists, err := r.CheckTrashCategoryExists(ctx, categoryID) + if err != nil { + return err + } + if !exists { + return errors.New("trash category not found") + } + + detail.TrashCategoryID = categoryID + + if detail.StepOrder == 0 { + maxOrder, err := r.GetMaxStepOrderByCategory(ctx, categoryID) + if err != nil { + return err + } + detail.StepOrder = maxOrder + 1 + } + + if err := r.db.WithContext(ctx).Create(detail).Error; err != nil { + return fmt.Errorf("failed to add trash detail to category: %w", err) + } + + return nil +} + +func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error { + + exists, err := r.CheckTrashDetailExists(ctx, id) + if err != nil { + return err + } + if !exists { + return errors.New("trash detail not found") + } + + updates["updated_at"] = time.Now() + + result := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Updates(updates) + if result.Error != nil { + return fmt.Errorf("failed to update trash detail: %w", result.Error) + } + + if result.RowsAffected == 0 { + return errors.New("no rows affected during update") + } + + return nil +} + +func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) { + var details []model.TrashDetail + + if err := r.db.WithContext(ctx).Where("trash_category_id = ?", categoryID).Order("step_order ASC").Find(&details).Error; err != nil { + return nil, fmt.Errorf("failed to get trash details: %w", err) + } + + return details, nil +} + +func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) { + var detail model.TrashDetail + + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&detail).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("trash detail not found") + } + return nil, fmt.Errorf("failed to get trash detail: %w", err) + } + + return &detail, nil +} + +func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) error { + + exists, err := r.CheckTrashDetailExists(ctx, id) + if err != nil { + return err + } + if !exists { + return errors.New("trash detail not found") + } + + result := r.db.WithContext(ctx).Delete(&model.TrashDetail{ID: id}) + if result.Error != nil { + return fmt.Errorf("failed to delete trash detail: %w", result.Error) + } + + if result.RowsAffected == 0 { + return errors.New("no rows affected during deletion") + } + + return nil +} + +func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) { + var count int64 + + if err := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to check trash category existence: %w", err) + } + + return count > 0, nil +} + +func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) { + var count int64 + + if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to check trash detail existence: %w", err) + } + + return count > 0, nil +} + +func (r *TrashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) { + var maxOrder int + + if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}). + Where("trash_category_id = ?", categoryID). + Select("COALESCE(MAX(step_order), 0)"). + Scan(&maxOrder).Error; err != nil { + return 0, fmt.Errorf("failed to get max step order: %w", err) + } + + return maxOrder, nil +} diff --git a/internal/trash/trash_route.go b/internal/trash/trash_route.go new file mode 100644 index 0000000..fb1a11d --- /dev/null +++ b/internal/trash/trash_route.go @@ -0,0 +1 @@ +package trash \ No newline at end of file diff --git a/internal/trash/trash_service.go b/internal/trash/trash_service.go new file mode 100644 index 0000000..16f7b72 --- /dev/null +++ b/internal/trash/trash_service.go @@ -0,0 +1,750 @@ +package trash + +import ( + "context" + "errors" + "fmt" + "log" + "mime/multipart" + "os" + "path/filepath" + "rijig/model" + "strings" + "time" + + "github.com/google/uuid" +) + +type TrashServiceInterface interface { + CreateTrashCategory(ctx context.Context, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) + CreateTrashCategoryWithDetails(ctx context.Context, categoryReq RequestTrashCategoryDTO, detailsReq []RequestTrashDetailDTO) (*ResponseTrashCategoryDTO, error) + CreateTrashCategoryWithIcon(ctx context.Context, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) + UpdateTrashCategory(ctx context.Context, id string, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) + UpdateTrashCategoryWithIcon(ctx context.Context, id string, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) + GetAllTrashCategories(ctx context.Context) ([]ResponseTrashCategoryDTO, error) + GetAllTrashCategoriesWithDetails(ctx context.Context) ([]ResponseTrashCategoryDTO, error) + GetTrashCategoryByID(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) + GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) + DeleteTrashCategory(ctx context.Context, id string) error + + CreateTrashDetail(ctx context.Context, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) + CreateTrashDetailWithIcon(ctx context.Context, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) + AddTrashDetailToCategory(ctx context.Context, categoryID string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) + AddTrashDetailToCategoryWithIcon(ctx context.Context, categoryID string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) + UpdateTrashDetail(ctx context.Context, id string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) + UpdateTrashDetailWithIcon(ctx context.Context, id string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) + GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]ResponseTrashDetailDTO, error) + GetTrashDetailByID(ctx context.Context, id string) (*ResponseTrashDetailDTO, error) + DeleteTrashDetail(ctx context.Context, id string) error + + BulkCreateTrashDetails(ctx context.Context, categoryID string, detailsReq []RequestTrashDetailDTO) ([]ResponseTrashDetailDTO, error) + BulkDeleteTrashDetails(ctx context.Context, detailIDs []string) error + ReorderTrashDetails(ctx context.Context, categoryID string, orderedDetailIDs []string) error +} + +type TrashService struct { + trashRepo TrashRepositoryInterface +} + +func NewTrashService(trashRepo TrashRepositoryInterface) TrashServiceInterface { + return &TrashService{ + trashRepo: trashRepo, + } +} + +func (s *TrashService) saveIconOfTrash(iconTrash *multipart.FileHeader) (string, error) { + pathImage := "/uploads/icontrash/" + iconTrashDir := "./public" + os.Getenv("BASE_URL") + pathImage + + if _, err := os.Stat(iconTrashDir); os.IsNotExist(err) { + if err := os.MkdirAll(iconTrashDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for icon trash: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := strings.ToLower(filepath.Ext(iconTrash.Filename)) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed") + } + + iconTrashFileName := fmt.Sprintf("%s_icontrash%s", uuid.New().String(), extension) + iconTrashPath := filepath.Join(iconTrashDir, iconTrashFileName) + + src, err := iconTrash.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(iconTrashPath) + if err != nil { + return "", fmt.Errorf("failed to create icon trash file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save icon trash: %v", err) + } + + iconTrashUrl := fmt.Sprintf("%s%s", pathImage, iconTrashFileName) + return iconTrashUrl, nil +} + +func (s *TrashService) saveIconOfTrashDetail(iconTrashDetail *multipart.FileHeader) (string, error) { + pathImage := "/uploads/icontrashdetail/" + iconTrashDetailDir := "./public" + os.Getenv("BASE_URL") + pathImage + + if _, err := os.Stat(iconTrashDetailDir); os.IsNotExist(err) { + if err := os.MkdirAll(iconTrashDetailDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for icon trash detail: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} + extension := strings.ToLower(filepath.Ext(iconTrashDetail.Filename)) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed") + } + + iconTrashDetailFileName := fmt.Sprintf("%s_icontrashdetail%s", uuid.New().String(), extension) + iconTrashDetailPath := filepath.Join(iconTrashDetailDir, iconTrashDetailFileName) + + src, err := iconTrashDetail.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(iconTrashDetailPath) + if err != nil { + return "", fmt.Errorf("failed to create icon trash detail file: %v", err) + } + defer dst.Close() + + if _, err := dst.ReadFrom(src); err != nil { + return "", fmt.Errorf("failed to save icon trash detail: %v", err) + } + + iconTrashDetailUrl := fmt.Sprintf("%s%s", pathImage, iconTrashDetailFileName) + return iconTrashDetailUrl, nil +} + +func (s *TrashService) deleteIconTrashFile(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *TrashService) deleteIconTrashDetailFile(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Trash detail image deleted successfully: %s", absolutePath) + return nil +} + +func (s *TrashService) CreateTrashCategoryWithIcon(ctx context.Context, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) { + if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + var iconUrl string + var err error + + if iconFile != nil { + iconUrl, err = s.saveIconOfTrash(iconFile) + if err != nil { + return nil, fmt.Errorf("failed to save icon: %w", err) + } + } + + category := &model.TrashCategory{ + Name: req.Name, + IconTrash: iconUrl, + EstimatedPrice: req.EstimatedPrice, + Variety: req.Variety, + } + + if err := s.trashRepo.CreateTrashCategory(ctx, category); err != nil { + + if iconUrl != "" { + s.deleteIconTrashFile(iconUrl) + } + return nil, fmt.Errorf("failed to create trash category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTO(category) + return response, nil +} + +func (s *TrashService) UpdateTrashCategoryWithIcon(ctx context.Context, id string, req RequestTrashCategoryDTO, iconFile *multipart.FileHeader) (*ResponseTrashCategoryDTO, error) { + if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + existingCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get existing category: %w", err) + } + + var iconUrl string = existingCategory.IconTrash + + if iconFile != nil { + newIconUrl, err := s.saveIconOfTrash(iconFile) + if err != nil { + return nil, fmt.Errorf("failed to save new icon: %w", err) + } + iconUrl = newIconUrl + } + + updates := map[string]interface{}{ + "name": req.Name, + "icon_trash": iconUrl, + "estimated_price": req.EstimatedPrice, + "variety": req.Variety, + } + + if err := s.trashRepo.UpdateTrashCategory(ctx, id, updates); err != nil { + + if iconFile != nil && iconUrl != existingCategory.IconTrash { + s.deleteIconTrashFile(iconUrl) + } + return nil, fmt.Errorf("failed to update trash category: %w", err) + } + + if iconFile != nil && existingCategory.IconTrash != "" && iconUrl != existingCategory.IconTrash { + if err := s.deleteIconTrashFile(existingCategory.IconTrash); err != nil { + log.Printf("Warning: failed to delete old icon: %v", err) + } + } + + updatedCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTO(updatedCategory) + return response, nil +} + +func (s *TrashService) CreateTrashDetailWithIcon(ctx context.Context, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + var iconUrl string + var err error + + if iconFile != nil { + iconUrl, err = s.saveIconOfTrashDetail(iconFile) + if err != nil { + return nil, fmt.Errorf("failed to save icon: %w", err) + } + } + + detail := &model.TrashDetail{ + TrashCategoryID: req.CategoryID, + IconTrashDetail: iconUrl, + Description: req.Description, + StepOrder: req.StepOrder, + } + + if err := s.trashRepo.CreateTrashDetail(ctx, detail); err != nil { + + if iconUrl != "" { + s.deleteIconTrashDetailFile(iconUrl) + } + return nil, fmt.Errorf("failed to create trash detail: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(detail) + return response, nil +} + +func (s *TrashService) AddTrashDetailToCategoryWithIcon(ctx context.Context, categoryID string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + var iconUrl string + var err error + + if iconFile != nil { + iconUrl, err = s.saveIconOfTrashDetail(iconFile) + if err != nil { + return nil, fmt.Errorf("failed to save icon: %w", err) + } + } + + detail := &model.TrashDetail{ + IconTrashDetail: iconUrl, + Description: req.Description, + StepOrder: req.StepOrder, + } + + if err := s.trashRepo.AddTrashDetailToCategory(ctx, categoryID, detail); err != nil { + + if iconUrl != "" { + s.deleteIconTrashDetailFile(iconUrl) + } + return nil, fmt.Errorf("failed to add trash detail to category: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(detail) + return response, nil +} + +func (s *TrashService) UpdateTrashDetailWithIcon(ctx context.Context, id string, req RequestTrashDetailDTO, iconFile *multipart.FileHeader) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + existingDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get existing detail: %w", err) + } + + var iconUrl string = existingDetail.IconTrashDetail + + if iconFile != nil { + newIconUrl, err := s.saveIconOfTrashDetail(iconFile) + if err != nil { + return nil, fmt.Errorf("failed to save new icon: %w", err) + } + iconUrl = newIconUrl + } + + updates := map[string]interface{}{ + "icon_trash_detail": iconUrl, + "description": req.Description, + "step_order": req.StepOrder, + } + + if err := s.trashRepo.UpdateTrashDetail(ctx, id, updates); err != nil { + + if iconFile != nil && iconUrl != existingDetail.IconTrashDetail { + s.deleteIconTrashDetailFile(iconUrl) + } + return nil, fmt.Errorf("failed to update trash detail: %w", err) + } + + if iconFile != nil && existingDetail.IconTrashDetail != "" && iconUrl != existingDetail.IconTrashDetail { + if err := s.deleteIconTrashDetailFile(existingDetail.IconTrashDetail); err != nil { + log.Printf("Warning: failed to delete old icon: %v", err) + } + } + + updatedDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated detail: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(updatedDetail) + return response, nil +} + +func (s *TrashService) CreateTrashCategory(ctx context.Context, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) { + if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + category := &model.TrashCategory{ + Name: req.Name, + IconTrash: req.IconTrash, + EstimatedPrice: req.EstimatedPrice, + Variety: req.Variety, + } + + if err := s.trashRepo.CreateTrashCategory(ctx, category); err != nil { + return nil, fmt.Errorf("failed to create trash category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTO(category) + return response, nil +} + +func (s *TrashService) CreateTrashCategoryWithDetails(ctx context.Context, categoryReq RequestTrashCategoryDTO, detailsReq []RequestTrashDetailDTO) (*ResponseTrashCategoryDTO, error) { + if errors, valid := categoryReq.ValidateRequestTrashCategoryDTO(); !valid { + return nil, fmt.Errorf("category validation failed: %v", errors) + } + + for i, detailReq := range detailsReq { + if errors, valid := detailReq.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("detail %d validation failed: %v", i+1, errors) + } + } + + category := &model.TrashCategory{ + Name: categoryReq.Name, + IconTrash: categoryReq.IconTrash, + EstimatedPrice: categoryReq.EstimatedPrice, + Variety: categoryReq.Variety, + } + + details := make([]model.TrashDetail, len(detailsReq)) + for i, detailReq := range detailsReq { + details[i] = model.TrashDetail{ + IconTrashDetail: detailReq.IconTrashDetail, + Description: detailReq.Description, + StepOrder: detailReq.StepOrder, + } + } + + if err := s.trashRepo.CreateTrashCategoryWithDetails(ctx, category, details); err != nil { + return nil, fmt.Errorf("failed to create trash category with details: %w", err) + } + + createdCategory, err := s.trashRepo.GetTrashCategoryByIDWithDetails(ctx, category.ID) + if err != nil { + return nil, fmt.Errorf("failed to retrieve created category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTOWithDetails(createdCategory) + return response, nil +} + +func (s *TrashService) UpdateTrashCategory(ctx context.Context, id string, req RequestTrashCategoryDTO) (*ResponseTrashCategoryDTO, error) { + if errors, valid := req.ValidateRequestTrashCategoryDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to check category existence: %w", err) + } + if !exists { + return nil, errors.New("trash category not found") + } + + updates := map[string]interface{}{ + "name": req.Name, + "icon_trash": req.IconTrash, + "estimated_price": req.EstimatedPrice, + "variety": req.Variety, + } + + if err := s.trashRepo.UpdateTrashCategory(ctx, id, updates); err != nil { + return nil, fmt.Errorf("failed to update trash category: %w", err) + } + + updatedCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTO(updatedCategory) + return response, nil +} + +func (s *TrashService) GetAllTrashCategories(ctx context.Context) ([]ResponseTrashCategoryDTO, error) { + categories, err := s.trashRepo.GetAllTrashCategories(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get trash categories: %w", err) + } + + responses := make([]ResponseTrashCategoryDTO, len(categories)) + for i, category := range categories { + responses[i] = *s.convertTrashCategoryToResponseDTO(&category) + } + + return responses, nil +} + +func (s *TrashService) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]ResponseTrashCategoryDTO, error) { + categories, err := s.trashRepo.GetAllTrashCategoriesWithDetails(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get trash categories with details: %w", err) + } + + responses := make([]ResponseTrashCategoryDTO, len(categories)) + for i, category := range categories { + responses[i] = *s.convertTrashCategoryToResponseDTOWithDetails(&category) + } + + return responses, nil +} + +func (s *TrashService) GetTrashCategoryByID(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) { + category, err := s.trashRepo.GetTrashCategoryByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get trash category: %w", err) + } + + response := s.convertTrashCategoryToResponseDTO(category) + return response, nil +} + +func (s *TrashService) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*ResponseTrashCategoryDTO, error) { + category, err := s.trashRepo.GetTrashCategoryByIDWithDetails(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get trash category with details: %w", err) + } + + response := s.convertTrashCategoryToResponseDTOWithDetails(category) + return response, nil +} + +func (s *TrashService) DeleteTrashCategory(ctx context.Context, id string) error { + + category, err := s.trashRepo.GetTrashCategoryByID(ctx, id) + if err != nil { + return fmt.Errorf("failed to get category: %w", err) + } + + if err := s.trashRepo.DeleteTrashCategory(ctx, id); err != nil { + return fmt.Errorf("failed to delete trash category: %w", err) + } + + if category.IconTrash != "" { + if err := s.deleteIconTrashFile(category.IconTrash); err != nil { + log.Printf("Warning: failed to delete category icon: %v", err) + } + } + + return nil +} + +func (s *TrashService) CreateTrashDetail(ctx context.Context, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + detail := &model.TrashDetail{ + TrashCategoryID: req.CategoryID, + IconTrashDetail: req.IconTrashDetail, + Description: req.Description, + StepOrder: req.StepOrder, + } + + if err := s.trashRepo.CreateTrashDetail(ctx, detail); err != nil { + return nil, fmt.Errorf("failed to create trash detail: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(detail) + return response, nil +} + +func (s *TrashService) AddTrashDetailToCategory(ctx context.Context, categoryID string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + detail := &model.TrashDetail{ + IconTrashDetail: req.IconTrashDetail, + Description: req.Description, + StepOrder: req.StepOrder, + } + + if err := s.trashRepo.AddTrashDetailToCategory(ctx, categoryID, detail); err != nil { + return nil, fmt.Errorf("failed to add trash detail to category: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(detail) + return response, nil +} + +func (s *TrashService) UpdateTrashDetail(ctx context.Context, id string, req RequestTrashDetailDTO) (*ResponseTrashDetailDTO, error) { + if errors, valid := req.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("validation failed: %v", errors) + } + + exists, err := s.trashRepo.CheckTrashDetailExists(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to check detail existence: %w", err) + } + if !exists { + return nil, errors.New("trash detail not found") + } + + updates := map[string]interface{}{ + "icon_trash_detail": req.IconTrashDetail, + "description": req.Description, + "step_order": req.StepOrder, + } + + if err := s.trashRepo.UpdateTrashDetail(ctx, id, updates); err != nil { + return nil, fmt.Errorf("failed to update trash detail: %w", err) + } + + updatedDetail, err := s.trashRepo.GetTrashDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to retrieve updated detail: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(updatedDetail) + return response, nil +} + +func (s *TrashService) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]ResponseTrashDetailDTO, error) { + exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID) + if err != nil { + return nil, fmt.Errorf("failed to check category existence: %w", err) + } + if !exists { + return nil, errors.New("trash category not found") + } + + details, err := s.trashRepo.GetTrashDetailsByCategory(ctx, categoryID) + if err != nil { + return nil, fmt.Errorf("failed to get trash details: %w", err) + } + + responses := make([]ResponseTrashDetailDTO, len(details)) + for i, detail := range details { + responses[i] = *s.convertTrashDetailToResponseDTO(&detail) + } + + return responses, nil +} + +func (s *TrashService) GetTrashDetailByID(ctx context.Context, id string) (*ResponseTrashDetailDTO, error) { + detail, err := s.trashRepo.GetTrashDetailByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed to get trash detail: %w", err) + } + + response := s.convertTrashDetailToResponseDTO(detail) + return response, nil +} + +func (s *TrashService) DeleteTrashDetail(ctx context.Context, id string) error { + + detail, err := s.trashRepo.GetTrashDetailByID(ctx, id) + if err != nil { + return fmt.Errorf("failed to get detail: %w", err) + } + + if err := s.trashRepo.DeleteTrashDetail(ctx, id); err != nil { + return fmt.Errorf("failed to delete trash detail: %w", err) + } + + if detail.IconTrashDetail != "" { + if err := s.deleteIconTrashDetailFile(detail.IconTrashDetail); err != nil { + log.Printf("Warning: failed to delete detail icon: %v", err) + } + } + + return nil +} + +func (s *TrashService) BulkCreateTrashDetails(ctx context.Context, categoryID string, detailsReq []RequestTrashDetailDTO) ([]ResponseTrashDetailDTO, error) { + exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID) + if err != nil { + return nil, fmt.Errorf("failed to check category existence: %w", err) + } + if !exists { + return nil, errors.New("trash category not found") + } + + for i, detailReq := range detailsReq { + if errors, valid := detailReq.ValidateRequestTrashDetailDTO(); !valid { + return nil, fmt.Errorf("detail %d validation failed: %v", i+1, errors) + } + } + + responses := make([]ResponseTrashDetailDTO, len(detailsReq)) + for i, detailReq := range detailsReq { + response, err := s.AddTrashDetailToCategory(ctx, categoryID, detailReq) + if err != nil { + return nil, fmt.Errorf("failed to create detail %d: %w", i+1, err) + } + responses[i] = *response + } + + return responses, nil +} + +func (s *TrashService) BulkDeleteTrashDetails(ctx context.Context, detailIDs []string) error { + for _, id := range detailIDs { + if err := s.DeleteTrashDetail(ctx, id); err != nil { + return fmt.Errorf("failed to delete detail %s: %w", id, err) + } + } + return nil +} + +func (s *TrashService) ReorderTrashDetails(ctx context.Context, categoryID string, orderedDetailIDs []string) error { + exists, err := s.trashRepo.CheckTrashCategoryExists(ctx, categoryID) + if err != nil { + return fmt.Errorf("failed to check category existence: %w", err) + } + if !exists { + return errors.New("trash category not found") + } + + for i, detailID := range orderedDetailIDs { + updates := map[string]interface{}{ + "step_order": i + 1, + } + if err := s.trashRepo.UpdateTrashDetail(ctx, detailID, updates); err != nil { + return fmt.Errorf("failed to reorder detail %s: %w", detailID, err) + } + } + + return nil +} + +func (s *TrashService) convertTrashCategoryToResponseDTO(category *model.TrashCategory) *ResponseTrashCategoryDTO { + return &ResponseTrashCategoryDTO{ + ID: category.ID, + TrashName: category.Name, + TrashIcon: category.IconTrash, + EstimatedPrice: category.EstimatedPrice, + Variety: category.Variety, + CreatedAt: category.CreatedAt.Format(time.RFC3339), + UpdatedAt: category.UpdatedAt.Format(time.RFC3339), + } +} + +func (s *TrashService) convertTrashCategoryToResponseDTOWithDetails(category *model.TrashCategory) *ResponseTrashCategoryDTO { + response := s.convertTrashCategoryToResponseDTO(category) + + details := make([]ResponseTrashDetailDTO, len(category.Details)) + for i, detail := range category.Details { + details[i] = *s.convertTrashDetailToResponseDTO(&detail) + } + response.TrashDetail = details + + return response +} + +func (s *TrashService) convertTrashDetailToResponseDTO(detail *model.TrashDetail) *ResponseTrashDetailDTO { + return &ResponseTrashDetailDTO{ + ID: detail.ID, + CategoryID: detail.TrashCategoryID, + IconTrashDetail: detail.IconTrashDetail, + Description: detail.Description, + StepOrder: detail.StepOrder, + CreatedAt: detail.CreatedAt.Format(time.RFC3339), + UpdatedAt: detail.UpdatedAt.Format(time.RFC3339), + } +} diff --git a/internal/userpin/userpin_dto.go b/internal/userpin/userpin_dto.go new file mode 100644 index 0000000..5370ea6 --- /dev/null +++ b/internal/userpin/userpin_dto.go @@ -0,0 +1,48 @@ +package userpin + +import ( + "rijig/utils" + "strings" +) + +type RequestPinDTO struct { + DeviceId string `json:"device_id"` + Pin string `json:"userpin"` +} + +func (r *RequestPinDTO) ValidateRequestPinDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if err := utils.ValidatePin(r.Pin); err != nil { + errors["pin"] = append(errors["pin"], err.Error()) + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +type UpdatePinDTO struct { + OldPin string `json:"old_pin"` + NewPin string `json:"new_pin"` +} + +func (u *UpdatePinDTO) ValidateUpdatePinDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(u.OldPin) == "" { + errors["old_pin"] = append(errors["old_pin"], "Old pin is required") + } + + if err := utils.ValidatePin(u.NewPin); err != nil { + errors["new_pin"] = append(errors["new_pin"], err.Error()) + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/userpin/userpin_handler.go b/internal/userpin/userpin_handler.go new file mode 100644 index 0000000..518551e --- /dev/null +++ b/internal/userpin/userpin_handler.go @@ -0,0 +1,77 @@ +package userpin + +import ( + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type UserPinHandler struct { + service UserPinService +} + +func NewUserPinHandler(service UserPinService) *UserPinHandler { + return &UserPinHandler{service} +} + +// userID, ok := c.Locals("user_id").(string) +// +// if !ok || userID == "" { +// return utils.Unauthorized(c, "user_id is missing or invalid") +// } +func (h *UserPinHandler) CreateUserPinHandler(c *fiber.Ctx) error { + // Ambil klaim pengguna yang sudah diautentikasi + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + // Parsing body request untuk PIN + var req RequestPinDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + // Validasi request PIN + if errs, ok := req.ValidateRequestPinDTO(); !ok { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation error", errs) + } + + // Panggil service untuk membuat PIN + err = h.service.CreateUserPin(c.Context(), claims.UserID, &req) + if err != nil { + if err.Error() == "PIN already created" { + return utils.BadRequest(c, err.Error()) // Jika PIN sudah ada, kembalikan error 400 + } + return utils.InternalServerError(c, err.Error()) // Jika terjadi error lain, internal server error + } + + // Mengembalikan response sukses jika berhasil + return utils.Success(c, "PIN created successfully") +} + +func (h *UserPinHandler) VerifyPinHandler(c *fiber.Ctx) error { + // userID, ok := c.Locals("user_id").(string) + // if !ok || userID == "" { + // return utils.Unauthorized(c, "user_id is missing or invalid") + // } + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + var req RequestPinDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request body") + } + + token, err := h.service.VerifyUserPin(c.Context(), claims.UserID, &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "PIN verified successfully", fiber.Map{ + "token": token, + }) +} diff --git a/internal/userpin/userpin_repo.go b/internal/userpin/userpin_repo.go new file mode 100644 index 0000000..4cd6c7e --- /dev/null +++ b/internal/userpin/userpin_repo.go @@ -0,0 +1,50 @@ +package userpin + +import ( + "context" + "rijig/model" + + "gorm.io/gorm" +) + +type UserPinRepository interface { + FindByUserID(ctx context.Context, userID string) (*model.UserPin, error) + Create(ctx context.Context, userPin *model.UserPin) error + Update(ctx context.Context, userPin *model.UserPin) error +} + +type userPinRepository struct { + db *gorm.DB +} + +func NewUserPinRepository(db *gorm.DB) UserPinRepository { + return &userPinRepository{db} +} + +func (r *userPinRepository) FindByUserID(ctx context.Context, userID string) (*model.UserPin, error) { + var userPin model.UserPin + err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&userPin).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, nil + } + return nil, err + } + return &userPin, nil +} + +func (r *userPinRepository) Create(ctx context.Context, userPin *model.UserPin) error { + err := r.db.WithContext(ctx).Create(userPin).Error + if err != nil { + return err + } + return nil +} + +func (r *userPinRepository) Update(ctx context.Context, userPin *model.UserPin) error { + err := r.db.WithContext(ctx).Save(userPin).Error + if err != nil { + return err + } + return nil +} diff --git a/internal/userpin/userpin_route.go b/internal/userpin/userpin_route.go new file mode 100644 index 0000000..81f4dc4 --- /dev/null +++ b/internal/userpin/userpin_route.go @@ -0,0 +1,23 @@ +package userpin + +import ( + "rijig/config" + "rijig/internal/authentication" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func UsersPinRoute(api fiber.Router) { + userPinRepo := NewUserPinRepository(config.DB) + authRepo := authentication.NewAuthenticationRepository(config.DB) + + userPinService := NewUserPinService(userPinRepo, authRepo) + + userPinHandler := NewUserPinHandler(userPinService) + + pin := api.Group("/pin", middleware.AuthMiddleware()) + + pin.Post("/create", userPinHandler.CreateUserPinHandler) + pin.Post("/verif", userPinHandler.VerifyPinHandler) +} diff --git a/internal/userpin/userpin_service.go b/internal/userpin/userpin_service.go new file mode 100644 index 0000000..fd6415f --- /dev/null +++ b/internal/userpin/userpin_service.go @@ -0,0 +1,97 @@ +package userpin + +import ( + "context" + "fmt" + "rijig/internal/authentication" + "rijig/model" + "rijig/utils" + "strings" +) + +type UserPinService interface { + CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error + VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error) +} + +type userPinService struct { + UserPinRepo UserPinRepository + authRepo authentication.AuthenticationRepository +} + +func NewUserPinService(UserPinRepo UserPinRepository, + authRepo authentication.AuthenticationRepository) UserPinService { + return &userPinService{UserPinRepo, authRepo} +} + +func (s *userPinService) CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error { + + if errs, ok := dto.ValidateRequestPinDTO(); !ok { + return fmt.Errorf("validation error: %v", errs) + } + + existingPin, err := s.UserPinRepo.FindByUserID(ctx, userID) + if err != nil { + return fmt.Errorf("failed to check existing PIN: %w", err) + } + if existingPin != nil { + return fmt.Errorf("PIN already created") + } + + hashed, err := utils.HashingPlainText(dto.Pin) + if err != nil { + return fmt.Errorf("failed to hash PIN: %w", err) + } + + userPin := &model.UserPin{ + UserID: userID, + Pin: hashed, + } + + if err := s.UserPinRepo.Create(ctx, userPin); err != nil { + return fmt.Errorf("failed to create PIN: %w", err) + } + + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + return fmt.Errorf("user not found") + } + + roleName := strings.ToLower(user.Role.RoleName) + + progress := authentication.IsRegistrationComplete(roleName, int(user.RegistrationProgress)) + // progress := utils.GetNextRegistrationStep(roleName, int(user.RegistrationProgress)) + // progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress) + // progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress) + + if !progress { + err = s.authRepo.PatchUser(ctx, userID, map[string]interface{}{ + "registration_progress": int(user.RegistrationProgress) + 1, + "registration_status": utils.RegStatusComplete, + }) + if err != nil { + return fmt.Errorf("failed to update user progress: %w", err) + } + } + + return nil +} + +func (s *userPinService) VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error) { + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("user not found") + } + + userPin, err := s.UserPinRepo.FindByUserID(ctx, userID) + if err != nil || userPin == nil { + return nil, fmt.Errorf("PIN not found") + } + + if !utils.CompareHashAndPlainText(userPin.Pin, pin.Pin) { + return nil, fmt.Errorf("PIN does not match, %s , %s", userPin.Pin, pin.Pin) + } + + roleName := strings.ToLower(user.Role.RoleName) + return utils.GenerateTokenPair(user.ID, roleName, pin.DeviceId, user.RegistrationStatus, int(user.RegistrationProgress)) +} diff --git a/internal/userprofile/userprofile_dto.go b/internal/userprofile/userprofile_dto.go new file mode 100644 index 0000000..9f7cd99 --- /dev/null +++ b/internal/userprofile/userprofile_dto.go @@ -0,0 +1,67 @@ +package userprofile + +import ( + "rijig/internal/role" + "rijig/utils" + "strings" +) + +type UserProfileResponseDTO struct { + ID string `json:"id,omitempty"` + Avatar string `json:"avatar,omitempty"` + Name string `json:"name,omitempty"` + Gender string `json:"gender,omitempty"` + Dateofbirth string `json:"dateofbirth,omitempty"` + Placeofbirth string `json:"placeofbirth,omitempty"` + Phone string `json:"phone,omitempty"` + Email string `json:"email,omitempty"` + PhoneVerified bool `json:"phone_verified,omitempty"` + Password string `json:"password,omitempty"` + Role role.RoleResponseDTO `json:"role"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` +} + +type RequestUserProfileDTO struct { + Name string `json:"name"` + Gender string `json:"gender"` + Dateofbirth string `json:"dateofbirth"` + Placeofbirth string `json:"placeofbirth"` + Phone string `json:"phone"` +} + +func (r *RequestUserProfileDTO) ValidateRequestUserProfileDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.Name) == "" { + errors["name"] = append(errors["name"], "Name is required") + } + + if strings.TrimSpace(r.Gender) == "" { + errors["gender"] = append(errors["gender"], "jenis kelamin tidak boleh kosong") + } else if r.Gender != "perempuan" && r.Gender != "laki-laki" { + errors["gender"] = append(errors["gender"], "jenis kelamin harus 'perempuan' atau 'laki-laki'") + } + + if strings.TrimSpace(r.Dateofbirth) == "" { + errors["dateofbirth"] = append(errors["dateofbirth"], "tanggal lahir dibutuhkan") + } else if !utils.IsValidDate(r.Dateofbirth) { + errors["dateofbirth"] = append(errors["dateofbirth"], "tanggal lahir harus berformat DD-MM-YYYY") + } + + if strings.TrimSpace(r.Placeofbirth) == "" { + errors["placeofbirth"] = append(errors["placeofbirth"], "Name is required") + } + + if strings.TrimSpace(r.Phone) == "" { + errors["phone"] = append(errors["phone"], "Phone number is required") + } else if !utils.IsValidPhoneNumber(r.Phone) { + errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/userprofile/userprofile_handler.go b/internal/userprofile/userprofile_handler.go new file mode 100644 index 0000000..27ac07d --- /dev/null +++ b/internal/userprofile/userprofile_handler.go @@ -0,0 +1 @@ +package userprofile \ No newline at end of file diff --git a/internal/userprofile/userprofile_repo.go b/internal/userprofile/userprofile_repo.go new file mode 100644 index 0000000..052c798 --- /dev/null +++ b/internal/userprofile/userprofile_repo.go @@ -0,0 +1,35 @@ +package userprofile + +import ( + "context" + "rijig/model" + + "gorm.io/gorm" +) + +type AuthenticationRepository interface { + UpdateUser(ctx context.Context, user *model.User) error + PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error +} + +type authenticationRepository struct { + db *gorm.DB +} + +func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository { + return &authenticationRepository{db} +} + +func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error { + return r.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", user.ID). + Updates(user).Error +} + +func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error { + return r.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", userID). + Updates(updates).Error +} diff --git a/internal/userprofile/userprofile_route.go b/internal/userprofile/userprofile_route.go new file mode 100644 index 0000000..27ac07d --- /dev/null +++ b/internal/userprofile/userprofile_route.go @@ -0,0 +1 @@ +package userprofile \ No newline at end of file diff --git a/internal/userprofile/userprofile_service.go b/internal/userprofile/userprofile_service.go new file mode 100644 index 0000000..27ac07d --- /dev/null +++ b/internal/userprofile/userprofile_service.go @@ -0,0 +1 @@ +package userprofile \ No newline at end of file diff --git a/internal/whatsapp/whatsapp_handler.go b/internal/whatsapp/whatsapp_handler.go new file mode 100644 index 0000000..e086fc3 --- /dev/null +++ b/internal/whatsapp/whatsapp_handler.go @@ -0,0 +1,24 @@ +package whatsapp + +import ( + "log" + "rijig/config" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func WhatsAppHandler(c *fiber.Ctx) error { + userID, ok := c.Locals("userID").(string) + if !ok || userID == "" { + return utils.Unauthorized(c, "User is not logged in or invalid session") + } + + err := config.LogoutWhatsApp() + if err != nil { + log.Printf("Error during logout process for user %s: %v", userID, err) + return utils.InternalServerError(c, err.Error()) + } + + return utils.Success(c, "Logged out successfully") +} diff --git a/internal/whatsapp/whatsapp_route.go b/internal/whatsapp/whatsapp_route.go new file mode 100644 index 0000000..877bbed --- /dev/null +++ b/internal/whatsapp/whatsapp_route.go @@ -0,0 +1,11 @@ +package whatsapp + +import ( + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func WhatsAppRouter(api fiber.Router) { + api.Post("/logout/whastapp", middleware.AuthMiddleware(), WhatsAppHandler) +} diff --git a/internal/wilayahindo/wilayahindo_dto.go b/internal/wilayahindo/wilayahindo_dto.go new file mode 100644 index 0000000..ff4015a --- /dev/null +++ b/internal/wilayahindo/wilayahindo_dto.go @@ -0,0 +1,27 @@ +package wilayahindo + +type ProvinceResponseDTO struct { + ID string `json:"id"` + Name string `json:"name"` + Regencies []RegencyResponseDTO `json:"regencies,omitempty"` +} + +type RegencyResponseDTO struct { + ID string `json:"id"` + ProvinceID string `json:"province_id"` + Name string `json:"name"` + Districts []DistrictResponseDTO `json:"districts,omitempty"` +} + +type DistrictResponseDTO struct { + ID string `json:"id"` + RegencyID string `json:"regency_id"` + Name string `json:"name"` + Villages []VillageResponseDTO `json:"villages,omitempty"` +} + +type VillageResponseDTO struct { + ID string `json:"id"` + DistrictID string `json:"district_id"` + Name string `json:"name"` +} diff --git a/internal/wilayahindo/wilayahindo_handler.go b/internal/wilayahindo/wilayahindo_handler.go new file mode 100644 index 0000000..2445724 --- /dev/null +++ b/internal/wilayahindo/wilayahindo_handler.go @@ -0,0 +1 @@ +package wilayahindo \ No newline at end of file diff --git a/internal/wilayahindo/wilayahindo_repository.go b/internal/wilayahindo/wilayahindo_repository.go new file mode 100644 index 0000000..0ed548f --- /dev/null +++ b/internal/wilayahindo/wilayahindo_repository.go @@ -0,0 +1,310 @@ +package wilayahindo + +import ( + "context" + "errors" + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type WilayahIndonesiaRepository interface { + ImportProvinces(ctx context.Context, provinces []model.Province) error + ImportRegencies(ctx context.Context, regencies []model.Regency) error + ImportDistricts(ctx context.Context, districts []model.District) error + ImportVillages(ctx context.Context, villages []model.Village) error + + FindAllProvinces(ctx context.Context, page, limit int) ([]model.Province, int, error) + FindProvinceByID(ctx context.Context, id string, page, limit int) (*model.Province, int, error) + + FindAllRegencies(ctx context.Context, page, limit int) ([]model.Regency, int, error) + FindRegencyByID(ctx context.Context, id string, page, limit int) (*model.Regency, int, error) + + FindAllDistricts(ctx context.Context, page, limit int) ([]model.District, int, error) + FindDistrictByID(ctx context.Context, id string, page, limit int) (*model.District, int, error) + + FindAllVillages(ctx context.Context, page, limit int) ([]model.Village, int, error) + FindVillageByID(ctx context.Context, id string) (*model.Village, error) +} + +type wilayahIndonesiaRepository struct { + DB *gorm.DB +} + +func NewWilayahIndonesiaRepository(db *gorm.DB) WilayahIndonesiaRepository { + return &wilayahIndonesiaRepository{DB: db} +} + +func (r *wilayahIndonesiaRepository) ImportProvinces(ctx context.Context, provinces []model.Province) error { + if len(provinces) == 0 { + return errors.New("no provinces to import") + } + + if err := r.DB.WithContext(ctx).CreateInBatches(provinces, 100).Error; err != nil { + return fmt.Errorf("failed to import provinces: %w", err) + } + return nil +} + +func (r *wilayahIndonesiaRepository) ImportRegencies(ctx context.Context, regencies []model.Regency) error { + if len(regencies) == 0 { + return errors.New("no regencies to import") + } + + if err := r.DB.WithContext(ctx).CreateInBatches(regencies, 100).Error; err != nil { + return fmt.Errorf("failed to import regencies: %w", err) + } + return nil +} + +func (r *wilayahIndonesiaRepository) ImportDistricts(ctx context.Context, districts []model.District) error { + if len(districts) == 0 { + return errors.New("no districts to import") + } + + if err := r.DB.WithContext(ctx).CreateInBatches(districts, 100).Error; err != nil { + return fmt.Errorf("failed to import districts: %w", err) + } + return nil +} + +func (r *wilayahIndonesiaRepository) ImportVillages(ctx context.Context, villages []model.Village) error { + if len(villages) == 0 { + return errors.New("no villages to import") + } + + if err := r.DB.WithContext(ctx).CreateInBatches(villages, 100).Error; err != nil { + return fmt.Errorf("failed to import villages: %w", err) + } + return nil +} + +func (r *wilayahIndonesiaRepository) FindAllProvinces(ctx context.Context, page, limit int) ([]model.Province, int, error) { + var provinces []model.Province + var total int64 + + if err := r.DB.WithContext(ctx).Model(&model.Province{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count provinces: %w", err) + } + + query := r.DB.WithContext(ctx) + + if page > 0 && limit > 0 { + if page < 1 { + return nil, 0, errors.New("page must be greater than 0") + } + if limit < 1 || limit > 1000 { + return nil, 0, errors.New("limit must be between 1 and 1000") + } + query = query.Offset((page - 1) * limit).Limit(limit) + } + + if err := query.Find(&provinces).Error; err != nil { + return nil, 0, fmt.Errorf("failed to find provinces: %w", err) + } + + return provinces, int(total), nil +} + +func (r *wilayahIndonesiaRepository) FindProvinceByID(ctx context.Context, id string, page, limit int) (*model.Province, int, error) { + if id == "" { + return nil, 0, errors.New("province ID cannot be empty") + } + + var province model.Province + + preloadQuery := func(db *gorm.DB) *gorm.DB { + if page > 0 && limit > 0 { + if page < 1 { + return db + } + if limit < 1 || limit > 1000 { + return db + } + return db.Offset((page - 1) * limit).Limit(limit) + } + return db + } + + if err := r.DB.WithContext(ctx).Preload("Regencies", preloadQuery).Where("id = ?", id).First(&province).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("province with ID %s not found", id) + } + return nil, 0, fmt.Errorf("failed to find province: %w", err) + } + + var totalRegencies int64 + if err := r.DB.WithContext(ctx).Model(&model.Regency{}).Where("province_id = ?", id).Count(&totalRegencies).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count regencies: %w", err) + } + + return &province, int(totalRegencies), nil +} + +func (r *wilayahIndonesiaRepository) FindAllRegencies(ctx context.Context, page, limit int) ([]model.Regency, int, error) { + var regencies []model.Regency + var total int64 + + if err := r.DB.WithContext(ctx).Model(&model.Regency{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count regencies: %w", err) + } + + query := r.DB.WithContext(ctx) + + if page > 0 && limit > 0 { + if page < 1 { + return nil, 0, errors.New("page must be greater than 0") + } + if limit < 1 || limit > 1000 { + return nil, 0, errors.New("limit must be between 1 and 1000") + } + query = query.Offset((page - 1) * limit).Limit(limit) + } + + if err := query.Find(®encies).Error; err != nil { + return nil, 0, fmt.Errorf("failed to find regencies: %w", err) + } + + return regencies, int(total), nil +} + +func (r *wilayahIndonesiaRepository) FindRegencyByID(ctx context.Context, id string, page, limit int) (*model.Regency, int, error) { + if id == "" { + return nil, 0, errors.New("regency ID cannot be empty") + } + + var regency model.Regency + + preloadQuery := func(db *gorm.DB) *gorm.DB { + if page > 0 && limit > 0 { + if page < 1 { + return db + } + if limit < 1 || limit > 1000 { + return db + } + return db.Offset((page - 1) * limit).Limit(limit) + } + return db + } + + if err := r.DB.WithContext(ctx).Preload("Districts", preloadQuery).Where("id = ?", id).First(®ency).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("regency with ID %s not found", id) + } + return nil, 0, fmt.Errorf("failed to find regency: %w", err) + } + + var totalDistricts int64 + if err := r.DB.WithContext(ctx).Model(&model.District{}).Where("regency_id = ?", id).Count(&totalDistricts).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count districts: %w", err) + } + + return ®ency, int(totalDistricts), nil +} + +func (r *wilayahIndonesiaRepository) FindAllDistricts(ctx context.Context, page, limit int) ([]model.District, int, error) { + var districts []model.District + var total int64 + + if err := r.DB.WithContext(ctx).Model(&model.District{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count districts: %w", err) + } + + query := r.DB.WithContext(ctx) + + if page > 0 && limit > 0 { + if page < 1 { + return nil, 0, errors.New("page must be greater than 0") + } + if limit < 1 || limit > 1000 { + return nil, 0, errors.New("limit must be between 1 and 1000") + } + query = query.Offset((page - 1) * limit).Limit(limit) + } + + if err := query.Find(&districts).Error; err != nil { + return nil, 0, fmt.Errorf("failed to find districts: %w", err) + } + + return districts, int(total), nil +} + +func (r *wilayahIndonesiaRepository) FindDistrictByID(ctx context.Context, id string, page, limit int) (*model.District, int, error) { + if id == "" { + return nil, 0, errors.New("district ID cannot be empty") + } + + var district model.District + + preloadQuery := func(db *gorm.DB) *gorm.DB { + if page > 0 && limit > 0 { + if page < 1 { + return db + } + if limit < 1 || limit > 1000 { + return db + } + return db.Offset((page - 1) * limit).Limit(limit) + } + return db + } + + if err := r.DB.WithContext(ctx).Preload("Villages", preloadQuery).Where("id = ?", id).First(&district).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("district with ID %s not found", id) + } + return nil, 0, fmt.Errorf("failed to find district: %w", err) + } + + var totalVillages int64 + if err := r.DB.WithContext(ctx).Model(&model.Village{}).Where("district_id = ?", id).Count(&totalVillages).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count villages: %w", err) + } + + return &district, int(totalVillages), nil +} + +func (r *wilayahIndonesiaRepository) FindAllVillages(ctx context.Context, page, limit int) ([]model.Village, int, error) { + var villages []model.Village + var total int64 + + if err := r.DB.WithContext(ctx).Model(&model.Village{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count villages: %w", err) + } + + query := r.DB.WithContext(ctx) + + if page > 0 && limit > 0 { + if page < 1 { + return nil, 0, errors.New("page must be greater than 0") + } + if limit < 1 || limit > 1000 { + return nil, 0, errors.New("limit must be between 1 and 1000") + } + query = query.Offset((page - 1) * limit).Limit(limit) + } + + if err := query.Find(&villages).Error; err != nil { + return nil, 0, fmt.Errorf("failed to find villages: %w", err) + } + + return villages, int(total), nil +} + +func (r *wilayahIndonesiaRepository) FindVillageByID(ctx context.Context, id string) (*model.Village, error) { + if id == "" { + return nil, errors.New("village ID cannot be empty") + } + + var village model.Village + if err := r.DB.WithContext(ctx).Where("id = ?", id).First(&village).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("village with ID %s not found", id) + } + return nil, fmt.Errorf("failed to find village: %w", err) + } + + return &village, nil +} diff --git a/internal/wilayahindo/wilayahindo_route.go b/internal/wilayahindo/wilayahindo_route.go new file mode 100644 index 0000000..2445724 --- /dev/null +++ b/internal/wilayahindo/wilayahindo_route.go @@ -0,0 +1 @@ +package wilayahindo \ No newline at end of file diff --git a/internal/wilayahindo/wilayahindo_service.go b/internal/wilayahindo/wilayahindo_service.go new file mode 100644 index 0000000..492b54f --- /dev/null +++ b/internal/wilayahindo/wilayahindo_service.go @@ -0,0 +1,455 @@ +package wilayahindo + +import ( + "context" + "fmt" + "time" + + "rijig/dto" + "rijig/model" + "rijig/utils" +) + +type WilayahIndonesiaService interface { + ImportDataFromCSV(ctx context.Context) error + + GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error) + GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) + + GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error) + GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error) + + GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error) + GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error) + + GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error) + GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error) +} + +type wilayahIndonesiaService struct { + WilayahRepo WilayahIndonesiaRepository +} + +func NewWilayahIndonesiaService(wilayahRepo WilayahIndonesiaRepository) WilayahIndonesiaService { + return &wilayahIndonesiaService{WilayahRepo: wilayahRepo} +} + +func (s *wilayahIndonesiaService) ImportDataFromCSV(ctx context.Context) error { + + provinces, err := utils.ReadCSV("public/document/provinces.csv") + if err != nil { + return fmt.Errorf("failed to read provinces CSV: %w", err) + } + + var provinceList []model.Province + for _, record := range provinces[1:] { + if len(record) >= 2 { + province := model.Province{ + ID: record[0], + Name: record[1], + } + provinceList = append(provinceList, province) + } + } + + if err := s.WilayahRepo.ImportProvinces(ctx, provinceList); err != nil { + return fmt.Errorf("failed to import provinces: %w", err) + } + + regencies, err := utils.ReadCSV("public/document/regencies.csv") + if err != nil { + return fmt.Errorf("failed to read regencies CSV: %w", err) + } + + var regencyList []model.Regency + for _, record := range regencies[1:] { + if len(record) >= 3 { + regency := model.Regency{ + ID: record[0], + ProvinceID: record[1], + Name: record[2], + } + regencyList = append(regencyList, regency) + } + } + + if err := s.WilayahRepo.ImportRegencies(ctx, regencyList); err != nil { + return fmt.Errorf("failed to import regencies: %w", err) + } + + districts, err := utils.ReadCSV("public/document/districts.csv") + if err != nil { + return fmt.Errorf("failed to read districts CSV: %w", err) + } + + var districtList []model.District + for _, record := range districts[1:] { + if len(record) >= 3 { + district := model.District{ + ID: record[0], + RegencyID: record[1], + Name: record[2], + } + districtList = append(districtList, district) + } + } + + if err := s.WilayahRepo.ImportDistricts(ctx, districtList); err != nil { + return fmt.Errorf("failed to import districts: %w", err) + } + + villages, err := utils.ReadCSV("public/document/villages.csv") + if err != nil { + return fmt.Errorf("failed to read villages CSV: %w", err) + } + + var villageList []model.Village + for _, record := range villages[1:] { + if len(record) >= 3 { + village := model.Village{ + ID: record[0], + DistrictID: record[1], + Name: record[2], + } + villageList = append(villageList, village) + } + } + + if err := s.WilayahRepo.ImportVillages(ctx, villageList); err != nil { + return fmt.Errorf("failed to import villages: %w", err) + } + + return nil +} + +func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error) { + cacheKey := fmt.Sprintf("provinces_page:%d_limit:%d", page, limit) + + var cachedResponse struct { + Data []dto.ProvinceResponseDTO `json:"data"` + Total int `json:"total"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return cachedResponse.Data, cachedResponse.Total, nil + } + + provinces, total, err := s.WilayahRepo.FindAllProvinces(ctx, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch provinces: %w", err) + } + + provinceDTOs := make([]dto.ProvinceResponseDTO, len(provinces)) + for i, province := range provinces { + provinceDTOs[i] = dto.ProvinceResponseDTO{ + ID: province.ID, + Name: province.Name, + } + } + + cacheData := struct { + Data []dto.ProvinceResponseDTO `json:"data"` + Total int `json:"total"` + }{ + Data: provinceDTOs, + Total: total, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching provinces data: %v\n", err) + } + + return provinceDTOs, total, nil +} + +func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) { + cacheKey := fmt.Sprintf("province:%s_page:%d_limit:%d", id, page, limit) + + var cachedResponse struct { + Data dto.ProvinceResponseDTO `json:"data"` + TotalRegencies int `json:"total_regencies"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return &cachedResponse.Data, cachedResponse.TotalRegencies, nil + } + + province, totalRegencies, err := s.WilayahRepo.FindProvinceByID(ctx, id, page, limit) + if err != nil { + return nil, 0, err + } + + provinceDTO := dto.ProvinceResponseDTO{ + ID: province.ID, + Name: province.Name, + } + + regencyDTOs := make([]dto.RegencyResponseDTO, len(province.Regencies)) + for i, regency := range province.Regencies { + regencyDTOs[i] = dto.RegencyResponseDTO{ + ID: regency.ID, + ProvinceID: regency.ProvinceID, + Name: regency.Name, + } + } + provinceDTO.Regencies = regencyDTOs + + cacheData := struct { + Data dto.ProvinceResponseDTO `json:"data"` + TotalRegencies int `json:"total_regencies"` + }{ + Data: provinceDTO, + TotalRegencies: totalRegencies, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching province data: %v\n", err) + } + + return &provinceDTO, totalRegencies, nil +} + +func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error) { + cacheKey := fmt.Sprintf("regencies_page:%d_limit:%d", page, limit) + + var cachedResponse struct { + Data []dto.RegencyResponseDTO `json:"data"` + Total int `json:"total"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return cachedResponse.Data, cachedResponse.Total, nil + } + + regencies, total, err := s.WilayahRepo.FindAllRegencies(ctx, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch regencies: %w", err) + } + + regencyDTOs := make([]dto.RegencyResponseDTO, len(regencies)) + for i, regency := range regencies { + regencyDTOs[i] = dto.RegencyResponseDTO{ + ID: regency.ID, + ProvinceID: regency.ProvinceID, + Name: regency.Name, + } + } + + cacheData := struct { + Data []dto.RegencyResponseDTO `json:"data"` + Total int `json:"total"` + }{ + Data: regencyDTOs, + Total: total, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching regencies data: %v\n", err) + } + + return regencyDTOs, total, nil +} + +func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error) { + cacheKey := fmt.Sprintf("regency:%s_page:%d_limit:%d", id, page, limit) + + var cachedResponse struct { + Data dto.RegencyResponseDTO `json:"data"` + TotalDistricts int `json:"total_districts"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return &cachedResponse.Data, cachedResponse.TotalDistricts, nil + } + + regency, totalDistricts, err := s.WilayahRepo.FindRegencyByID(ctx, id, page, limit) + if err != nil { + return nil, 0, err + } + + regencyDTO := dto.RegencyResponseDTO{ + ID: regency.ID, + ProvinceID: regency.ProvinceID, + Name: regency.Name, + } + + districtDTOs := make([]dto.DistrictResponseDTO, len(regency.Districts)) + for i, district := range regency.Districts { + districtDTOs[i] = dto.DistrictResponseDTO{ + ID: district.ID, + RegencyID: district.RegencyID, + Name: district.Name, + } + } + regencyDTO.Districts = districtDTOs + + cacheData := struct { + Data dto.RegencyResponseDTO `json:"data"` + TotalDistricts int `json:"total_districts"` + }{ + Data: regencyDTO, + TotalDistricts: totalDistricts, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching regency data: %v\n", err) + } + + return ®encyDTO, totalDistricts, nil +} + +func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error) { + cacheKey := fmt.Sprintf("districts_page:%d_limit:%d", page, limit) + + var cachedResponse struct { + Data []dto.DistrictResponseDTO `json:"data"` + Total int `json:"total"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return cachedResponse.Data, cachedResponse.Total, nil + } + + districts, total, err := s.WilayahRepo.FindAllDistricts(ctx, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch districts: %w", err) + } + + districtDTOs := make([]dto.DistrictResponseDTO, len(districts)) + for i, district := range districts { + districtDTOs[i] = dto.DistrictResponseDTO{ + ID: district.ID, + RegencyID: district.RegencyID, + Name: district.Name, + } + } + + cacheData := struct { + Data []dto.DistrictResponseDTO `json:"data"` + Total int `json:"total"` + }{ + Data: districtDTOs, + Total: total, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching districts data: %v\n", err) + } + + return districtDTOs, total, nil +} + +func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error) { + cacheKey := fmt.Sprintf("district:%s_page:%d_limit:%d", id, page, limit) + + var cachedResponse struct { + Data dto.DistrictResponseDTO `json:"data"` + TotalVillages int `json:"total_villages"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return &cachedResponse.Data, cachedResponse.TotalVillages, nil + } + + district, totalVillages, err := s.WilayahRepo.FindDistrictByID(ctx, id, page, limit) + if err != nil { + return nil, 0, err + } + + districtDTO := dto.DistrictResponseDTO{ + ID: district.ID, + RegencyID: district.RegencyID, + Name: district.Name, + } + + villageDTOs := make([]dto.VillageResponseDTO, len(district.Villages)) + for i, village := range district.Villages { + villageDTOs[i] = dto.VillageResponseDTO{ + ID: village.ID, + DistrictID: village.DistrictID, + Name: village.Name, + } + } + districtDTO.Villages = villageDTOs + + cacheData := struct { + Data dto.DistrictResponseDTO `json:"data"` + TotalVillages int `json:"total_villages"` + }{ + Data: districtDTO, + TotalVillages: totalVillages, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching district data: %v\n", err) + } + + return &districtDTO, totalVillages, nil +} + +func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error) { + cacheKey := fmt.Sprintf("villages_page:%d_limit:%d", page, limit) + + var cachedResponse struct { + Data []dto.VillageResponseDTO `json:"data"` + Total int `json:"total"` + } + + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return cachedResponse.Data, cachedResponse.Total, nil + } + + villages, total, err := s.WilayahRepo.FindAllVillages(ctx, page, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to fetch villages: %w", err) + } + + villageDTOs := make([]dto.VillageResponseDTO, len(villages)) + for i, village := range villages { + villageDTOs[i] = dto.VillageResponseDTO{ + ID: village.ID, + DistrictID: village.DistrictID, + Name: village.Name, + } + } + + cacheData := struct { + Data []dto.VillageResponseDTO `json:"data"` + Total int `json:"total"` + }{ + Data: villageDTOs, + Total: total, + } + + if err := utils.SetCache(cacheKey, cacheData, 24*time.Hour); err != nil { + fmt.Printf("Error caching villages data: %v\n", err) + } + + return villageDTOs, total, nil +} + +func (s *wilayahIndonesiaService) GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error) { + cacheKey := fmt.Sprintf("village:%s", id) + + var cachedResponse dto.VillageResponseDTO + if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { + return &cachedResponse, nil + } + + village, err := s.WilayahRepo.FindVillageByID(ctx, id) + if err != nil { + return nil, fmt.Errorf("village not found: %w", err) + } + + villageResponse := &dto.VillageResponseDTO{ + ID: village.ID, + DistrictID: village.DistrictID, + Name: village.Name, + } + + if err := utils.SetCache(cacheKey, villageResponse, 24*time.Hour); err != nil { + fmt.Printf("Error caching village data: %v\n", err) + } + + return villageResponse, nil +} diff --git a/middleware/additional_middleware.go b/middleware/additional_middleware.go new file mode 100644 index 0000000..95db86a --- /dev/null +++ b/middleware/additional_middleware.go @@ -0,0 +1,199 @@ +package middleware +/* +import ( + "fmt" + "time" + + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func RateLimitByUser(maxRequests int, duration time.Duration) fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + key := fmt.Sprintf("rate_limit:%s:%s", claims.UserID, c.Route().Path) + + count, err := utils.IncrementCounter(key, duration) + if err != nil { + + return c.Next() + } + + if count > int64(maxRequests) { + + ttl, _ := utils.GetTTL(key) + return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ + "error": "Rate limit exceeded", + "message": "Terlalu banyak permintaan, silakan coba lagi nanti", + "retry_after": int64(ttl.Seconds()), + "limit": maxRequests, + "remaining": 0, + }) + } + + c.Set("X-RateLimit-Limit", fmt.Sprintf("%d", maxRequests)) + c.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", maxRequests-int(count))) + c.Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(duration).Unix())) + + return c.Next() + } +} + +func SessionValidation() fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + if claims.SessionID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Invalid session", + "message": "Session tidak valid", + }) + } + + sessionKey := fmt.Sprintf("session:%s", claims.SessionID) + var sessionData map[string]interface{} + err = utils.GetCache(sessionKey, &sessionData) + if err != nil { + if err.Error() == "ErrCacheMiss" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Session not found", + "message": "Session tidak ditemukan, silakan login kembali", + }) + } + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Session error", + "message": "Terjadi kesalahan saat validasi session", + }) + } + + if sessionData["user_id"] != claims.UserID { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Session mismatch", + "message": "Session tidak sesuai dengan user", + }) + } + + if expiryInterface, exists := sessionData["expires_at"]; exists { + if expiry, ok := expiryInterface.(float64); ok { + if time.Now().Unix() > int64(expiry) { + + utils.DeleteCache(sessionKey) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Session expired", + "message": "Session telah berakhir, silakan login kembali", + }) + } + } + } + + return c.Next() + } +} + +func RequireApprovedRegistration() fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + if claims.RegistrationStatus == utils.RegStatusRejected { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Registration rejected", + "message": "Registrasi Anda ditolak, silakan hubungi admin", + }) + } + + if claims.RegistrationStatus == utils.RegStatusPending { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Registration pending", + "message": "Registrasi Anda masih menunggu persetujuan admin", + }) + } + + if claims.RegistrationStatus != utils.RegStatusComplete { + progress := utils.GetUserRegistrationProgress(claims.UserID) + nextStep := utils.GetNextRegistrationStep(claims.Role, progress) + + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Registration incomplete", + "message": "Silakan lengkapi registrasi terlebih dahulu", + "registration_status": claims.RegistrationStatus, + "next_step": nextStep, + }) + } + + return c.Next() + } +} + +func ConditionalAuth(condition func(*utils.JWTClaims) bool, errorMessage string) fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + if !condition(claims) { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Condition not met", + "message": errorMessage, + }) + } + + return c.Next() + } +} + +func RequireSpecificRole(role string) fiber.Handler { + return ConditionalAuth( + func(claims *utils.JWTClaims) bool { + return claims.Role == role + }, + fmt.Sprintf("Akses ini hanya untuk role %s", role), + ) +} + +func RequireCompleteRegistrationAndSpecificRole(role string) fiber.Handler { + return ConditionalAuth( + func(claims *utils.JWTClaims) bool { + return claims.Role == role && utils.IsRegistrationComplete(claims.RegistrationStatus) + }, + fmt.Sprintf("Akses ini hanya untuk role %s dengan registrasi lengkap", role), + ) +} + +func DeviceValidation() fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + deviceID := c.Get("X-Device-ID") + if deviceID == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + "error": "Device ID required", + "message": "Device ID diperlukan", + }) + } + + if claims.DeviceID != deviceID { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Device mismatch", + "message": "Token tidak valid untuk device ini", + }) + } + + return c.Next() + } +} + */ \ No newline at end of file diff --git a/middleware/api_key.go b/middleware/api_key.go index 3f2c7d6..1077eb0 100644 --- a/middleware/api_key.go +++ b/middleware/api_key.go @@ -11,12 +11,12 @@ import ( func APIKeyMiddleware(c *fiber.Ctx) error { apiKey := c.Get("x-api-key") if apiKey == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: API key is required") + return utils.Unauthorized(c, "Unauthorized: API key is required") } validAPIKey := os.Getenv("API_KEY") if apiKey != validAPIKey { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid API key") + return utils.Unauthorized(c, "Unauthorized: Invalid API key") } return c.Next() diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go deleted file mode 100644 index 9549e25..0000000 --- a/middleware/auth_middleware.go +++ /dev/null @@ -1,65 +0,0 @@ -package middleware - -import ( - "fmt" - "log" - "os" - "rijig/utils" - - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" -) - -func AuthMiddleware(c *fiber.Ctx) error { - tokenString := c.Get("Authorization") - if tokenString == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: No token provided") - } - - if len(tokenString) > 7 && tokenString[:7] == "Bearer " { - tokenString = tokenString[7:] - } - - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return []byte(os.Getenv("SECRET_KEY")), nil - }) - - if err != nil || !token.Valid { - log.Printf("Error parsing token: %v", err) - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token") - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || claims["sub"] == nil || claims["device_id"] == nil { - log.Println("Invalid token claims") - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid token claims") - } - - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Unexpected signing method") - } - - userID := claims["sub"].(string) - deviceID := claims["device_id"].(string) - - sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) - sessionData, err := utils.GetJSONData(sessionKey) - if err != nil || sessionData == nil { - log.Printf("Session expired or invalid for userID: %s, deviceID: %s", userID, deviceID) - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Session expired or invalid") - } - - roleID, roleOK := sessionData["roleID"].(string) - roleName, roleNameOK := sessionData["roleName"].(string) - if !roleOK || !roleNameOK { - log.Println("Invalid session data for userID:", userID) - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Invalid session data") - } - - c.Locals("userID", userID) - c.Locals("roleID", roleID) - c.Locals("roleName", roleName) - c.Locals("device_id", deviceID) - - return c.Next() -} diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..c369de4 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,564 @@ +package middleware + +import ( + "crypto/subtle" + "fmt" + "rijig/utils" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +type AuthConfig struct { + RequiredTokenType utils.TokenType + RequiredRoles []string + RequiredStatuses []string + RequiredStep int + RequireComplete bool + SkipAuth bool + AllowPartialToken bool + CustomErrorHandler ErrorHandler +} + +type ErrorHandler func(c *fiber.Ctx, err error) error + +type AuthContext struct { + Claims *utils.JWTClaims + StepInfo *utils.RegistrationStepInfo + IsAdmin bool + CanAccess bool +} + +type AuthError struct { + Code string `json:"error"` + Message string `json:"message"` + Details interface{} `json:"details,omitempty"` +} + +func (e *AuthError) Error() string { + return e.Message +} + +var ( + ErrMissingToken = &AuthError{ + Code: "MISSING_TOKEN", + Message: "Token akses diperlukan", + } + + ErrInvalidTokenFormat = &AuthError{ + Code: "INVALID_TOKEN_FORMAT", + Message: "Format token tidak valid", + } + + ErrInvalidToken = &AuthError{ + Code: "INVALID_TOKEN", + Message: "Token tidak valid atau telah kadaluarsa", + } + + ErrUserContextNotFound = &AuthError{ + Code: "USER_CONTEXT_NOT_FOUND", + Message: "Silakan login terlebih dahulu", + } + + ErrInsufficientPermissions = &AuthError{ + Code: "INSUFFICIENT_PERMISSIONS", + Message: "Akses ditolak untuk role ini", + } + + ErrRegistrationIncomplete = &AuthError{ + Code: "REGISTRATION_INCOMPLETE", + Message: "Registrasi belum lengkap", + } + + ErrRegistrationNotApproved = &AuthError{ + Code: "REGISTRATION_NOT_APPROVED", + Message: "Registrasi belum disetujui", + } + + ErrInvalidTokenType = &AuthError{ + Code: "INVALID_TOKEN_TYPE", + Message: "Tipe token tidak sesuai", + } + + ErrStepNotAccessible = &AuthError{ + Code: "STEP_NOT_ACCESSIBLE", + Message: "Step registrasi belum dapat diakses", + } + + ErrAwaitingApproval = &AuthError{ + Code: "AWAITING_ADMIN_APPROVAL", + Message: "Menunggu persetujuan admin", + } + + ErrInvalidRegistrationStatus = &AuthError{ + Code: "INVALID_REGISTRATION_STATUS", + Message: "Status registrasi tidak sesuai", + } +) + +func defaultErrorHandler(c *fiber.Ctx, err error) error { + if authErr, ok := err.(*AuthError); ok { + statusCode := getStatusCodeForError(authErr.Code) + return c.Status(statusCode).JSON(authErr) + } + + return c.Status(fiber.StatusInternalServerError).JSON(&AuthError{ + Code: "INTERNAL_ERROR", + Message: "Terjadi kesalahan internal", + }) +} + +func getStatusCodeForError(errorCode string) int { + switch errorCode { + case "MISSING_TOKEN", "INVALID_TOKEN_FORMAT", "INVALID_TOKEN", "USER_CONTEXT_NOT_FOUND": + return fiber.StatusUnauthorized + case "INSUFFICIENT_PERMISSIONS", "REGISTRATION_INCOMPLETE", "REGISTRATION_NOT_APPROVED", + "INVALID_TOKEN_TYPE", "STEP_NOT_ACCESSIBLE", "AWAITING_ADMIN_APPROVAL", + "INVALID_REGISTRATION_STATUS": + return fiber.StatusForbidden + default: + return fiber.StatusInternalServerError + } +} + +func AuthMiddleware(config ...AuthConfig) fiber.Handler { + cfg := AuthConfig{} + if len(config) > 0 { + cfg = config[0] + } + + if cfg.CustomErrorHandler == nil { + cfg.CustomErrorHandler = defaultErrorHandler + } + + return func(c *fiber.Ctx) error { + + if cfg.SkipAuth { + return c.Next() + } + + claims, err := extractAndValidateToken(c) + if err != nil { + return cfg.CustomErrorHandler(c, err) + } + + authCtx := createAuthContext(claims) + + if err := validateAuthConfig(authCtx, cfg); err != nil { + return cfg.CustomErrorHandler(c, err) + } + + c.Locals("user", claims) + c.Locals("auth_context", authCtx) + + return c.Next() + } +} + +func extractAndValidateToken(c *fiber.Ctx) (*utils.JWTClaims, error) { + authHeader := c.Get("Authorization") + if authHeader == "" { + return nil, ErrMissingToken + } + + token, err := utils.ExtractTokenFromHeader(authHeader) + if err != nil { + return nil, ErrInvalidTokenFormat + } + + claims, err := utils.ValidateAccessToken(token) + if err != nil { + return nil, ErrInvalidToken + } + + return claims, nil +} + +func createAuthContext(claims *utils.JWTClaims) *AuthContext { + stepInfo := utils.GetRegistrationStepInfo( + claims.Role, + claims.RegistrationProgress, + claims.RegistrationStatus, + ) + + return &AuthContext{ + Claims: claims, + StepInfo: stepInfo, + IsAdmin: claims.Role == utils.RoleAdministrator, + CanAccess: stepInfo.IsAccessible, + } +} + +func validateAuthConfig(authCtx *AuthContext, cfg AuthConfig) error { + claims := authCtx.Claims + + if cfg.RequiredTokenType != "" { + if claims.TokenType != cfg.RequiredTokenType { + return &AuthError{ + Code: "INVALID_TOKEN_TYPE", + Message: fmt.Sprintf("Endpoint memerlukan token type: %s", cfg.RequiredTokenType), + Details: fiber.Map{ + "current_token_type": claims.TokenType, + "required_token_type": cfg.RequiredTokenType, + }, + } + } + } + + if len(cfg.RequiredRoles) > 0 { + if !contains(cfg.RequiredRoles, claims.Role) { + return &AuthError{ + Code: "INSUFFICIENT_PERMISSIONS", + Message: "Akses ditolak untuk role ini", + Details: fiber.Map{ + "user_role": claims.Role, + "allowed_roles": cfg.RequiredRoles, + }, + } + } + } + + if len(cfg.RequiredStatuses) > 0 { + if !contains(cfg.RequiredStatuses, claims.RegistrationStatus) { + return &AuthError{ + Code: "INVALID_REGISTRATION_STATUS", + Message: "Status registrasi tidak sesuai", + Details: fiber.Map{ + "current_status": claims.RegistrationStatus, + "allowed_statuses": cfg.RequiredStatuses, + "next_step": authCtx.StepInfo.Description, + }, + } + } + } + + if cfg.RequiredStep > 0 { + if claims.RegistrationProgress < cfg.RequiredStep { + return &AuthError{ + Code: "STEP_NOT_ACCESSIBLE", + Message: "Step registrasi belum dapat diakses", + Details: fiber.Map{ + "current_step": claims.RegistrationProgress, + "required_step": cfg.RequiredStep, + "current_step_info": authCtx.StepInfo.Description, + }, + } + } + + if authCtx.StepInfo.RequiresAdminApproval && !authCtx.CanAccess { + return &AuthError{ + Code: "AWAITING_ADMIN_APPROVAL", + Message: "Menunggu persetujuan admin", + Details: fiber.Map{ + "status": claims.RegistrationStatus, + }, + } + } + } + + if cfg.RequireComplete { + if claims.TokenType != utils.TokenTypeFull { + return &AuthError{ + Code: "REGISTRATION_INCOMPLETE", + Message: "Registrasi belum lengkap", + Details: fiber.Map{ + "registration_status": claims.RegistrationStatus, + "registration_progress": claims.RegistrationProgress, + "next_step": authCtx.StepInfo.Description, + "requires_admin_approval": authCtx.StepInfo.RequiresAdminApproval, + "can_proceed": authCtx.CanAccess, + }, + } + } + + if !utils.IsRegistrationComplete(claims.RegistrationStatus) { + return ErrRegistrationNotApproved + } + } + + return nil +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func RequireAuth() fiber.Handler { + return AuthMiddleware() +} + +func RequireFullToken() fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredTokenType: utils.TokenTypeFull, + RequireComplete: true, + }) +} + +func RequirePartialToken() fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredTokenType: utils.TokenTypePartial, + }) +} + +func RequireRoles(roles ...string) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredRoles: roles, + }) +} + +func RequireAdminRole() fiber.Handler { + return RequireRoles(utils.RoleAdministrator) +} + +func RequireRegistrationStep(step int) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredStep: step, + }) +} + +func RequireRegistrationStatus(statuses ...string) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredStatuses: statuses, + }) +} + +func RequireTokenType(tokenType utils.TokenType) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredTokenType: tokenType, + }) +} + +func RequireCompleteRegistrationForRole(roles ...string) fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + if contains(roles, claims.Role) { + return RequireFullToken()(c) + } + + return c.Next() + } +} + +func RequireRoleAndComplete(roles ...string) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredRoles: roles, + RequireComplete: true, + }) +} + +func RequireRoleAndStep(step int, roles ...string) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredRoles: roles, + RequiredStep: step, + }) +} + +func RequireRoleAndStatus(statuses []string, roles ...string) fiber.Handler { + return AuthMiddleware(AuthConfig{ + RequiredRoles: roles, + RequiredStatuses: statuses, + }) +} + +func GetUserFromContext(c *fiber.Ctx) (*utils.JWTClaims, error) { + claims, ok := c.Locals("user").(*utils.JWTClaims) + if !ok { + return nil, ErrUserContextNotFound + } + return claims, nil +} + +func GetAuthContextFromContext(c *fiber.Ctx) (*AuthContext, error) { + authCtx, ok := c.Locals("auth_context").(*AuthContext) + if !ok { + + claims, err := GetUserFromContext(c) + if err != nil { + return nil, err + } + return createAuthContext(claims), nil + } + return authCtx, nil +} + +func MustGetUserFromContext(c *fiber.Ctx) *utils.JWTClaims { + claims, err := GetUserFromContext(c) + if err != nil { + panic("user context not found") + } + return claims +} + +func GetUserID(c *fiber.Ctx) (string, error) { + claims, err := GetUserFromContext(c) + if err != nil { + return "", err + } + return claims.UserID, nil +} + +func GetUserRole(c *fiber.Ctx) (string, error) { + claims, err := GetUserFromContext(c) + if err != nil { + return "", err + } + return claims.Role, nil +} + +func IsAdmin(c *fiber.Ctx) bool { + claims, err := GetUserFromContext(c) + if err != nil { + return false + } + return claims.Role == utils.RoleAdministrator +} + +func IsRegistrationComplete(c *fiber.Ctx) bool { + claims, err := GetUserFromContext(c) + if err != nil { + return false + } + return utils.IsRegistrationComplete(claims.RegistrationStatus) +} + +func HasRole(c *fiber.Ctx, role string) bool { + claims, err := GetUserFromContext(c) + if err != nil { + return false + } + return claims.Role == role +} + +func HasAnyRole(c *fiber.Ctx, roles ...string) bool { + claims, err := GetUserFromContext(c) + if err != nil { + return false + } + return contains(roles, claims.Role) +} + +type RateLimitConfig struct { + MaxRequests int + Window time.Duration + KeyFunc func(*fiber.Ctx) string + SkipFunc func(*fiber.Ctx) bool +} + +func AuthRateLimit(config RateLimitConfig) fiber.Handler { + if config.KeyFunc == nil { + config.KeyFunc = func(c *fiber.Ctx) string { + claims, err := GetUserFromContext(c) + if err != nil { + return c.IP() + } + return fmt.Sprintf("user:%s", claims.UserID) + } + } + + return func(c *fiber.Ctx) error { + if config.SkipFunc != nil && config.SkipFunc(c) { + return c.Next() + } + + key := fmt.Sprintf("rate_limit:%s", config.KeyFunc(c)) + + var count int + err := utils.GetCache(key, &count) + if err != nil { + count = 0 + } + + if count >= config.MaxRequests { + return c.Status(fiber.StatusTooManyRequests).JSON(&AuthError{ + Code: "RATE_LIMIT_EXCEEDED", + Message: "Terlalu banyak permintaan, coba lagi nanti", + Details: fiber.Map{ + "max_requests": config.MaxRequests, + "window": config.Window.String(), + }, + }) + } + + count++ + utils.SetCache(key, count, config.Window) + + return c.Next() + } +} + +func DeviceValidation() fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + deviceID := c.Get("X-Device-ID") + if deviceID == "" { + return c.Status(fiber.StatusBadRequest).JSON(&AuthError{ + Code: "MISSING_DEVICE_ID", + Message: "Device ID diperlukan", + }) + } + + if subtle.ConstantTimeCompare([]byte(claims.DeviceID), []byte(deviceID)) != 1 { + return c.Status(fiber.StatusForbidden).JSON(&AuthError{ + Code: "DEVICE_MISMATCH", + Message: "Device tidak cocok dengan token", + }) + } + + return c.Next() + } +} + +func SessionValidation() fiber.Handler { + return func(c *fiber.Ctx) error { + claims, err := GetUserFromContext(c) + if err != nil { + return err + } + + sessionKey := fmt.Sprintf("session:%s", claims.SessionID) + var sessionData interface{} + err = utils.GetCache(sessionKey, &sessionData) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(&AuthError{ + Code: "SESSION_EXPIRED", + Message: "Sesi telah berakhir, silakan login kembali", + }) + } + + return c.Next() + } +} + +func AuthLogger() fiber.Handler { + return logger.New(logger.Config{ + Format: "[${time}] ${status} - ${method} ${path} - User: ${locals:user_id} Role: ${locals:user_role} IP: ${ip}\n", + CustomTags: map[string]logger.LogFunc{ + "user_id": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) { + if claims, err := GetUserFromContext(c); err == nil { + return output.WriteString(claims.UserID) + } + return output.WriteString("anonymous") + }, + "user_role": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) { + if claims, err := GetUserFromContext(c); err == nil { + return output.WriteString(claims.Role) + } + return output.WriteString("none") + }, + }, + }) +} diff --git a/middleware/role_middleware.go b/middleware/role_middleware.go deleted file mode 100644 index cfd864c..0000000 --- a/middleware/role_middleware.go +++ /dev/null @@ -1,29 +0,0 @@ -package middleware - -import ( - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func RoleMiddleware(allowedRoles ...string) fiber.Handler { - return func(c *fiber.Ctx) error { - - if len(allowedRoles) == 0 { - return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: No roles specified") - } - - roleID, ok := c.Locals("roleID").(string) - if !ok || roleID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: Role not found") - } - - for _, role := range allowedRoles { - if role == roleID { - return c.Next() - } - } - - return utils.GenericResponse(c, fiber.StatusForbidden, "Access Denied: You don't have permission to access this resource") - } -} diff --git a/model/company_profile_model.go b/model/company_profile_model.go index 915652e..8e39ae7 100644 --- a/model/company_profile_model.go +++ b/model/company_profile_model.go @@ -11,7 +11,7 @@ type CompanyProfile struct { CompanyName string `gorm:"not null" json:"company_name"` CompanyAddress string `gorm:"not null" json:"company_address"` CompanyPhone string `gorm:"not null" json:"company_phone"` - CompanyEmail string `gorm:"not null" json:"company_email"` + CompanyEmail string `json:"company_email,omitempty"` CompanyLogo string `json:"company_logo,omitempty"` CompanyWebsite string `json:"company_website,omitempty"` TaxID string `json:"tax_id,omitempty"` diff --git a/model/identitycard_model.go b/model/identitycard_model.go index d7f6f31..c35cfc8 100644 --- a/model/identitycard_model.go +++ b/model/identitycard_model.go @@ -11,9 +11,13 @@ type IdentityCard struct { Dateofbirth string `gorm:"not null" json:"dateofbirth"` Gender string `gorm:"not null" json:"gender"` BloodType string `gorm:"not null" json:"bloodtype"` + Province string `gorm:"not null" json:"province"` District string `gorm:"not null" json:"district"` + SubDistrict string `gorm:"not null" json:"subdistrict"` + Hamlet string `gorm:"not null" json:"hamlet"` Village string `gorm:"not null" json:"village"` Neighbourhood string `gorm:"not null" json:"neighbourhood"` + PostalCode string `gorm:"not null" json:"postalcode"` Religion string `gorm:"not null" json:"religion"` Maritalstatus string `gorm:"not null" json:"maritalstatus"` Job string `gorm:"not null" json:"job"` diff --git a/model/trash_model.go b/model/trash_model.go index ba44939..63884a6 100644 --- a/model/trash_model.go +++ b/model/trash_model.go @@ -4,19 +4,21 @@ import "time" type TrashCategory struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - Name string `gorm:"not null" json:"name"` - Icon string `json:"icon,omitempty"` + Name string `gorm:"not null" json:"trash_name"` + IconTrash string `json:"trash_icon,omitempty"` EstimatedPrice float64 `gorm:"not null" json:"estimated_price"` - Details []TrashDetail `gorm:"foreignKey:CategoryID;constraint:OnDelete:CASCADE;" json:"details"` + Variety string `gorm:"not null" json:"variety"` + Details []TrashDetail `gorm:"foreignKey:TrashCategoryID;constraint:OnDelete:CASCADE;" json:"trash_detail"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } type TrashDetail struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` - CategoryID string `gorm:"type:uuid;not null" json:"category_id"` - Description string `gorm:"not null" json:"description"` - Price float64 `gorm:"not null" json:"price"` - 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()" json:"trashdetail_id"` + TrashCategoryID string `gorm:"type:uuid;not null" json:"category_id"` + IconTrashDetail string `json:"trashdetail_icon,omitempty"` + Description string `gorm:"not null" json:"description"` + StepOrder int `gorm:"not null" json:"step_order"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/user_model.go b/model/user_model.go index c6fecf7..7890cb1 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,19 +3,20 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Name string `gorm:"not null" json:"name"` - Gender string `gorm:"not null" json:"gender"` - Dateofbirth string `gorm:"not null" json:"dateofbirth"` - Placeofbirth string `gorm:"not null" json:"placeofbirth"` - Phone string `gorm:"not null" json:"phone"` - Email string `json:"email,omitempty"` - PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` - Password string `json:"password,omitempty"` - RoleID string `gorm:"not null" json:"roleId"` - Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` - RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` - 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" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + Gender string `gorm:"not null" json:"gender"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Phone string `gorm:"not null;index" json:"phone"` + Email string `json:"email,omitempty"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + Password string `json:"password,omitempty"` + RoleID string `gorm:"not null" json:"roleId"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` + RegistrationProgress int8 `json:"registration_progress"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/model/userpin_model.go b/model/userpin_model.go index 0ad17be..8d4f876 100644 --- a/model/userpin_model.go +++ b/model/userpin_model.go @@ -5,6 +5,7 @@ import "time" type UserPin struct { ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"-"` Pin string `gorm:"not null" json:"pin"` CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` diff --git a/presentation/about_route.go b/presentation/about_route.go index a803038..78871ed 100644 --- a/presentation/about_route.go +++ b/presentation/about_route.go @@ -2,7 +2,7 @@ package presentation import ( "rijig/config" - "rijig/internal/handler" + "rijig/internal/about" "rijig/internal/repositories" "rijig/internal/services" "rijig/middleware" @@ -14,22 +14,22 @@ import ( func AboutRouter(api fiber.Router) { aboutRepo := repositories.NewAboutRepository(config.DB) aboutService := services.NewAboutService(aboutRepo) - aboutHandler := handler.NewAboutHandler(aboutService) + aboutHandler := about.NewAboutHandler(aboutService) aboutRoutes := api.Group("/about") - aboutRoutes.Use(middleware.AuthMiddleware) + aboutRoutes.Use(middleware.AuthMiddleware()) aboutRoutes.Get("/", aboutHandler.GetAllAbout) aboutRoutes.Get("/:id", aboutHandler.GetAboutByID) aboutRoutes.Post("/", aboutHandler.CreateAbout) // admin - aboutRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAbout) + aboutRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAbout) aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) // admin aboutDetailRoutes := api.Group("/about-detail") - aboutDetailRoutes.Use(middleware.AuthMiddleware) + aboutDetailRoutes.Use(middleware.AuthMiddleware()) aboutDetailRoute := api.Group("/about-detail") aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin - aboutDetailRoutes.Put("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) - aboutDetailRoutes.Delete("/:id", middleware.RoleMiddleware(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) + aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) + aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) } diff --git a/presentation/address_route.go b/presentation/address_route.go index f9038be..6b563a8 100644 --- a/presentation/address_route.go +++ b/presentation/address_route.go @@ -18,9 +18,9 @@ func AddressRouter(api fiber.Router) { adddressAPI := api.Group("/user/address") - adddressAPI.Post("/create-address", middleware.AuthMiddleware, addressHandler.CreateAddress) - adddressAPI.Get("/get-address", middleware.AuthMiddleware, addressHandler.GetAddressByUserID) - adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware, addressHandler.GetAddressByID) - adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware, addressHandler.UpdateAddress) - adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware, addressHandler.DeleteAddress) + adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress) + adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID) + adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID) + adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress) + adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware(), addressHandler.DeleteAddress) } diff --git a/presentation/article_route.go b/presentation/article_route.go index 1606b01..1a741f2 100644 --- a/presentation/article_route.go +++ b/presentation/article_route.go @@ -18,9 +18,9 @@ func ArticleRouter(api fiber.Router) { articleAPI := api.Group("/article-rijik") - articleAPI.Post("/create-article", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.CreateArticle) + articleAPI.Post("/create-article", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.CreateArticle) articleAPI.Get("/view-article", articleHandler.GetAllArticles) articleAPI.Get("/view-article/:article_id", articleHandler.GetArticleByID) - articleAPI.Put("/update-article/:article_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.UpdateArticle) - articleAPI.Delete("/delete-article/:article_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), articleHandler.DeleteArticle) + articleAPI.Put("/update-article/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.UpdateArticle) + articleAPI.Delete("/delete-article/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.DeleteArticle) } diff --git a/presentation/auth/auth_admin_route.go b/presentation/auth/auth_admin_route.go index f77535e..3d20de9 100644 --- a/presentation/auth/auth_admin_route.go +++ b/presentation/auth/auth_admin_route.go @@ -1,5 +1,5 @@ package presentation - +/* import ( "log" "os" @@ -33,3 +33,4 @@ func AuthAdminRouter(api fiber.Router) { adminAuthAPI.Post("/login", adminAuthHandler.LoginAdmin) adminAuthAPI.Post("/logout", middleware.AuthMiddleware, adminAuthHandler.LogoutAdmin) } + */ \ No newline at end of file diff --git a/presentation/auth/auth_masyarakat_route.go b/presentation/auth/auth_masyarakat_route.go index dbf4e7a..c5f8af7 100644 --- a/presentation/auth/auth_masyarakat_route.go +++ b/presentation/auth/auth_masyarakat_route.go @@ -1,5 +1,5 @@ package presentation - +/* import ( "rijig/config" handler "rijig/internal/handler/auth" @@ -24,3 +24,4 @@ func AuthMasyarakatRouter(api fiber.Router) { authMasyarakat.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) authMasyarakat.Post("/verify-otp", authHandler.VerifyOTPHandler) } + */ \ No newline at end of file diff --git a/presentation/auth/auth_pengelola_route.go b/presentation/auth/auth_pengelola_route.go index 358b244..efcd9e1 100644 --- a/presentation/auth/auth_pengelola_route.go +++ b/presentation/auth/auth_pengelola_route.go @@ -1,5 +1,5 @@ package presentation - +/* import ( "rijig/config" handler "rijig/internal/handler/auth" @@ -24,3 +24,4 @@ func AuthPengelolaRouter(api fiber.Router) { authPengelola.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) authPengelola.Post("/verify-otp", authHandler.VerifyOTPHandler) } + */ \ No newline at end of file diff --git a/presentation/auth/auth_pengepul_route.go b/presentation/auth/auth_pengepul_route.go index 1f60f2d..d796da4 100644 --- a/presentation/auth/auth_pengepul_route.go +++ b/presentation/auth/auth_pengepul_route.go @@ -1,5 +1,5 @@ package presentation - +/* import ( "rijig/config" handler "rijig/internal/handler/auth" @@ -24,3 +24,4 @@ func AuthPengepulRouter(api fiber.Router) { authPengepul.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) authPengepul.Post("/verify-otp", authHandler.VerifyOTPHandler) } + */ \ No newline at end of file diff --git a/presentation/banner_route.go b/presentation/banner_route.go deleted file mode 100644 index 411b0f0..0000000 --- a/presentation/banner_route.go +++ /dev/null @@ -1,26 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func BannerRouter(api fiber.Router) { - bannerRepo := repositories.NewBannerRepository(config.DB) - bannerService := services.NewBannerService(bannerRepo) - BannerHandler := handler.NewBannerHandler(bannerService) - - bannerAPI := api.Group("/banner-rijik") - - bannerAPI.Post("/create-banner", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.CreateBanner) - bannerAPI.Get("/getall-banner", BannerHandler.GetAllBanners) - bannerAPI.Get("/get-banner/:banner_id", BannerHandler.GetBannerByID) - bannerAPI.Put("/update-banner/:banner_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.UpdateBanner) - bannerAPI.Delete("/delete-banner/:banner_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), BannerHandler.DeleteBanner) -} diff --git a/presentation/cart_router.go b/presentation/cart_router.go index 2066cca..05d66fb 100644 --- a/presentation/cart_router.go +++ b/presentation/cart_router.go @@ -17,7 +17,7 @@ func TrashCartRouter(api fiber.Router) { cartHandler := handler.NewCartHandler(cartService) cart := api.Group("/cart") - cart.Use(middleware.AuthMiddleware) + cart.Use(middleware.AuthMiddleware()) cart.Get("/", cartHandler.GetCart) cart.Post("/item", cartHandler.AddOrUpdateItem) diff --git a/presentation/collector_route.go b/presentation/collector_route.go index d31f741..a5f9b17 100644 --- a/presentation/collector_route.go +++ b/presentation/collector_route.go @@ -27,7 +27,7 @@ func CollectorRouter(api fiber.Router) { collectorHandler := handler.NewCollectorHandler(collectorService) collectors := api.Group("/collectors") - collectors.Use(middleware.AuthMiddleware) + collectors.Use(middleware.AuthMiddleware()) collectors.Post("/", collectorHandler.CreateCollector) collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) diff --git a/presentation/company_profile_route.go b/presentation/company_profile_route.go index 013a463..709ab0b 100644 --- a/presentation/company_profile_route.go +++ b/presentation/company_profile_route.go @@ -17,7 +17,7 @@ func CompanyProfileRouter(api fiber.Router) { companyProfileHandler := handler.NewCompanyProfileHandler(companyProfileService) companyProfileAPI := api.Group("/company-profile") - companyProfileAPI.Use(middleware.AuthMiddleware) + companyProfileAPI.Use(middleware.AuthMiddleware()) companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile) companyProfileAPI.Get("/get/:company_id", companyProfileHandler.GetCompanyProfileByID) diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go index 16a00a2..39301b2 100644 --- a/presentation/identitycard_route.go +++ b/presentation/identitycard_route.go @@ -1,5 +1,5 @@ package presentation - +/* import ( "rijig/config" "rijig/internal/handler" @@ -26,3 +26,4 @@ func IdentityCardRouter(api fiber.Router) { identityCardApi.Put("/update/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.UpdateIdentityCard) identityCardApi.Delete("/delete/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.DeleteIdentityCard) } + */ \ No newline at end of file diff --git a/presentation/initialcoint_route.go b/presentation/initialcoint_route.go deleted file mode 100644 index fba9237..0000000 --- a/presentation/initialcoint_route.go +++ /dev/null @@ -1,28 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func InitialCointRoute(api fiber.Router) { - - initialCointRepo := repositories.NewInitialCointRepository(config.DB) - initialCointService := services.NewInitialCointService(initialCointRepo) - initialCointHandler := handler.NewInitialCointHandler(initialCointService) - - initialCoint := api.Group("/initialcoint") - initialCoint.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) - - initialCoint.Post("/create", initialCointHandler.CreateInitialCoint) - initialCoint.Get("/getall", initialCointHandler.GetAllInitialCoints) - initialCoint.Get("/get/:coin_id", initialCointHandler.GetInitialCointByID) - initialCoint.Put("/update/:coin_id", initialCointHandler.UpdateInitialCoint) - initialCoint.Delete("/delete/:coin_id", initialCointHandler.DeleteInitialCoint) -} diff --git a/presentation/pickup_matching_route.go b/presentation/pickup_matching_route.go index 025252b..a389640 100644 --- a/presentation/pickup_matching_route.go +++ b/presentation/pickup_matching_route.go @@ -16,10 +16,10 @@ func PickupMatchingRouter(api fiber.Router) { handler := handler.NewPickupMatchingHandler(service) manual := api.Group("/pickup/manual") - manual.Use(middleware.AuthMiddleware) + manual.Use(middleware.AuthMiddleware()) manual.Get("/:pickupID/nearby-collectors", handler.GetNearbyCollectorsForPickup) auto := api.Group("/pickup/otomatis") - auto.Use(middleware.AuthMiddleware) + auto.Use(middleware.AuthMiddleware()) auto.Get("/available-requests", handler.GetAvailablePickupForCollector) } diff --git a/presentation/product_route.go b/presentation/product_route.go deleted file mode 100644 index c1a74e6..0000000 --- a/presentation/product_route.go +++ /dev/null @@ -1,36 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func ProductRouter(api fiber.Router) { - productRepo := repositories.NewProductRepository(config.DB) - storeRepo := repositories.NewStoreRepository(config.DB) - productService := services.NewProductService(productRepo, storeRepo) - productHandler := handler.NewProductHandler(productService) - - productAPI := api.Group("/productinstore") - - productAPI.Post("/add-product", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.CreateProduct) - - productAPI.Get("/getproductbyuser", middleware.AuthMiddleware, productHandler.GetAllProductsByStoreID) - productAPI.Get("getproduct/:product_id", middleware.AuthMiddleware, productHandler.GetProductByID) - - productAPI.Put("updateproduct/:product_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.UpdateProduct) - - productAPI.Delete("/delete/:product_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProduct) - - productAPI.Delete("/delete-products", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProducts) - - productAPI.Delete("/delete-image/:image_id", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProductImage) - - productAPI.Delete("/delete-images", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator, utils.RolePengelola, utils.RolePengepul), productHandler.DeleteProductImages) -} diff --git a/presentation/rating_route.go b/presentation/rating_route.go index a86d8ec..6d801aa 100644 --- a/presentation/rating_route.go +++ b/presentation/rating_route.go @@ -15,7 +15,7 @@ func PickupRatingRouter(api fiber.Router) { ratingHandler := handler.NewPickupRatingHandler(ratingService) rating := api.Group("/pickup") - rating.Use(middleware.AuthMiddleware) + rating.Use(middleware.AuthMiddleware()) rating.Post("/:id/rating", ratingHandler.CreateRating) collector := api.Group("/collector") diff --git a/presentation/request_pickup_route.go b/presentation/request_pickup_route.go index 1facfff..81c6681 100644 --- a/presentation/request_pickup_route.go +++ b/presentation/request_pickup_route.go @@ -24,7 +24,7 @@ func RequestPickupRouter(api fiber.Router) { statuspickupHandler := handler.NewPickupStatusHistoryHandler(historyService) reqpickup := api.Group("/reqpickup") - reqpickup.Use(middleware.AuthMiddleware) + reqpickup.Use(middleware.AuthMiddleware()) reqpickup.Post("/manual", pickupHandler.CreateRequestPickup) reqpickup.Get("/pickup/:id/history", statuspickupHandler.GetStatusHistory) diff --git a/presentation/role_route.go b/presentation/role_route.go index b221e8f..6ec230a 100644 --- a/presentation/role_route.go +++ b/presentation/role_route.go @@ -1,19 +1,19 @@ package presentation -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" +// import ( +// "rijig/config" +// "rijig/internal/handler" +// "rijig/internal/repositories" +// "rijig/internal/services" - "github.com/gofiber/fiber/v2" -) +// "github.com/gofiber/fiber/v2" +// ) -func RoleRouter(api fiber.Router) { - roleRepo := repositories.NewRoleRepository(config.DB) - roleService := services.NewRoleService(roleRepo) - roleHandler := handler.NewRoleHandler(roleService) +// func RoleRouter(api fiber.Router) { +// roleRepo := repositories.NewRoleRepository(config.DB) +// roleService := services.NewRoleService(roleRepo) +// roleHandler := handler.NewRoleHandler(roleService) - api.Get("/roles", roleHandler.GetRoles) - api.Get("/role/:role_id", roleHandler.GetRoleByID) -} +// api.Get("/roles", roleHandler.GetRoles) +// api.Get("/role/:role_id", roleHandler.GetRoleByID) +// } diff --git a/presentation/store_route.go b/presentation/store_route.go deleted file mode 100644 index fd6c6e9..0000000 --- a/presentation/store_route.go +++ /dev/null @@ -1,26 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -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, 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) -} diff --git a/presentation/trash_route.go b/presentation/trash_route.go index 1d738fa..bef7b83 100644 --- a/presentation/trash_route.go +++ b/presentation/trash_route.go @@ -17,17 +17,17 @@ func TrashRouter(api fiber.Router) { trashHandler := handler.NewTrashHandler(trashService) trashAPI := api.Group("/trash") - trashAPI.Use(middleware.AuthMiddleware) + trashAPI.Use(middleware.AuthMiddleware()) - trashAPI.Post("/category", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.CreateCategory) - trashAPI.Post("/category/detail", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.AddDetailToCategory) + trashAPI.Post("/category", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.CreateCategory) + trashAPI.Post("/category/detail", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.AddDetailToCategory) trashAPI.Get("/categories", trashHandler.GetCategories) trashAPI.Get("/category/:category_id", trashHandler.GetCategoryByID) trashAPI.Get("/detail/:detail_id", trashHandler.GetTrashDetailByID) - trashAPI.Patch("/category/:category_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateCategory) - trashAPI.Put("/detail/:detail_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.UpdateDetail) + trashAPI.Patch("/category/:category_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.UpdateCategory) + trashAPI.Put("/detail/:detail_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.UpdateDetail) - trashAPI.Delete("/category/:category_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteCategory) - trashAPI.Delete("/detail/:detail_id", middleware.RoleMiddleware(utils.RoleAdministrator), trashHandler.DeleteDetail) + trashAPI.Delete("/category/:category_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.DeleteCategory) + trashAPI.Delete("/detail/:detail_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.DeleteDetail) } diff --git a/presentation/user_route.go b/presentation/user_route.go index afe7d3e..c008ace 100644 --- a/presentation/user_route.go +++ b/presentation/user_route.go @@ -17,13 +17,13 @@ func UserProfileRouter(api fiber.Router) { userProfilRoute := api.Group("/user") - userProfilRoute.Get("/info", middleware.AuthMiddleware, userProfileHandler.GetUserByIDHandler) + userProfilRoute.Get("/info", middleware.AuthMiddleware(), userProfileHandler.GetUserByIDHandler) - userProfilRoute.Get("/show-all", middleware.AuthMiddleware, userProfileHandler.GetAllUsersHandler) + userProfilRoute.Get("/show-all", middleware.AuthMiddleware(), userProfileHandler.GetAllUsersHandler) // userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) // userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) - userProfilRoute.Put("/update-user", middleware.AuthMiddleware, userProfileHandler.UpdateUserHandler) - userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware, userProfileHandler.UpdateUserPasswordHandler) - userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware, userProfileHandler.UpdateUserAvatarHandler) + userProfilRoute.Put("/update-user", middleware.AuthMiddleware(), userProfileHandler.UpdateUserHandler) + userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware(), userProfileHandler.UpdateUserPasswordHandler) + userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware(), userProfileHandler.UpdateUserAvatarHandler) } diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go index c145907..9d6dbbb 100644 --- a/presentation/userpin_route.go +++ b/presentation/userpin_route.go @@ -1,24 +1,24 @@ package presentation import ( - "rijig/config" + /* "rijig/config" "rijig/internal/handler" "rijig/internal/repositories" "rijig/internal/services" - "rijig/middleware" + "rijig/middleware" */ "github.com/gofiber/fiber/v2" ) func UserPinRouter(api fiber.Router) { - userPinRepo := repositories.NewUserPinRepository(config.DB) + // userPinRepo := repositories.NewUserPinRepository(config.DB) - userPinService := services.NewUserPinService(userPinRepo) + // userPinService := services.NewUserPinService(userPinRepo) - userPinHandler := handler.NewUserPinHandler(userPinService) + // userPinHandler := handler.NewUserPinHandler(userPinService) - api.Post("/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) - api.Post("/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) - api.Get("/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) - api.Patch("/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) + // api.Post("/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) + // api.Post("/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) + // api.Get("/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) + // api.Patch("/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) } diff --git a/presentation/whatsapp_route.go b/presentation/whatsapp_route.go deleted file mode 100644 index dd9c14e..0000000 --- a/presentation/whatsapp_route.go +++ /dev/null @@ -1,13 +0,0 @@ -package presentation - -import ( - "rijig/internal/handler" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func WhatsAppRouter(api fiber.Router) { - api.Post("/logout/whastapp", middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator), handler.WhatsAppHandler) -} diff --git a/presentation/wilayahindonesia_route.go b/presentation/wilayahindonesia_route.go index 9eb1ffa..13bfa44 100644 --- a/presentation/wilayahindonesia_route.go +++ b/presentation/wilayahindonesia_route.go @@ -17,7 +17,7 @@ func WilayahRouter(api fiber.Router) { wilayahService := services.NewWilayahIndonesiaService(wilayahRepo) wilayahHandler := handler.NewWilayahImportHandler(wilayahService) - api.Post("/import/data-wilayah-indonesia", middleware.RoleMiddleware(utils.RoleAdministrator), wilayahHandler.ImportWilayahData) + api.Post("/import/data-wilayah-indonesia", middleware.RequireRoles(utils.RoleAdministrator), wilayahHandler.ImportWilayahData) wilayahAPI := api.Group("/wilayah-indonesia") diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 4a6e4ea..cdcb51f 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -3,9 +3,17 @@ package router import ( "os" + "rijig/internal/article" + "rijig/internal/authentication" + "rijig/internal/company" + "rijig/internal/identitycart" + "rijig/internal/role" + "rijig/internal/userpin" + "rijig/internal/whatsapp" "rijig/middleware" "rijig/presentation" - presentationn "rijig/presentation/auth" + + // presentationn "rijig/presentation/auth" "github.com/gofiber/fiber/v2" ) @@ -17,14 +25,22 @@ func SetupRoutes(app *fiber.App) { api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) + authentication.AuthenticationRouter(api) + identitycart.UserIdentityCardRoute(api) + company.CompanyRouter(api) + userpin.UsersPinRoute(api) + role.UserRoleRouter(api) + + article.ArticleRouter(api) + // || auth router || // // presentation.AuthRouter(api) - presentationn.AuthAdminRouter(api) - presentationn.AuthPengelolaRouter(api) - presentationn.AuthPengepulRouter(api) - presentationn.AuthMasyarakatRouter(api) + // presentationn.AuthAdminRouter(api) + // presentationn.AuthPengelolaRouter(api) + // presentationn.AuthPengepulRouter(api) + // presentationn.AuthMasyarakatRouter(api) // || auth router || // - presentation.IdentityCardRouter(api) + // presentation.IdentityCardRouter(api) presentation.CompanyProfileRouter(api) presentation.RequestPickupRouter(api) presentation.PickupMatchingRouter(api) @@ -35,17 +51,12 @@ func SetupRoutes(app *fiber.App) { presentation.UserProfileRouter(api) presentation.UserPinRouter(api) - presentation.RoleRouter(api) + // presentation.RoleRouter(api) presentation.WilayahRouter(api) presentation.AddressRouter(api) - presentation.ArticleRouter(api) - presentation.BannerRouter(api) - presentation.InitialCointRoute(api) + // presentation.ArticleRouter(api) presentation.AboutRouter(api) presentation.TrashRouter(api) presentation.CoverageAreaRouter(api) - presentation.StoreRouter(api) - presentation.ProductRouter(api) - presentation.WhatsAppRouter(api) - + whatsapp.WhatsAppRouter(api) } diff --git a/utils/api_response.go b/utils/api_response.go new file mode 100644 index 0000000..ea94afd --- /dev/null +++ b/utils/api_response.go @@ -0,0 +1,102 @@ +package utils + +import ( + "github.com/gofiber/fiber/v2" +) + +type Meta struct { + Status int `json:"status"` + Message string `json:"message"` + Page *int `json:"page,omitempty"` + Limit *int `json:"limit,omitempty"` +} + +type Response struct { + Meta Meta `json:"meta"` + Data interface{} `json:"data,omitempty"` +} + +func ResponseMeta(c *fiber.Ctx, status int, message string) error { + response := Response{ + Meta: Meta{ + Status: status, + Message: message, + }, + } + return c.Status(status).JSON(response) +} + +func ResponseData(c *fiber.Ctx, status int, message string, data interface{}) error { + response := Response{ + Meta: Meta{ + Status: status, + Message: message, + }, + Data: data, + } + return c.Status(status).JSON(response) +} + +func ResponsePagination(c *fiber.Ctx, status int, message string, data interface{}, page, limit int) error { + response := Response{ + Meta: Meta{ + Status: status, + Message: message, + Page: &page, + Limit: &limit, + }, + Data: data, + } + return c.Status(status).JSON(response) +} + +func ResponseErrorData(c *fiber.Ctx, status int, message string, errors interface{}) error { + type ResponseWithErrors struct { + Meta Meta `json:"meta"` + Errors interface{} `json:"errors"` + } + response := ResponseWithErrors{ + Meta: Meta{ + Status: status, + Message: message, + }, + Errors: errors, + } + return c.Status(status).JSON(response) +} + +func Success(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusOK, message) +} + +func SuccessWithData(c *fiber.Ctx, message string, data interface{}) error { + return ResponseData(c, fiber.StatusOK, message, data) +} + +func CreateSuccessWithData(c *fiber.Ctx, message string, data interface{}) error { + return ResponseData(c, fiber.StatusCreated, message, data) +} + +func SuccessWithPagination(c *fiber.Ctx, message string, data interface{}, page, limit int) error { + return ResponsePagination(c, fiber.StatusOK, message, data, page, limit) +} + +func BadRequest(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusBadRequest, message) +} + +func NotFound(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusNotFound, message) +} + +func InternalServerError(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusInternalServerError, message) +} + +func Unauthorized(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusUnauthorized, message) +} + +func Forbidden(c *fiber.Ctx, message string) error { + return ResponseMeta(c, fiber.StatusForbidden, message) +} diff --git a/utils/identity_number_validator.go b/utils/identity_number_validator.go new file mode 100644 index 0000000..b5ecf53 --- /dev/null +++ b/utils/identity_number_validator.go @@ -0,0 +1,95 @@ +package utils + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" +) + +var Wilayah = `{"provinsi":{"11":"ACEH","12":"SUMATERA UTARA","13":"SUMATERA BARAT","14":"RIAU","15":"JAMBI","16":"SUMATERA SELATAN","17":"BENGKULU","18":"LAMPUNG","19":"KEPULAUAN BANGKA BELITUNG","21":"KEPULAUAN RIAU","31":"DKI JAKARTA","32":"JAWA BARAT","33":"JAWA TENGAH","34":"DAERAH ISTIMEWA YOGYAKARTA","35":"JAWA TIMUR","36":"BANTEN","51":"BALI","52":"NUSA TENGGARA BARAT","53":"NUSA TENGGARA TIMUR","61":"KALIMANTAN BARAT","62":"KALIMANTAN TENGAH","63":"KALIMANTAN SELATAN","64":"KALIMANTAN TIMUR","65":"KALIMANTAN UTARA","71":"SULAWESI UTARA","72":"SULAWESI TENGAH","73":"SULAWESI SELATAN","74":"SULAWESI TENGGARA","75":"GORONTALO","76":"SULAWESI BARAT","81":"MALUKU","82":"MALUKU UTARA","91":"P A P U A","92":"PAPUA BARAT"},"kabkot":{"1101":"KAB. ACEH SELATAN","1102":"KAB. ACEH TENGGARA","1103":"KAB. ACEH TIMUR","1104":"KAB. ACEH TENGAH","1105":"KAB. ACEH BARAT","1106":"KAB. ACEH BESAR","1107":"KAB. PIDIE","1108":"KAB. ACEH UTARA","1109":"KAB. SIMEULUE","1110":"KAB. ACEH SINGKIL","1111":"KAB. BIREUEN","1112":"KAB. ACEH BARAT DAYA","1113":"KAB. GAYO LUES","1114":"KAB. ACEH JAYA","1115":"KAB. NAGAN RAYA","1116":"KAB. ACEH TAMIANG","1117":"KAB. BENER MERIAH","1118":"KAB. PIDIE JAYA","1171":"KOTA BANDA ACEH","1172":"KOTA SABANG","1173":"KOTA LHOKSEUMAWE","1174":"KOTA LANGSA","1175":"KOTA SUBULUSSALAM","1201":"KAB. TAPANULI TENGAH","1202":"KAB. TAPANULI UTARA","1203":"KAB. TAPANULI SELATAN","1204":"KAB. NIAS","1205":"KAB. LANGKAT","1206":"KAB. KARO","1207":"KAB. DELI SERDANG","1208":"KAB. SIMALUNGUN","1209":"KAB. ASAHAN","1210":"KAB. LABUHANBATU","1211":"KAB. DAIRI","1212":"KAB. TOBA SAMOSIR","1213":"KAB. MANDAILING NATAL","1214":"KAB. NIAS SELATAN","1215":"KAB. PAKPAK BHARAT","1216":"KAB. HUMBANG HASUNDUTAN","1217":"KAB. SAMOSIR","1218":"KAB. SERDANG BEDAGAI","1219":"KAB. BATU BARA","1220":"KAB. PADANG LAWAS UTARA","1221":"KAB. PADANG LAWAS","1222":"KAB. LABUHANBATU SELATAN","1223":"KAB. LABUHANBATU UTARA","1224":"KAB. NIAS UTARA","1225":"KAB. NIAS BARAT","1271":"KOTA MEDAN","1272":"KOTA PEMATANG SIANTAR","1273":"KOTA SIBOLGA","1274":"KOTA TANJUNG BALAI","1275":"KOTA BINJAI","1276":"KOTA TEBING TINGGI","1277":"KOTA PADANGSIDIMPUAN","1278":"KOTA GUNUNGSITOLI","1301":"KAB. PESISIR SELATAN","1302":"KAB. SOLOK","1303":"KAB. SIJUNJUNG","1304":"KAB. TANAH DATAR","1305":"KAB. PADANG PARIAMAN","1306":"KAB. AGAM","1307":"KAB. LIMA PULUH KOTA","1308":"KAB. PASAMAN","1309":"KAB. KEPULAUAN MENTAWAI","1310":"KAB. DHARMASRAYA","1311":"KAB. SOLOK SELATAN","1312":"KAB. PASAMAN BARAT","1371":"KOTA PADANG","1372":"KOTA SOLOK","1373":"KOTA SAWAHLUNTO","1374":"KOTA PADANG PANJANG","1375":"KOTA BUKITTINGGI","1376":"KOTA PAYAKUMBUH","1377":"KOTA PARIAMAN","1401":"KAB. KAMPAR","1402":"KAB. INDRAGIRI HULU","1403":"KAB. BENGKALIS","1404":"KAB. INDRAGIRI HILIR","1405":"KAB. PELALAWAN","1406":"KAB. ROKAN HULU","1407":"KAB. ROKAN HILIR","1408":"KAB. SIAK","1409":"KAB. KUANTAN SINGINGI","1410":"KAB. KEPULAUAN MERANTI","1471":"KOTA PEKANBARU","1472":"KOTA DUMAI","1501":"KAB. KERINCI","1502":"KAB. MERANGIN","1503":"KAB. SAROLANGUN","1504":"KAB. BATANGHARI","1505":"KAB. MUARO JAMBI","1506":"KAB. TANJUNG JABUNG BARAT","1507":"KAB. TANJUNG JABUNG TIMUR","1508":"KAB. BUNGO","1509":"KAB. TEBO","1571":"KOTA JAMBI","1572":"KOTA SUNGAI PENUH","1601":"KAB. OGAN KOMERING ULU","1602":"KAB. OGAN KOMERING ILIR","1603":"KAB. MUARA ENIM","1604":"KAB. LAHAT","1605":"KAB. MUSI RAWAS","1606":"KAB. MUSI BANYUASIN","1607":"KAB. BANYUASIN","1608":"KAB. OGAN KOMERING ULU TIMU","1609":"KAB. OGAN KOMERING ULU SELAT","1610":"KAB. OGAN ILIR","1611":"KAB. EMPAT LAWANG","1612":"KAB. PENUKAL ABAB LEMATANG I","1613":"KAB. MUSI RAWAS UTARA","1671":"KOTA PALEMBANG","1672":"KOTA PAGAR ALAM","1673":"KOTA LUBUK LINGGAU","1674":"KOTA PRABUMULIH","1701":"KAB. BENGKULU SELATAN","1702":"KAB. REJANG LEBONG","1703":"KAB. BENGKULU UTARA","1704":"KAB. KAUR","1705":"KAB. SELUMA","1706":"KAB. MUKO MUKO","1707":"KAB. LEBONG","1708":"KAB. KEPAHIANG","1709":"KAB. BENGKULU TENGAH","1771":"KOTA BENGKULU","1801":"KAB. LAMPUNG SELATAN","1802":"KAB. LAMPUNG TENGAH","1803":"KAB. LAMPUNG UTARA","1804":"KAB. LAMPUNG BARAT","1805":"KAB. TULANG BAWANG","1806":"KAB. TANGGAMUS","1807":"KAB. LAMPUNG TIMUR","1808":"KAB. WAY KANAN","1809":"KAB. PESAWARAN","1810":"KAB. PRINGSEWU","1811":"KAB. MESUJI","1812":"KAB. TULANG BAWANG BARAT","1813":"KAB. PESISIR BARAT","1871":"KOTA BANDAR LAMPUNG","1872":"KOTA METRO","1901":"KAB. BANGKA","1902":"KAB. BELITUNG","1903":"KAB. BANGKA SELATAN","1904":"KAB. BANGKA TENGAH","1905":"KAB. BANGKA BARAT","1906":"KAB. BELITUNG TIMUR","1971":"KOTA PANGKAL PINANG","2101":"KAB. BINTAN","2102":"KAB. KARIMUN","2103":"KAB. NATUNA","2104":"KAB. LINGGA","2105":"KAB. KEPULAUAN ANAMBAS","2171":"KOTA BATAM","2172":"KOTA TANJUNG PINANG","3101":"KAB. ADM. KEP. SERIBU","3171":"KOTA ADM. JAKARTA PUSAT","3172":"KOTA ADM. JAKARTA UTARA","3173":"KOTA ADM. JAKARTA BARAT","3174":"KOTA ADM. JAKARTA SELATAN","3175":"KOTA ADM. JAKARTA TIMUR","3201":"KAB. BOGOR","3202":"KAB. SUKABUMI","3203":"KAB. CIANJUR","3204":"KAB. BANDUNG","3205":"KAB. GARUT","3206":"KAB. TASIKMALAYA","3207":"KAB. CIAMIS","3208":"KAB. KUNINGAN","3209":"KAB. CIREBON","3210":"KAB. MAJALENGKA","3211":"KAB. SUMEDANG","3212":"KAB. INDRAMAYU","3213":"KAB. SUBANG","3214":"KAB. PURWAKARTA","3215":"KAB. KARAWANG","3216":"KAB. BEKASI","3217":"KAB. BANDUNG BARAT","3218":"KAB. PANGANDARAN","3271":"KOTA BOGOR","3272":"KOTA SUKABUMI","3273":"KOTA BANDUNG","3274":"KOTA CIREBON","3275":"KOTA BEKASI","3276":"KOTA DEPOK","3277":"KOTA CIMAHI","3278":"KOTA TASIKMALAYA","3279":"KOTA BANJAR","3301":"KAB. CILACAP","3302":"KAB. BANYUMAS","3303":"KAB. PURBALINGGA","3304":"KAB. BANJARNEGARA","3305":"KAB. KEBUMEN","3306":"KAB. PURWOREJO","3307":"KAB. WONOSOBO","3308":"KAB. MAGELANG","3309":"KAB. BOYOLALI","3310":"KAB. KLATEN","3311":"KAB. SUKOHARJO","3312":"KAB. WONOGIRI","3313":"KAB. KARANGANYAR","3314":"KAB. SRAGEN","3315":"KAB. GROBOGAN","3316":"KAB. BLORA","3317":"KAB. REMBANG","3318":"KAB. PATI","3319":"KAB. KUDUS","3320":"KAB. JEPARA","3321":"KAB. DEMAK","3322":"KAB. SEMARANG","3323":"KAB. TEMANGGUNG","3324":"KAB. KENDAL","3325":"KAB. BATANG","3326":"KAB. PEKALONGAN","3327":"KAB. PEMALANG","3328":"KAB. TEGAL","3329":"KAB. BREBES","3371":"KOTA MAGELANG","3372":"KOTA SURAKARTA","3373":"KOTA SALATIGA","3374":"KOTA SEMARANG","3375":"KOTA PEKALONGAN","3376":"KOTA TEGAL","3401":"KAB. KULON PROGO","3402":"KAB. BANTUL","3403":"KAB. GUNUNG KIDUL","3404":"KAB. SLEMAN","3471":"KOTA YOGYAKARTA","3501":"KAB. PACITAN","3502":"KAB. PONOROGO","3503":"KAB. TRENGGALEK","3504":"KAB. TULUNGAGUNG","3505":"KAB. BLITAR","3506":"KAB. KEDIRI","3507":"KAB. MALANG","3508":"KAB. LUMAJANG","3509":"KAB. JEMBER","3510":"KAB. BANYUWANGI","3511":"KAB. BONDOWOSO","3512":"KAB. SITUBONDO","3513":"KAB. PROBOLINGGO","3514":"KAB. PASURUAN","3515":"KAB. SIDOARJO","3516":"KAB. MOJOKERTO","3517":"KAB. JOMBANG","3518":"KAB. NGANJUK","3519":"KAB. MADIUN","3520":"KAB. MAGETAN","3521":"KAB. NGAWI","3522":"KAB. BOJONEGORO","3523":"KAB. TUBAN","3524":"KAB. LAMONGAN","3525":"KAB. GRESIK","3526":"KAB. BANGKALAN","3527":"KAB. SAMPANG","3528":"KAB. PAMEKASAN","3529":"KAB. SUMENEP","3571":"KOTA KEDIRI","3572":"KOTA BLITAR","3573":"KOTA MALANG","3574":"KOTA PROBOLINGGO","3575":"KOTA PASURUAN","3576":"KOTA MOJOKERTO","3577":"KOTA MADIUN","3578":"KOTA SURABAYA","3579":"KOTA BATU","3601":"KAB. PANDEGLANG","3602":"KAB. LEBAK","3603":"KAB. TANGERANG","3604":"KAB. SERANG","3671":"KOTA TANGERANG","3672":"KOTA CILEGON","3673":"KOTA SERANG","3674":"KOTA TANGERANG SELATAN","5101":"KAB. JEMBRANA","5102":"KAB. TABANAN","5103":"KAB. BADUNG","5104":"KAB. GIANYAR","5105":"KAB. KLUNGKUNG","5106":"KAB. BANGLI","5107":"KAB. KARANGASEM","5108":"KAB. BULELENG","5171":"KOTA DENPASAR","5201":"KAB. LOMBOK BARAT","5202":"KAB. LOMBOK TENGAH","5203":"KAB. LOMBOK TIMUR","5204":"KAB. SUMBAWA","5205":"KAB. DOMPU","5206":"KAB. BIMA","5207":"KAB. SUMBAWA BARAT","5208":"KAB. LOMBOK UTARA","5271":"KOTA MATARAM","5272":"KOTA BIMA","5301":"KAB. KUPANG","5302":"KAB TIMOR TENGAH SELATAN","5303":"KAB. TIMOR TENGAH UTARA","5304":"KAB. BELU","5305":"KAB. ALOR","5306":"KAB. FLORES TIMUR","5307":"KAB. SIKKA","5308":"KAB. ENDE","5309":"KAB. NGADA","5310":"KAB. MANGGARAI","5311":"KAB. SUMBA TIMUR","5312":"KAB. SUMBA BARAT","5313":"KAB. LEMBATA","5314":"KAB. ROTE NDAO","5315":"KAB. MANGGARAI BARAT","5316":"KAB. NAGEKEO","5317":"KAB. SUMBA TENGAH","5318":"KAB. SUMBA BARAT DAYA","5319":"KAB. MANGGARAI TIMUR","5320":"KAB. SABU RAIJUA","5321":"KAB. MALAKA","5371":"KOTA KUPANG","6101":"KAB. SAMBAS","6102":"KAB. MEMPAWAH","6103":"KAB. SANGGAU","6104":"KAB. KETAPANG","6105":"KAB. SINTANG","6106":"KAB. KAPUAS HULU","6107":"KAB. BENGKAYANG","6108":"KAB. LANDAK","6109":"KAB. SEKADAU","6110":"KAB. MELAWI","6111":"KAB. KAYONG UTARA","6112":"KAB. KUBU RAYA","6171":"KOTA PONTIANAK","6172":"KOTA SINGKAWANG","6201":"KAB. KOTAWARINGIN BARAT","6202":"KAB. KOTAWARINGIN TIMUR","6203":"KAB. KAPUAS","6204":"KAB. BARITO SELATAN","6205":"KAB. BARITO UTARA","6206":"KAB. KATINGAN","6207":"KAB. SERUYAN","6208":"KAB. SUKAMARA","6209":"KAB. LAMANDAU","6210":"KAB. GUNUNG MAS","6211":"KAB. PULANG PISAU","6212":"KAB. MURUNG RAYA","6213":"KAB. BARITO TIMUR","6271":"KOTA PALANGKARAYA","6301":"KAB. TANAH LAUT","6302":"KAB. KOTABARU","6303":"KAB. BANJAR","6304":"KAB. BARITO KUALA","6305":"KAB. TAPIN","6306":"KAB. HULU SUNGAI SELATAN","6307":"KAB. HULU SUNGAI TENGAH","6308":"KAB. HULU SUNGAI UTARA","6309":"KAB. TABALONG","6310":"KAB. TANAH BUMBU","6311":"KAB. BALANGAN","6371":"KOTA BANJARMASIN","6372":"KOTA BANJARBARU","6401":"KAB. PASER","6402":"KAB. KUTAI KARTANEGARA","6403":"KAB. BERAU","6407":"KAB. KUTAI BARAT","6408":"KAB. KUTAI TIMUR","6409":"KAB. PENAJAM PASER UTARA","6411":"KAB. MAHAKAM ULU","6471":"KOTA BALIKPAPAN","6472":"KOTA SAMARINDA","6474":"KOTA BONTANG","6501":"KAB. BULUNGAN","6502":"KAB. MALINAU","6503":"KAB. NUNUKAN","6504":"KAB. TANA TIDUNG","6571":"KOT. TARAKAN","7101":"KAB. BOLAANG MONGONDOW","7102":"KAB. MINAHASA","7103":"KAB. KEPULAUAN SANGIHE","7104":"KAB. KEPULAUAN TALAUD","7105":"KAB. MINAHASA SELATAN","7106":"KAB. MINAHASA UTARA","7107":"KAB. MINAHASA TENGGARA","7108":"KAB. BOLAANG MONGONDOW UT","7109":"KAB. KEP. SIAU TAGULANDANG B","7110":"KAB. BOLAANG MONGONDOW TI","7111":"KAB. BOLAANG MONGONDOW SE","7171":"KOTA MANADO","7172":"KOTA BITUNG","7173":"KOTA TOMOHON","7174":"KOTA KOTAMOBAGU","7201":"KAB. BANGGAI","7202":"KAB. POSO","7203":"KAB. DONGGALA","7204":"KAB. TOLI TOLI","7205":"KAB. BUOL","7206":"KAB. MOROWALI","7207":"KAB. BANGGAI KEPULAUAN","7208":"KAB. PARIGI MOUTONG","7209":"KAB. TOJO UNA UNA","7210":"KAB. SIGI","7211":"KAB. BANGGAI LAUT","7212":"KAB. MOROWALI UTARA","7271":"KOTA PALU","7301":"KAB. KEPULAUAN SELAYAR","7302":"KAB. BULUKUMBA","7303":"KAB. BANTAENG","7304":"KAB. JENEPONTO","7305":"KAB. TAKALAR","7306":"KAB. GOWA","7307":"KAB. SINJAI","7308":"KAB. BONE","7309":"KAB. MAROS","7310":"KAB. PANGKAJENE KEPULAUAN","7311":"KAB. BARRU","7312":"KAB. SOPPENG","7313":"KAB. WAJO","7314":"KAB. SIDENRENG RAPPANG","7315":"KAB. PINRANG","7316":"KAB. ENREKANG","7317":"KAB. LUWU","7318":"KAB. TANA TORAJA","7322":"KAB. LUWU UTARA","7324":"KAB. LUWU TIMUR","7326":"KAB. TORAJA UTARA","7371":"KOTA MAKASSAR","7372":"KOTA PARE PARE","7373":"KOTA PALOPO","7401":"KAB. KOLAKA","7402":"KAB. KONAWE","7403":"KAB. MUNA","7404":"KAB. BUTON","7405":"KAB. KONAWE SELATAN","7406":"KAB. BOMBANA","7407":"KAB. WAKATOBI","7408":"KAB. KOLAKA UTARA","7409":"KAB. KONAWE UTARA","7410":"KAB. BUTON UTARA","7411":"KAB. KOLAKA TIMUR","7412":"KAB. KONAWE KEPULAUAN","7413":"KAB. MUNA BARAT","7414":"KAB. BUTON TENGAH","7415":"KAB. BUTON SELATAN","7471":"KOTA KENDARI","7472":"KOTA BAU BAU","7501":"KAB. GORONTALO","7502":"KAB. BOALEMO","7503":"KAB. BONE BOLANGO","7504":"KAB. PAHUWATO","7505":"KAB. GORONTALO UTARA","7571":"KOTA GORONTALO","7601":"KAB. MAMUJU UTARA","7602":"KAB. MAMUJU","7603":"KAB. MAMASA","7604":"KAB. POLEWALI MANDAR","7605":"KAB. MAJENE","7606":"KAB. MAMUJU TENGAH","8101":"KAB. MALUKU TENGAH","8102":"KAB. MALUKU TENGGARA","8103":"KAB MALUKU TENGGARA BARAT","8104":"KAB. BURU","8105":"KAB. SERAM BAGIAN TIMUR","8106":"KAB. SERAM BAGIAN BARAT","8107":"KAB. KEPULAUAN ARU","8108":"KAB. MALUKU BARAT DAYA","8109":"KAB. BURU SELATAN","8171":"KOTA AMBON","8172":"KOTA TUAL","8201":"KAB. HALMAHERA BARAT","8202":"KAB. HALMAHERA TENGAH","8203":"KAB. HALMAHERA UTARA","8204":"KAB. HALMAHERA SELATAN","8205":"KAB. KEPULAUAN SULA","8206":"KAB. HALMAHERA TIMUR","8207":"KAB. PULAU MOROTAI","8208":"KAB. PULAU TALIABU","8271":"KOTA TERNATE","8272":"KOTA TIDORE KEPULAUAN","9101":"KAB. MERAUKE","9102":"KAB. JAYAWIJAYA","9103":"KAB. JAYAPURA","9104":"KAB. NABIRE","9105":"KAB. KEPULAUAN YAPEN","9106":"KAB. BIAK NUMFOR","9107":"KAB. PUNCAK JAYA","9108":"KAB. PANIAI","9109":"KAB. MIMIKA","9110":"KAB. SARMI","9111":"KAB. KEEROM","9112":"KAB PEGUNUNGAN BINTANG","9113":"KAB. YAHUKIMO","9114":"KAB. TOLIKARA","9115":"KAB. WAROPEN","9116":"KAB. BOVEN DIGOEL","9117":"KAB. MAPPI","9118":"KAB. ASMAT","9119":"KAB. SUPIORI","9120":"KAB. MAMBERAMO RAYA","9121":"KAB. MAMBERAMO TENGAH","9122":"KAB. YALIMO","9123":"KAB. LANNY JAYA","9124":"KAB. NDUGA","9125":"KAB. PUNCAK","9126":"KAB. DOGIYAI","9127":"KAB. INTAN JAYA","9128":"KAB. DEIYAI","9171":"KOTA JAYAPURA","9201":"KAB. SORONG","9202":"KOT. MANOKWARI","9203":"KAB. FAK FAK","9204":"KAB. SORONG SELATAN","9205":"KAB. RAJA AMPAT","9206":"KAB. TELUK BINTUNI","9207":"KAB. TELUK WONDAMA","9208":"KAB. KAIMANA","9209":"KAB. TAMBRAUW","9210":"KAB. MAYBRAT","9211":"KAB. MANOKWARI SELATAN","9212":"KAB. PEGUNUNGAN ARFAK","9271":"KOTA SORONG"},"kecamatan":{"110101":"BAKONGAN -- 23773","110102":"KLUET UTARA -- 23771","110103":"KLUET SELATAN -- 23772","110104":"LABUHAN HAJI -- 23761","110105":"MEUKEK -- 23754","110106":"SAMADUA -- 23752","110107":"SAWANG -- 24377","110108":"TAPAKTUAN -- 23711","110109":"TRUMON -- 23774","110110":"PASI RAJA -- 23755","110111":"LABUHAN HAJI TIMUR -- 23758","110112":"LABUHAN HAJI BARAT -- 23757","110113":"KLUET TENGAH -- 23772","110114":"KLUET TIMUR -- 23772","110115":"BAKONGAN TIMUR -- 23773","110116":"TRUMON TIMUR -- 23774","110117":"KOTA BAHAGIA -- 23773","110118":"TRUMON TENGAH -- 23774","110201":"LAWE ALAS -- 24661","110202":"LAWE SIGALA-GALA -- 24673","110203":"BAMBEL -- 24671","110204":"BABUSSALAM -- 24651","110205":"BADAR -- 24652","110206":"BABUL MAKMUR -- 24673","110207":"DARUL HASANAH -- 24653","110208":"LAWE BULAN -- 24651","110209":"BUKIT TUSAM -- 24671","110210":"SEMADAM -- 24678","110211":"BABUL RAHMAH -- 24673","110212":"KETAMBE -- 24652","110213":"DELENG POKHKISEN -- 24660","110214":"LAWE SUMUR -- 24671","110215":"TANOH ALAS -- 24673","110216":"LEUSER -- 24673","110301":"DARUL AMAN -- 24455","110302":"JULOK -- 24457","110303":"IDI RAYEUK -- 24454","110304":"BIREM BAYEUN -- 24452","110305":"SERBAJADI -- 24461","110306":"NURUSSALAM -- 24456","110307":"PEUREULAK -- 24453","110308":"RANTAU SELAMAT -- 24452","110309":"SIMPANG ULIM -- 24458","110310":"RANTAU PEUREULAK -- 24441","110311":"PANTE BIDARI -- 24458","110312":"MADAT -- 24458","110313":"INDRA MAKMU -- 24457","110314":"IDI TUNONG -- 24454","110315":"BANDA ALAM -- 24458","110316":"PEUDAWA -- 24454","110317":"PEUREULAK TIMUR -- 24453","110318":"PEUREULAK BARAT -- 24453","110319":"SUNGAI RAYA -- 24458","110320":"SIMPANG JERNIH -- 24458","110321":"DARUL IHSAN -- 24468","110322":"DARUL FALAH -- 24454","110323":"IDI TIMUR -- 24456","110324":"PEUNARON -- 24461","110401":"LINGE -- 24563","110402":"SILIH NARA -- 24562","110403":"BEBESEN -- 24552","110407":"PEGASING -- 24561","110408":"BINTANG -- 24571","110410":"KETOL -- 24562","110411":"KEBAYAKAN -- 24519","110412":"KUTE PANANG -- 24568","110413":"CELALA -- 24562","110417":"LAUT TAWAR -- 24511 - 24516","110418":"ATU LINTANG -- 24563","110419":"JAGONG JEGET -- 24563","110420":"BIES -- 24561","110421":"RUSIP ANTARA -- 24562","110501":"JOHAN PAHWALAN -- 23617 - 23618","110502":"KAWAY XVI -- 23681","110503":"SUNGAI MAS -- 23681","110504":"WOYLA -- 23682","110505":"SAMATIGA -- 23652","110506":"BUBON -- 23652","110507":"ARONGAN LAMBALEK -- 23652","110508":"PANTE CEUREUMEN -- 23681","110509":"MEUREUBO -- 23615","110510":"WOYLA BARAT -- 23682","110511":"WOYLA TIMUR -- 23682","110512":"PANTON REU -- 23681","110601":"LHOONG -- 23354","110602":"LHOKNGA -- 23353","110603":"INDRAPURI -- 23363","110604":"SEULIMEUM -- 23951","110605":"MONTASIK -- 23362","110606":"SUKAMAKMUR -- 23361","110607":"DARUL IMARAH -- 23352","110608":"PEUKAN BADA -- 23351","110609":"MESJID RAYA -- 23381","110610":"INGIN JAYA -- 23371","110611":"KUTA BARO -- 23372","110612":"DARUSSALAM -- 23373","110613":"PULO ACEH -- 23391","110614":"LEMBAH SEULAWAH -- 23952","110615":"KOTA JANTHO -- 23917","110616":"KOTA COT GLIE -- 23363","110617":"KUTA MALAKA -- 23363","110618":"SIMPANG TIGA -- 23371","110619":"DARUL KAMAL -- 23352","110620":"BAITUSSALAM -- 23373","110621":"KRUENG BARONA JAYA -- 23371","110622":"LEUPUNG -- 23353","110623":"BLANG BINTANG -- 23360","110703":"BATEE -- 24152","110704":"DELIMA -- 24162","110705":"GEUMPANG -- 24167","110706":"GEULUMPANG TIGA -- 24183","110707":"INDRA JAYA -- 23657","110708":"KEMBANG TANJONG -- 24182","110709":"KOTA SIGLI -- 24112","110711":"MILA -- 24163","110712":"MUARA TIGA -- 24153","110713":"MUTIARA -- 24173","110714":"PADANG TIJI -- 24161","110715":"PEUKAN BARO -- 24172","110716":"PIDIE -- 24151","110717":"SAKTI -- 24164","110718":"SIMPANG TIGA -- 23371","110719":"TANGSE -- 24166","110721":"TIRO/TRUSEB -- 24174","110722":"KEUMALA -- 24165","110724":"MUTIARA TIMUR -- 24173","110725":"GRONG-GRONG -- 24150","110727":"MANE -- 24186","110729":"GLUMPANG BARO -- 24183","110731":"TITEUE -- 24165","110801":"BAKTIYA -- 24392","110802":"DEWANTARA -- 24354","110803":"KUTA MAKMUR -- 24371","110804":"LHOKSUKON -- 24382","110805":"MATANGKULI -- 24386","110806":"MUARA BATU -- 24355","110807":"MEURAH MULIA -- 24372","110808":"SAMUDERA -- 24374","110809":"SEUNUDDON -- 24393","110810":"SYAMTALIRA ARON -- 24381","110811":"SYAMTALIRA BAYU -- 24373","110812":"TANAH LUAS -- 24385","110813":"TANAH PASIR -- 24391","110814":"T. JAMBO AYE -- 24395","110815":"SAWANG -- 24377","110816":"NISAM -- 24376","110817":"COT GIREK -- 24352","110818":"LANGKAHAN -- 24394","110819":"BAKTIYA BARAT -- 24392","110820":"PAYA BAKONG -- 24386","110821":"NIBONG -- 24385","110822":"SIMPANG KRAMAT -- 24313","110823":"LAPANG -- 24391","110824":"PIRAK TIMUR -- 24386","110825":"GEUREDONG PASE -- 24373","110826":"BANDA BARO -- 24376","110827":"NISAM ANTARA -- 24376","110901":"SIMEULUE TENGAH -- 23894","110902":"SALANG -- 23893","110903":"TEUPAH BARAT -- 23892","110904":"SIMEULUE TIMUR -- 23891","110905":"TELUK DALAM -- 23891","110906":"SIMEULUE BARAT -- 23892","110907":"TEUPAH SELATAN -- 23891","110908":"ALAPAN -- 23893","110909":"TEUPAH TENGAH -- 23891","110910":"SIMEULUE CUT -- 23894","111001":"PULAU BANYAK -- 24791","111002":"SIMPANG KANAN -- 24783","111004":"SINGKIL -- 24785","111006":"GUNUNG MERIAH -- 24784","111009":"KOTA BAHARU -- 24784","111010":"SINGKIL UTARA -- 24785","111011":"DANAU PARIS -- 24784","111012":"SURO MAKMUR -- 24784","111013":"SINGKOHOR -- 24784","111014":"KUALA BARU -- 24784","111016":"PULAU BANYAK BARAT -- 24791","111101":"SAMALANGA -- 24264","111102":"JEUNIEB -- 24263","111103":"PEUDADA -- 24262","111104":"JEUMPA -- 24251","111105":"PEUSANGAN -- 24261","111106":"MAKMUR -- 23662","111107":"GANDAPURA -- 24356","111108":"PANDRAH -- 24263","111109":"JULI -- 24251","111110":"JANGKA -- 24261","111111":"SIMPANG MAMPLAM -- 24251","111112":"PEULIMBANG -- 24263","111113":"KOTA JUANG -- 24251","111114":"KUALA -- 23661","111115":"PEUSANGAN SIBLAH KRUENG -- 24261","111116":"PEUSANGAN SELATAN -- 24261","111117":"KUTA BLANG -- 24356","111201":"BLANG PIDIE -- 23764","111202":"TANGAN-TANGAN -- 23763","111203":"MANGGENG -- 23762","111204":"SUSOH -- 23765","111205":"KUALA BATEE -- 23766","111206":"BABAH ROT -- 23767","111207":"SETIA -- 23763","111208":"JEUMPA -- 24251","111209":"LEMBAH SABIL -- 23762","111301":"BLANGKEJEREN -- 24655","111302":"KUTAPANJANG -- 24655","111303":"RIKIT GAIB -- 24654","111304":"TERANGUN -- 24656","111305":"PINING -- 24655","111306":"BLANGPEGAYON -- 24653","111307":"PUTERI BETUNG -- 24658","111308":"DABUN GELANG -- 24653","111309":"BLANGJERANGO -- 24655","111310":"TERIPE JAYA -- 24657","111311":"PANTAN CUACA -- 24654","111401":"TEUNOM -- 23653","111402":"KRUENG SABEE -- 23654","111403":"SETIA BHAKTI -- 23655","111404":"SAMPOINIET -- 23656","111405":"JAYA -- 23371","111406":"PANGA -- 23653","111407":"INDRA JAYA -- 23657","111408":"DARUL HIKMAH -- 23656","111409":"PASIE RAYA -- 23653","111501":"KUALA -- 23661","111502":"SEUNAGAN -- 23671","111503":"SEUNAGAN TIMUR -- 23671","111504":"BEUTONG -- 23672","111505":"DARUL MAKMUR -- 23662","111506":"SUKA MAKMUE -- 23671","111507":"KUALA PESISIR -- 23661","111508":"TADU RAYA -- 23661","111509":"TRIPA MAKMUR -- 23662","111510":"BEUTONG ATEUH BANGGALANG -- 23672","111601":"MANYAK PAYED -- 24471","111602":"BENDAHARA -- 24472","111603":"KARANG BARU -- 24476","111604":"SERUWAY -- 24473","111605":"KOTA KUALASINPANG -- 24475","111606":"KEJURUAN MUDA -- 24477","111607":"TAMIANG HULU -- 24478","111608":"RANTAU -- 24452","111609":"BANDA MULIA -- 24472","111610":"BANDAR PUSAKA -- 24478","111611":"TENGGULUN -- 24477","111612":"SEKERAK -- 24476","111701":"PINTU RIME GAYO -- 24553","111702":"PERMATA -- 24582","111703":"SYIAH UTAMA -- 24582","111704":"BANDAR -- 24184","111705":"BUKIT -- 24671","111706":"WIH PESAM -- 24581","111707":"TIMANG GAJAH -- 24553","111708":"BENER KELIPAH -- 24582","111709":"MESIDAH -- 24582","111710":"GAJAH PUTIH -- 24553","111801":"MEUREUDU -- 24186","111802":"ULIM -- 24458","111803":"JANGKA BUAYA -- 24186","111804":"BANDAR DUA -- 24188","111805":"MEURAH DUA -- 24186","111806":"BANDAR BARU -- 24184","111807":"PANTERAJA -- 24185","111808":"TRIENGGADENG -- 24185","117101":"BAITURRAHMAN -- 23244","117102":"KUTA ALAM -- 23126","117103":"MEURAXA -- 23232","117104":"SYIAH KUALA -- 23116","117105":"LUENG BATA -- 23245","117106":"KUTA RAJA -- 23128","117107":"BANDA RAYA -- 23239","117108":"JAYA BARU -- 23235","117109":"ULEE KARENG -- 23117","117201":"SUKAKARYA -- 23514","117202":"SUKAJAYA -- 23524","117301":"MUARA DUA -- 24352","117302":"BANDA SAKTI -- 24351","117303":"BLANG MANGAT -- 24375","117304":"MUARA SATU -- 24352","117401":"LANGSA TIMUR -- 24411","117402":"LANGSA BARAT -- 24410","117403":"LANGSA KOTA -- 24410","117404":"LANGSA LAMA -- 24416","117405":"LANGSA BARO -- 24415","117501":"SIMPANG KIRI -- 24782","117502":"PENANGGALAN -- 24782","117503":"RUNDENG -- 24786","117504":"SULTAN DAULAT -- 24782","117505":"LONGKIB -- 24782","120101":"BARUS -- 22564","120102":"SORKAM -- 22563","120103":"PANDAN -- 22613","120104":"PINANGSORI -- 22654","120105":"MANDUAMAS -- 22565","120106":"KOLANG -- 22562","120107":"TAPIAN NAULI -- 22618","120108":"SIBABANGUN -- 22654","120109":"SOSOR GADONG -- 22564","120110":"SORKAM BARAT -- 22563","120111":"SIRANDORUNG -- 22565","120112":"ANDAM DEWI -- 22651","120113":"SITAHUIS -- 22611","120114":"TUKKA -- 22617","120115":"BADIRI -- 22654","120116":"PASARIBU TOBING -- 22563","120117":"BARUS UTARA -- 22564","120118":"SUKA BANGUN -- 22654","120119":"LUMUT -- 22654","120120":"SARUDIK -- 22611","120201":"TARUTUNG -- 22413","120202":"SIATAS BARITA -- 22417","120203":"ADIAN KOTING -- 22461","120204":"SIPOHOLON -- 22452","120205":"PAHAE JULU -- 22463","120206":"PAHAE JAE -- 22465","120207":"SIMANGUMBAN -- 22466","120208":"PURBA TUA -- 22465","120209":"SIBORONG-BORONG -- 22474","120210":"PAGARAN -- 22458","120211":"PARMONANGAN -- 22453","120212":"SIPAHUTAR -- 22471","120213":"PANGARIBUAN -- 22472","120214":"GAROGA -- 22473","120215":"MUARA -- 22476","120301":"ANGKOLA BARAT -- 22735","120302":"BATANG TORU -- 22738","120303":"ANGKOLA TIMUR -- 22733","120304":"SIPIROK -- 22742","120305":"SAIPAR DOLOK HOLE -- 22758","120306":"ANGKOLA SELATAN -- 22732","120307":"BATANG ANGKOLA -- 22773","120314":"ARSE -- 21126","120320":"MARANCAR -- 22738","120321":"SAYUR MATINGGI -- 22774","120322":"AEK BILAH -- 22758","120329":"MUARA BATANG TORU -- 22738","120330":"TANO TOMBANGAN ANGKOLA -- 22774","120331":"ANGKOLA SANGKUNUR -- 22735","120405":"HILIDUHO -- 22854","120406":"GIDO -- 22871","120410":"IDANOGAWO -- 22872","120411":"BAWOLATO -- 22876","120420":"HILISERANGKAI -- 22851","120421":"BOTOMUZOI -- 22815","120427":"ULUGAWO -- 22861","120428":"MA'U -- -","120429":"SOMOLO-MOLO -- 22871","120435":"SOGAE'ADU -- -","120501":"BAHOROK -- 20774","120502":"SALAPIAN -- 20773","120503":"KUALA -- 21475","120504":"SEI BINGEI -- -","120505":"BINJAI -- 20719","120506":"SELESAI -- 20762","120507":"STABAT -- 20811","120508":"WAMPU -- 20851","120509":"SECANGGANG -- 20855","120510":"HINAI -- 20854","120511":"TANJUNG PURA -- 20853","120512":"PADANG TUALANG -- 20852","120513":"GEBANG -- 20856","120514":"BABALAN -- 20857","120515":"PANGKALAN SUSU -- 20858","120516":"BESITANG -- 20859","120517":"SEI LEPAN -- 20773","120518":"BRANDAN BARAT -- 20881","120519":"BATANG SERANGAN -- 20852","120520":"SAWIT SEBERANG -- 20811","120521":"SIRAPIT -- 20772","120522":"KUTAMBARU -- 20773","120523":"PEMATANG JAYA -- 20858","120601":"KABANJAHE -- 22111","120602":"BERASTAGI -- 22152","120603":"BARUSJAHE -- 22172","120604":"TIGAPANAH -- 22171","120605":"MEREK -- 22173","120606":"MUNTE -- 22755","120607":"JUHAR -- 22163","120608":"TIGABINANGA -- 22162","120609":"LAUBALENG -- 22164","120610":"MARDINGDING -- -","120611":"PAYUNG -- 22154","120612":"SIMPANG EMPAT -- 21271","120613":"KUTABULUH -- 22155","120614":"DOLAT RAYAT -- 22171","120615":"MERDEKA -- 22153","120616":"NAMAN TERAN -- -","120617":"TIGANDERKET -- 22154","120701":"GUNUNG MERIAH -- 20583","120702":"TANJUNG MORAWA -- 20362","120703":"SIBOLANGIT -- 20357","120704":"KUTALIMBARU -- 20354","120705":"PANCUR BATU -- 20353","120706":"NAMORAMBE -- 20356","120707":"SIBIRU-BIRU -- -","120708":"STM HILIR -- -","120709":"BANGUN PURBA -- 20581","120719":"GALANG -- 20585","120720":"STM HULU -- -","120721":"PATUMBAK -- 20361","120722":"DELI TUA -- 20355","120723":"SUNGGAL -- 20121","120724":"HAMPARAN PERAK -- 20374","120725":"LABUHAN DELI -- 20373","120726":"PERCUT SEI TUAN -- 20371","120727":"BATANG KUIS -- 20372","120728":"LUBUK PAKAM -- 20511","120731":"PAGAR MERBAU -- 20551","120732":"PANTAI LABU -- 20553","120733":"BERINGIN -- 20552","120801":"SIANTAR -- 21126","120802":"GUNUNG MALELA -- 21174","120803":"GUNUNG MALIGAS -- 21174","120804":"PANEI -- 21161","120805":"PANOMBEIAN PANE -- 21165","120806":"JORLANG HATARAN -- 21172","120807":"RAYA KAHEAN -- 21156","120808":"BOSAR MALIGAS -- 21183","120809":"SIDAMANIK -- 21171","120810":"PEMATANG SIDAMANIK -- 21186","120811":"TANAH JAWA -- 21181","120812":"HATONDUHAN -- 21174","120813":"DOLOK PANRIBUAN -- 21173","120814":"PURBA -- 20581","120815":"HARANGGAOL HORISON -- 21174","120816":"GIRSANG SIPANGAN BOLON -- 21174","120817":"DOLOK BATU NANGGAR -- 21155","120818":"HUTA BAYU RAJA -- 21182","120819":"JAWA MARAJA BAH JAMBI -- 21153","120820":"DOLOK PARDAMEAN -- 21163","120821":"PEMATANG BANDAR -- 21186","120822":"BANDAR HULUAN -- 21184","120823":"BANDAR -- 21274","120824":"BANDAR MASILAM -- 21184","120825":"SILIMAKUTA -- 21167","120826":"DOLOK SILAU -- 21168","120827":"SILOU KAHEAN -- 21157","120828":"TAPIAN DOLOK -- 21154","120829":"RAYA -- 22866","120830":"UJUNG PADANG -- 21187","120831":"PAMATANG SILIMA HUTA -- -","120908":"MERANTI -- 21264","120909":"AIR JOMAN -- 21263","120910":"TANJUNG BALAI -- 21352","120911":"SEI KEPAYANG -- 21381","120912":"SIMPANG EMPAT -- 21271","120913":"AIR BATU -- 21272","120914":"PULAU RAKYAT -- 21273","120915":"BANDAR PULAU -- 21274","120916":"BUNTU PANE -- 21261","120917":"BANDAR PASIR MANDOGE -- 21262","120918":"AEK KUASAN -- 21273","120919":"KOTA KISARAN BARAT -- -","120920":"KOTA KISARAN TIMUR -- -","120921":"AEK SONGSONGAN -- 21274","120922":"RAHUNIG -- -","120923":"SEI DADAP -- 21272","120924":"SEI KEPAYANG BARAT -- 21381","120925":"SEI KEPAYANG TIMUR -- 21381","120926":"TINGGI RAJA -- 21261","120927":"SETIA JANJI -- 21261","120928":"SILAU LAUT -- 21263","120929":"RAWANG PANCA ARGA -- 21264","120930":"PULO BANDRING -- 21264","120931":"TELUK DALAM -- 21271","120932":"AEK LEDONG -- 21273","121001":"RANTAU UTARA -- 21419","121002":"RANTAU SELATAN -- 21421","121007":"BILAH BARAT -- 21411","121008":"BILAH HILIR -- 21471","121009":"BILAH HULU -- 21451","121014":"PANGKATAN -- 21462","121018":"PANAI TENGAH -- 21472","121019":"PANAI HILIR -- 21473","121020":"PANAI HULU -- 21471","121101":"SIDIKALANG -- 22212","121102":"SUMBUL -- 22281","121103":"TIGALINGGA -- 22252","121104":"SIEMPAT NEMPU -- 22261","121105":"SILIMA PUNGGA PUNGA -- -","121106":"TANAH PINEM -- 22253","121107":"SIEMPAT NEMPU HULU -- 22254","121108":"SIEMPAT NEMPU HILIR -- 22263","121109":"PEGAGAN HILIR -- 22283","121110":"PARBULUAN -- 22282","121111":"LAE PARIRA -- 22281","121112":"GUNUNG SITEMBER -- 22251","121113":"BRAMPU -- 22251","121114":"SILAHISABUNGAN -- 22281","121115":"SITINJO -- 22219","121201":"BALIGE -- 22312","121202":"LAGUBOTI -- 22381","121203":"SILAEN -- 22382","121204":"HABINSARAN -- 22383","121205":"PINTU POHAN MERANTI -- 22384","121206":"BORBOR -- -","121207":"PORSEA -- 22384","121208":"AJIBATA -- 22386","121209":"LUMBAN JULU -- 22386","121210":"ULUAN -- 21184","121219":"SIGUMPAR -- 22381","121220":"SIANTAR NARUMONDA -- 22384","121221":"NASSAU -- 22383","121222":"TAMPAHAN -- 22312","121223":"BONATUA LUNASI -- 22386","121224":"PARMAKSIAN -- 22384","121301":"PANYABUNGAN -- 22912","121302":"PANYABUNGAN UTARA -- 22978","121303":"PANYABUNGAN TIMUR -- 22912","121304":"PANYABUNGAN SELATAN -- 22952","121305":"PANYABUNGAN BARAT -- 22911","121306":"SIABU -- 22976","121307":"BUKIT MALINTANG -- 22977","121308":"KOTANOPAN -- 22994","121309":"LEMBAH SORIK MARAPI -- -","121310":"TAMBANGAN -- 22994","121311":"ULU PUNGKUT -- 22998","121312":"MUARA SIPONGI -- 22998","121313":"BATANG NATAL -- 22983","121314":"LINGGA BAYU -- 22983","121315":"BATAHAN -- 22988","121316":"NATAL -- 22983","121317":"MUARA BATANG GADIS -- 22989","121318":"RANTO BAEK -- 22983","121319":"HUTA BARGOT -- 22978","121320":"PUNCAK SORIK MARAPI -- 22994","121321":"PAKANTAN -- 22998","121322":"SINUNUKAN -- 22988","121323":"NAGA JUANG -- 22977","121401":"LOLOMATUA -- 22867","121402":"GOMO -- 22873","121403":"LAHUSA -- 22874","121404":"HIBALA -- 22881","121405":"PULAU-PULAU BATU -- 22881","121406":"TELUK DALAM -- 21271","121407":"AMANDRAYA -- 22866","121408":"LALOWA'U -- -","121409":"SUSUA -- 22866","121410":"MANIAMOLO -- 22865","121411":"HILIMEGAI -- 22864","121412":"TOMA -- 22865","121413":"MAZINO -- 22865","121414":"UMBUNASI -- 22873","121415":"ARAMO -- 22866","121416":"PULAU-PULAU BATU TIMUR -- 22881","121417":"MAZO -- 22873","121418":"FANAYAMA -- 22865","121419":"ULUNOYO -- 22867","121420":"HURUNA -- 22867","121421":"O'O'U -- -","121422":"ONOHAZUMBA -- 22864","121423":"HILISALAWA'AHE -- -","121425":"SIDUA'ORI -- -","121426":"SOMAMBAWA -- 22874","121427":"BORONADU -- 22873","121428":"SIMUK -- 22881","121429":"PULAU-PULAU BATU BARAT -- 22881","121430":"PULAU-PULAU BATU UTARA -- 22881","121501":"SITELU TALI URANG JEHE -- -","121502":"KERAJAAN -- 22271","121503":"SALAK -- 22272","121504":"SITELU TALI URANG JULU -- -","121505":"PERGETTENG GETTENG SENGKUT -- 22271","121506":"PAGINDAR -- 22271","121507":"TINADA -- 22272","121508":"SIEMPAT RUBE -- 22272","121601":"PARLILITAN -- 22456","121602":"POLLUNG -- 22457","121603":"BAKTIRAJA -- 22457","121604":"PARANGINAN -- 22475","121605":"LINTONG NIHUTA -- 22475","121606":"DOLOK SANGGUL -- 22457","121607":"SIJAMAPOLANG -- 22457","121608":"ONAN GANJANG -- 22454","121609":"PAKKAT -- 22455","121610":"TARABINTANG -- 22456","121701":"SIMANINDO -- 22395","121702":"ONAN RUNGGU -- 22391","121703":"NAINGGOLAN -- 22394","121704":"PALIPI -- 22393","121705":"HARIAN -- 22391","121706":"SIANJAR MULA MULA -- -","121707":"RONGGUR NIHUTA -- 22392","121708":"PANGURURAN -- 22392","121709":"SITIO-TIO -- 22395","121801":"PANTAI CERMIN -- 20987","121802":"PERBAUNGAN -- 20986","121803":"TELUK MENGKUDU -- 20997","121804":"SEI. RAMPAH -- -","121805":"TANJUNG BERINGIN -- 20996","121806":"BANDAR KHALIFAH -- 20994","121807":"DOLOK MERAWAN -- 20993","121808":"SIPISPIS -- 20992","121809":"DOLOK MASIHUL -- 20991","121810":"KOTARIH -- 20984","121811":"SILINDA -- 20984","121812":"SERBA JADI -- 20991","121813":"TEBING TINGGI -- 20615","121814":"PEGAJAHAN -- 20986","121815":"SEI BAMBAN -- 20995","121816":"TEBING SYAHBANDAR -- 20998","121817":"BINTANG BAYU -- 20984","121901":"MEDANG DERAS -- 21258","121902":"SEI SUKA -- 21257","121903":"AIR PUTIH -- 21256","121904":"LIMA PULUH -- 21255","121905":"TALAWI -- 21254","121906":"TANJUNG TIRAM -- 21253","121907":"SEI BALAI -- 21252","122001":"DOLOK SIGOMPULON -- 22756","122002":"DOLOK -- 22756","122003":"HALONGONAN -- 22753","122004":"PADANG BOLAK -- 22753","122005":"PADANG BOLAK JULU -- 22753","122006":"PORTIBI -- 22741","122007":"BATANG ONANG -- 22762","122008":"SIMANGAMBAT -- 22747","122009":"HULU SIHAPAS -- 22733","122101":"SOSOPAN -- 22762","122102":"BARUMUN TENGAH -- 22755","122103":"HURISTAK -- 22742","122104":"LUBUK BARUMUN -- 22763","122105":"HUTA RAJA TINGGI -- 22774","122106":"ULU BARUMUN -- 22763","122107":"BARUMUN -- 22755","122108":"SOSA -- 22765","122109":"BATANG LUBU SUTAM -- 22742","122110":"BARUMUN SELATAN -- 22763","122111":"AEK NABARA BARUMUN -- 22755","122112":"SIHAPAS BARUMUN -- 22755","122201":"KOTAPINANG -- 21464","122202":"KAMPUNG RAKYAT -- 21463","122203":"TORGAMBA -- 21464","122204":"SUNGAI KANAN -- 21465","122205":"SILANGKITANG -- 21461","122301":"KUALUH HULU -- 21457","122302":"KUALUH LEIDONG -- 21475","122303":"KUALUH HILIR -- 21474","122304":"AEK KUO -- 21455","122305":"MARBAU -- 21452","122306":"NA IX - X -- 21454","122307":"AEK NATAS -- 21455","122308":"KUALUH SELATAN -- 21457","122401":"LOTU -- 22851","122402":"SAWO -- 22852","122403":"TUHEMBERUA -- 22852","122404":"SITOLU ORI -- 22852","122405":"NAMOHALU ESIWA -- 22816","122406":"ALASA TALUMUZOI -- 22814","122407":"ALASA -- 22861","122408":"TUGALA OYO -- 22861","122409":"AFULU -- 22857","122410":"LAHEWA -- 22853","122411":"LAHEWA TIMUR -- 22851","122501":"LAHOMI -- 22864","122502":"SIROMBU -- 22863","122503":"MANDREHE BARAT -- 22812","122504":"MORO'O -- -","122505":"MANDREHE -- 22814","122506":"MANDREHE UTARA -- 22814","122507":"LOLOFITU MOI -- 22875","122508":"ULU MORO'O -- -","127101":"MEDAN KOTA -- 20215","127102":"MEDAN SUNGGAL -- 20121","127103":"MEDAN HELVETIA -- 20126","127104":"MEDAN DENAI -- 20228","127105":"MEDAN BARAT -- 20115","127106":"MEDAN DELI -- 20243","127107":"MEDAN TUNTUNGAN -- 20136","127108":"MEDAN BELAWAN -- 20414","127109":"MEDAN AMPLAS -- 20229","127110":"MEDAN AREA -- 20215","127111":"MEDAN JOHOR -- 20144","127112":"MEDAN MARELAN -- 20254","127113":"MEDAN LABUHAN -- 20251","127114":"MEDAN TEMBUNG -- 20223","127115":"MEDAN MAIMUN -- 20151","127116":"MEDAN POLONIA -- 20152","127117":"MEDAN BARU -- 20154","127118":"MEDAN PERJUANGAN -- 20233","127119":"MEDAN PETISAH -- 20112","127120":"MEDAN TIMUR -- 20235","127121":"MEDAN SELAYANG -- 20133","127201":"SIANTAR TIMUR -- 21136","127202":"SIANTAR BARAT -- 21112","127203":"SIANTAR UTARA -- 21142","127204":"SIANTAR SELATAN -- 21126","127205":"SIANTAR MARIHAT -- 21129","127206":"SIANTAR MARTOBA -- 21137","127207":"SIANTAR SITALASARI -- 21139","127208":"SIANTAR MARIMBUN -- 21128","127301":"SIBOLGA UTARA -- 22511","127302":"SIBOLGA KOTA -- 22521","127303":"SIBOLGA SELATAN -- 22533","127304":"SIBOLGA SAMBAS -- 22535","127401":"TANJUNG BALAI SELATAN -- 21315","127402":"TANJUNG BALAI UTARA -- 21324","127403":"SEI TUALANG RASO -- 21344","127404":"TELUK NIBUNG -- 21335","127405":"DATUK BANDAR -- 21367","127406":"DATUK BANDAR TIMUR -- 21367","127501":"BINJAI UTARA -- 20747","127502":"BINJAI KOTA -- 20715","127503":"BINJAI BARAT -- 20719","127504":"BINJAI TIMUR -- 20736","127505":"BINJAI SELATAN -- 20728","127601":"PADANG HULU -- 20625","127602":"RAMBUTAN -- 20611","127603":"PADANG HILIR -- 20634","127604":"BAJENIS -- 20613","127605":"TEBING TINGGI KOTA -- 20615","127701":"PADANGSIDIMPUAN UTARA -- -","127702":"PADANGSIDIMPUAN SELATAN -- -","127703":"PADANGSIDIMPUAN BATUNADUA -- -","127704":"PADANGSIDIMPUAN HUTAIMBARU -- -","127705":"PADANGSIDIMPUAN TENGGARA -- -","127706":"PADANGSIDIMPUAN ANGKOLA JULU -- -","130101":"PANCUNG SOAL -- 25673","130102":"RANAH PESISIR -- 25666","130103":"LENGAYANG -- 25663","130104":"BATANG KAPAS -- 25661","130105":"IV JURAI -- 25651","130106":"BAYANG -- 25652","130107":"KOTO XI TARUSAN -- 25654","130108":"SUTERA -- 25662","130109":"LINGGO SARI BAGANTI -- 25668","130111":"BASA AMPEK BALAI TAPAN -- 25673","130112":"IV NAGARI BAYANG UTARA -- 25652","130113":"AIRPURA -- 25673","130114":"RANAH AMPEK HULU TAPAN -- 25673","130115":"SILAUT -- 25674","130203":"PANTAI CERMIN -- 27373","130204":"LEMBAH GUMANTI -- 27371","130205":"PAYUNG SEKAKI -- 27387","130206":"LEMBANG JAYA -- 27383","130207":"GUNUNG TALANG -- 27365","130208":"BUKIT SUNDI -- 27381","130209":"IX KOTO SUNGAI LASI -- -","130210":"KUBUNG -- 27361","130211":"X KOTO SINGKARAK -- 27356","130212":"X KOTO DIATAS -- 27355","130213":"JUNJUNG SIRIH -- 27388","130217":"HILIRAN GUMANTI -- 27372","130218":"TIGO LURAH -- 27372","130219":"DANAU KEMBAR -- 27383","130303":"TANJUNG GADANG -- 27571","130304":"SIJUNJUNG -- 27553","130305":"IV NAGARI -- 26161","130306":"KAMANG BARU -- 27572","130307":"LUBUAK TAROK -- 27553","130308":"KOTO VII -- 27562","130309":"SUMPUR KUDUS -- 27563","130310":"KUPITAN -- 27564","130401":"X KOTO -- 27151","130402":"BATIPUH -- 27265","130403":"RAMBATAN -- 27271","130404":"LIMA KAUM -- 27213","130405":"TANJUNG EMAS -- 27281","130406":"LINTAU BUO -- 27292","130407":"SUNGAYANG -- 27294","130408":"SUNGAI TARAB -- 27261","130409":"PARIANGAN -- 27264","130410":"SALIMPAUANG -- -","130411":"PADANG GANTING -- 27282","130412":"TANJUANG BARU -- -","130413":"LINTAU BUO UTARA -- 27292","130414":"BATIPUAH SELATAN -- -","130501":"LUBUK ALUNG -- 25581","130502":"BATANG ANAI -- 25586","130503":"NAN SABARIS -- 25571","130505":"VII KOTO SUNGAI SARIK -- 25573","130506":"V KOTO KAMPUNG DALAM -- 25552","130507":"SUNGAI GARINGGING -- -","130508":"SUNGAI LIMAU -- 25561","130509":"IV KOTO AUR MALINTANG -- 25564","130510":"ULAKAN TAPAKIH -- 25572","130511":"SINTUAK TOBOH GADANG -- 25582","130512":"PADANG SAGO -- 25573","130513":"BATANG GASAN -- 25563","130514":"V KOTO TIMUR -- 25573","130516":"PATAMUAN -- 25573","130517":"ENAM LINGKUNG -- 25584","130601":"TANJUNG MUTIARA -- 26473","130602":"LUBUK BASUNG -- 26451","130603":"TANJUNG RAYA -- 26471","130604":"MATUR -- 26162","130605":"IV KOTO -- 25564","130606":"BANUHAMPU -- 26181","130607":"AMPEK ANGKEK -- 26191","130608":"BASO -- 26192","130609":"TILATANG KAMANG -- 26152","130610":"PALUPUH -- 26151","130611":"PELEMBAYAN -- -","130612":"SUNGAI PUA -- 26181","130613":"AMPEK NAGARI -- 26161","130614":"CANDUNG -- 26191","130615":"KAMANG MAGEK -- 26152","130616":"MALALAK -- -","130701":"SULIKI -- 26255","130702":"GUGUAK -- 26111","130703":"PAYAKUMBUH -- 26228","130704":"LUAK -- 26261","130705":"HARAU -- 26271","130706":"PANGKALAN KOTO BARU -- 26272","130707":"KAPUR IX -- 26273","130708":"GUNUANG OMEH -- 26256","130709":"LAREH SAGO HALABAN -- 26262","130710":"SITUJUAH LIMO NAGARI -- -","130711":"MUNGKA -- 26254","130712":"BUKIK BARISAN -- 26257","130713":"AKABILURU -- 26252","130804":"BONJOL -- 26381","130805":"LUBUK SIKAPING -- 26318","130807":"PANTI -- 26352","130808":"MAPAT TUNGGUL -- 26353","130812":"DUO KOTO -- 26311","130813":"TIGO NAGARI -- 26353","130814":"RAO -- 26353","130815":"MAPAT TUNGGUL SELATAN -- 26353","130816":"SIMPANG ALAHAN MATI -- 26381","130817":"PADANG GELUGUR -- 26352","130818":"RAO UTARA -- 26353","130819":"RAO SELATAN -- 26353","130901":"PAGAI UTARA -- 25391","130902":"SIPORA SELATAN -- 25392","130903":"SIBERUT SELATAN -- 25393","130904":"SIBERUT UTARA -- 25394","130905":"SIBERUT BARAT -- 25393","130906":"SIBERUT BARAT DAYA -- 25393","130907":"SIBERUT TENGAH -- 25394","130908":"SIPORA UTARA -- 25392","130909":"SIKAKAP -- 25391","130910":"PAGAI SELATAN -- 25391","131001":"KOTO BARU -- 27681","131002":"PULAU PUNJUNG -- 27573","131003":"SUNGAI RUMBAI -- 27684","131004":"SITIUNG -- 27678","131005":"SEMBILAN KOTO -- 27681","131006":"TIMPEH -- 27678","131007":"KOTO SALAK -- 27681","131008":"TIUMANG -- 27681","131009":"PADANG LAWEH -- 27681","131010":"ASAM JUJUHAN -- 27684","131011":"KOTO BESAR -- 27684","131101":"SANGIR -- 27779","131102":"SUNGAI PAGU -- 27776","131103":"KOTO PARIK GADANG DIATEH -- 27775","131104":"SANGIR JUJUAN -- 27777","131105":"SANGIR BATANG HARI -- 27779","131106":"PAUH DUO -- 27776","131107":"SANGIR BALAI JANGGO -- 27777","131201":"SUNGAIBEREMAS -- -","131202":"LEMBAH MELINTANG -- 26572","131203":"PASAMAN -- 26566","131204":"TALAMAU -- 26561","131205":"KINALI -- 26567","131206":"GUNUNGTULEH -- 26571","131207":"RANAH BATAHAN -- 26366","131208":"KOTO BALINGKA -- 26572","131209":"SUNGAIAUR -- 26573","131210":"LUHAK NAN DUO -- 26567","131211":"SASAK RANAH PESISIR -- -","137101":"PADANG SELATAN -- 25217","137102":"PADANG TIMUR -- 25126","137103":"PADANG BARAT -- 25118","137104":"PADANG UTARA -- 25132","137105":"BUNGUS TELUK KABUNG -- 25237","137106":"LUBUK BEGALUNG -- 25222","137107":"LUBUK KILANGAN -- 25231","137108":"PAUH -- 27776","137109":"KURANJI -- 25154","137110":"NANGGALO -- 25145","137111":"KOTO TANGAH -- 25176","137201":"LUBUK SIKARAH -- 27317","137202":"TANJUNG HARAPAN -- 27321","137301":"LEMBAH SEGAR -- 27418","137302":"BARANGIN -- 27422","137303":"SILUNGKANG -- 27435","137304":"TALAWI -- 27443","137401":"PADANG PANJANG TIMUR -- 27125","137402":"PADANG PANJANG BARAT -- 27114","137501":"GUGUAK PANJANG -- 26111","137502":"MANDIANGIN K. SELAYAN -- -","137503":"AUR BIRUGO TIGO BALEH -- 26131","137601":"PAYAKUMBUH BARAT -- 26224","137602":"PAYAKUMBUH UTARA -- 26212","137603":"PAYAKUMBUH TIMUR -- 26231","137604":"LAMPOSI TIGO NAGORI -- -","137605":"PAYAKUMBUH SELATAN -- 26228","137701":"PARIAMAN TENGAH -- 25519","137702":"PARIAMAN UTARA -- 25522","137703":"PARIAMAN SELATAN -- 25531","137704":"PARIAMAN TIMUR -- 25531","140101":"BANGKINANG KOTA -- -","140102":"KAMPAR -- 28461","140103":"TAMBANG -- 28462","140104":"XIII KOTO KAMPAR -- 28453","140106":"SIAK HULU -- 28452","140107":"KAMPAR KIRI -- 28471","140108":"KAMPAR KIRI HILIR -- 28471","140109":"KAMPAR KIRI HULU -- 28471","140110":"TAPUNG -- 28464","140111":"TAPUNG HILIR -- 28464","140112":"TAPUNG HULU -- 28464","140113":"SALO -- 28451","140114":"RUMBIO JAYA -- 28458","140116":"PERHENTIAN RAJA -- 28462","140117":"KAMPAR TIMUR -- 28461","140118":"KAMPAR UTARA -- 28461","140119":"KAMPAR KIRI TENGAH -- 28471","140120":"GUNUNG SAHILAN -- 28471","140121":"KOTO KAMPAR HULU -- 28453","140201":"RENGAT -- 29351","140202":"RENGAT BARAT -- 29351","140203":"KELAYANG -- 29352","140204":"PASIR PENYU -- 29352","140205":"PERANAP -- 29354","140206":"SIBERIDA -- -","140207":"BATANG CENAKU -- 29355","140208":"BATANG GANGSAL -- -","140209":"LIRIK -- 29353","140210":"KUALA CENAKU -- 29335","140211":"SUNGAI LALA -- 29363","140212":"LUBUK BATU JAYA -- 29352","140213":"RAKIT KULIM -- 29352","140214":"BATANG PERANAP -- 29354","140301":"BENGKALIS -- 28711","140302":"BANTAN -- 28754","140303":"BUKIT BATU -- 28761","140309":"MANDAU -- 28784","140310":"RUPAT -- 28781","140311":"RUPAT UTARA -- 28781","140312":"SIAK KECIL -- 28771","140313":"PINGGIR -- 28784","140401":"RETEH -- 29273","140402":"ENOK -- 29272","140403":"KUALA INDRAGIRI -- 29281","140404":"TEMBILAHAN -- 29212","140405":"TEMPULING -- 29261","140406":"GAUNG ANAK SERKA -- 29253","140407":"MANDAH -- 29254","140408":"KATEMAN -- 29255","140409":"KERITANG -- 29274","140410":"TANAH MERAH -- 29271","140411":"BATANG TUAKA -- 29252","140412":"GAUNG -- 29282","140413":"TEMBILAHAN HULU -- 29213","140414":"KEMUNING -- 29274","140415":"PELANGIRAN -- 29255","140416":"TELUK BELENGKONG -- 29255","140417":"PULAU BURUNG -- 29256","140418":"CONCONG -- 29281","140419":"KEMPAS -- 29261","140420":"SUNGAI BATANG -- 29273","140501":"UKUI -- 28382","140502":"PANGKALAN KERINCI -- 28381","140503":"PANGKALAN KURAS -- 28382","140504":"PANGKALAN LESUNG -- 28382","140505":"LANGGAM -- 28381","140506":"PELALAWAN -- 28353","140507":"KERUMUTAN -- 28353","140508":"BUNUT -- 28383","140509":"TELUK MERANTI -- 28353","140510":"KUALA KAMPAR -- 28384","140511":"BANDAR SEI KIJANG -- 28383","140512":"BANDAR PETALANGAN -- 28384","140601":"UJUNG BATU -- 28554","140602":"ROKAN IV KOTO -- 28555","140603":"RAMBAH -- 28557","140604":"TAMBUSAI -- 28558","140605":"KEPENUHAN -- 28559","140606":"KUNTO DARUSSALAM -- 28556","140607":"RAMBAH SAMO -- 28565","140608":"RAMBAH HILIR -- 28557","140609":"TAMBUSAI UTARA -- 28558","140610":"BANGUN PURBA -- 28557","140611":"TANDUN -- 28554","140612":"KABUN -- 28554","140613":"BONAI DARUSSALAM -- 28559","140614":"PAGARAN TAPAH DARUSSALAM -- 28556","140615":"KEPENUHAN HULU -- 28559","140616":"PENDALIAN IV KOTO -- 28555","140701":"KUBU -- 28991","140702":"BANGKO -- 28912","140703":"TANAH PUTIH -- 28983","140704":"RIMBA MELINTANG -- 28953","140705":"BAGAN SINEMBAH -- 28992","140706":"PASIR LIMAU KAPAS -- 28991","140707":"SINABOI -- 28912","140708":"PUJUD -- 28983","140709":"TANAH PUTIH TANJUNG MELAWAN -- 28983","140710":"BANGKO PUSAKO -- -","140711":"SIMPANG KANAN -- 28992","140712":"BATU HAMPAR -- 28912","140713":"RANTAU KOPAR -- 28983","140714":"PEKAITAN -- 28912","140715":"KUBU BABUSSALAM -- 28991","140801":"SIAK -- 28771","140802":"SUNGAI APIT -- 28662","140803":"MINAS -- 28685","140804":"TUALANG -- 28772","140805":"SUNGAI MANDAU -- 28671","140806":"DAYUN -- 28671","140807":"KERINCI KANAN -- 28654","140808":"BUNGA RAYA -- 28763","140809":"KOTO GASIB -- 28671","140810":"KANDIS -- 28686","140811":"LUBUK DALAM -- 28654","140812":"SABAK AUH -- 28685","140813":"MEMPURA -- 28773","140814":"PUSAKO -- 28992","140901":"KUANTAN MUDIK -- 29564","140902":"KUANTAN TENGAH -- 29511","140903":"SINGINGI -- 29563","140904":"KUANTAN HILIR -- 29561","140905":"CERENTI -- 29555","140906":"BENAI -- 29566","140907":"GUNUNGTOAR -- 29565","140908":"SINGINGI HILIR -- 29563","140909":"PANGEAN -- 29553","140910":"LOGAS TANAH DARAT -- 29556","140911":"INUMAN -- 29565","140912":"HULU KUANTAN -- 29565","140913":"KUANTAN HILIR SEBERANG -- 29561","140914":"SENTAJO RAYA -- 29566","140915":"PUCUK RANTAU -- 29564","141001":"TEBING TINGGI -- 28753","141002":"RANGSANG BARAT -- 28755","141003":"RANGSANG -- 28755","141004":"TEBING TINGGI BARAT -- 28753","141005":"MERBAU -- 28752","141006":"PULAUMERBAU -- 28752","141007":"TEBING TINGGI TIMUR -- 28753","141008":"TASIK PUTRI PUYU -- 28752","147101":"SUKAJADI -- 28122","147102":"PEKANBARU KOTA -- 28114","147103":"SAIL -- 28131","147104":"LIMA PULUH -- 28144","147105":"SENAPELAN -- 28153","147106":"RUMBAI -- 28263","147107":"BUKIT RAYA -- 28284","147108":"TAMPAN -- 28291","147109":"MARPOYAN DAMAI -- 28125","147110":"TENAYAN RAYA -- 28286","147111":"PAYUNG SEKAKI -- 28292","147112":"RUMBAI PESISIR -- 28263","147201":"DUMAI BARAT -- 28821","147202":"DUMAI TIMUR -- 28811","147203":"BUKIT KAPUR -- 28882","147204":"SUNGAI SEMBILAN -- 28826","147205":"MEDANG KAMPAI -- 28825","147206":"DUMAI KOTA -- 28812","147207":"DUMAI SELATAN -- 28825","150101":"GUNUNG RAYA -- 37174","150102":"DANAU KERINCI -- 37172","150104":"SITINJAU LAUT -- 37171","150105":"AIR HANGAT -- 37161","150106":"GUNUNG KERINCI -- 37162","150107":"BATANG MERANGIN -- 37175","150108":"KELILING DANAU -- 37173","150109":"KAYU ARO -- 37163","150111":"AIR HANGAT TIMUR -- 37161","150115":"GUNUNG TUJUH -- 37163","150116":"SIULAK -- 37162","150117":"DEPATI TUJUH -- 37161","150118":"SIULAK MUKAI -- 37162","150119":"KAYU ARO BARAT -- 37163","150121":"AIR HANGAT BARAT -- 37161","150201":"JANGKAT -- 37372","150202":"BANGKO -- 37311","150203":"MUARA SIAU -- 37371","150204":"SUNGAI MANAU -- 37361","150205":"TABIR -- 37353","150206":"PAMENANG -- 37352","150207":"TABIR ULU -- 37356","150208":"TABIR SELATAN -- 37354","150209":"LEMBAH MASURAI -- 37372","150210":"BANGKO BARAT -- 37311","150211":"NALO TATAN -- -","150212":"BATANG MASUMAI -- 37311","150213":"PAMENANG BARAT -- 37352","150214":"TABIR ILIR -- 37353","150215":"TABIR TIMUR -- 37353","150216":"RENAH PEMBARAP -- 37361","150217":"PANGKALAN JAMBU -- 37361","150218":"SUNGAI TENANG -- 37372","150301":"BATANG ASAI -- 37485","150302":"LIMUN -- 37382","150303":"SAROLANGUN -- 37481","150304":"PAUH -- 37491","150305":"PELAWAN -- 37482","150306":"MANDIANGIN -- 37492","150307":"AIR HITAM -- 37491","150308":"BATHIN VIII -- 37481","150309":"SINGKUT -- 37482","150310":"CERMIN NAN GEDANG -- -","150401":"MERSAM -- 36654","150402":"MUARA TEMBESI -- 36653","150403":"MUARA BULIAN -- 36611","150404":"BATIN XXIV -- 36656","150405":"PEMAYUNG -- 36657","150406":"MARO SEBO ULU -- 36655","150407":"BAJUBANG -- 36611","150408":"MARO SEBO ILIR -- 36655","150501":"JAMBI LUAR KOTA -- 36361","150502":"SEKERNAN -- 36381","150503":"KUMPEH -- 36373","150504":"MARO SEBO -- 36382","150505":"MESTONG -- 36364","150506":"KUMPEH ULU -- 36373","150507":"SUNGAI BAHAR -- 36365","150508":"SUNGAI GELAM -- 36364","150510":"BAHAR SELATAN -- 36365","150601":"TUNGKAL ULU -- 36552","150602":"TUNGKAL ILIR -- 36555","150603":"PENGABUAN -- 36553","150604":"BETARA -- 36555","150605":"MERLUNG -- 36554","150606":"TEBING TINGGI -- 36552","150607":"BATANG ASAM -- 36552","150608":"RENAH MENDALUH -- 36554","150609":"MUARA PAPALIK -- 36554","150610":"SEBERANG KOTA -- 36511","150611":"BRAM ITAM -- 36514","150612":"KUALA BETARA -- 36555","150613":"SENYERANG -- 36553","150701":"MUARA SABAK TIMUR -- 36761","150702":"NIPAH PANJANG -- 36771","150703":"MENDAHARA -- 36764","150704":"RANTAU RASAU -- 36772","150705":"S A D U -- 36773","150706":"DENDANG -- 36763","150707":"MUARA SABAK BARAT -- 36761","150708":"KUALA JAMBI -- 36761","150709":"MENDAHARA ULU -- 36764","150710":"GERAGAI -- 36764","150711":"BERBAK -- 36572","150801":"TANAH TUMBUH -- 37255","150802":"RANTAU PANDAN -- 37261","150803":"PASAR MUARO BUNGO -- -","150804":"JUJUHAN -- 37257","150805":"TANAH SEPENGGAL -- 37263","150806":"PELEPAT -- 37262","150807":"LIMBUR LUBUK MENGKUANG -- 37211","150808":"MUKO-MUKO BATHIN VII -- -","150809":"PELEPAT ILIR -- 37252","150810":"BATIN II BABEKO -- -","150811":"BATHIN III -- 37211","150812":"BUNGO DANI -- 37211","150813":"RIMBO TENGAH -- 37211","150814":"BATHIN III ULU -- 37261","150815":"BATHIN II PELAYANG -- 37255","150816":"JUJUHAN ILIR -- 37257","150817":"TANAH SEPENGGAL LINTAS -- 37263","150901":"TEBO TENGAH -- 37571","150902":"TEBO ILIR -- 37572","150903":"TEBO ULU -- 37554","150904":"RIMBO BUJANG -- 37553","150905":"SUMAY -- 37573","150906":"VII KOTO -- 37259","150907":"RIMBO ULU -- 37553","150908":"RIMBO ILIR -- 37553","150909":"TENGAH ILIR -- 37572","150910":"SERAI SERUMPUN -- 37554","150911":"VII KOTO ILIR -- 37259","150912":"MUARA TABIR -- 37572","157101":"TELANAIPURA -- 36123","157102":"JAMBI SELATAN -- 36139","157103":"JAMBI TIMUR -- 36145","157104":"PASAR JAMBI -- 36112","157105":"PELAYANGAN -- 36251","157106":"DANAU TELUK -- 36262","157107":"KOTA BARU -- 36129","157108":"JELUTUNG -- 36134","157201":"SUNGAI PENUH -- 37111","157202":"PESISIR BUKIT -- 37111","157203":"HAMPARAN RAWANG -- 37152","157204":"TANAH KAMPUNG -- 37171","157205":"KUMUN DEBAI -- 37111","157206":"PONDOK TINGGI -- 37111","157207":"KOTO BARU -- 37152","157208":"SUNGAI BUNGKAL -- 37112","160107":"SOSOH BUAY RAYAP -- 32151","160108":"PENGANDONAN -- 32155","160109":"PENINJAUAN -- 32191","160113":"BATURAJA BARAT -- 32121","160114":"BATURAJA TIMUR -- 32111","160120":"ULU OGAN -- 32157","160121":"SEMIDANG AJI -- 32156","160122":"LUBUK BATANG -- 32192","160128":"LENGKITI -- 32158","160129":"SINAR PENINJAUAN -- 32159","160130":"LUBUK RAJA -- 32152","160131":"MUARA JAYA -- 32155","160202":"TANJUNG LUBUK -- 30671","160203":"PEDAMARAN -- 30672","160204":"MESUJI -- 30681","160205":"KAYU AGUNG -- 30618","160208":"SIRAH PULAU PADANG -- 30652","160211":"TULUNG SELAPAN -- 30655","160212":"PAMPANGAN -- 30654","160213":"LEMPUING -- 30657","160214":"AIR SUGIHAN -- 30656","160215":"SUNGAI MENANG -- 30681","160217":"JEJAWI -- 30652","160218":"CENGAL -- 30658","160219":"PANGKALAN LAMPAM -- 30659","160220":"MESUJI MAKMUR -- 30681","160221":"MESUJI RAYA -- 30681","160222":"LEMPUING JAYA -- 30657","160223":"TELUK GELAM -- 30673","160224":"PEDAMARAN TIMUR -- 30672","160301":"TANJUNG AGUNG -- 31355","160302":"MUARA ENIM -- 31311","160303":"RAMBANG DANGKU -- 31172","160304":"GUNUNG MEGANG -- 31352","160306":"GELUMBANG -- 31171","160307":"LAWANG KIDUL -- 31711","160308":"SEMENDE DARAT LAUT -- -","160309":"SEMENDE DARAT TENGAH -- -","160310":"SEMENDE DARAT ULU -- -","160311":"UJAN MAS -- 31351","160314":"LUBAI -- 31173","160315":"RAMBANG -- 31172","160316":"SUNGAI ROTAN -- 31357","160317":"LEMBAK -- 31171","160319":"BENAKAT -- 31626","160321":"KELEKAR -- 31171","160322":"MUARA BELIDA -- 31171","160323":"BELIMBING -- -","160324":"BELIDA DARAT -- -","160325":"LUBAI ULU -- -","160401":"TANJUNGSAKTI PUMU -- 31581","160406":"JARAI -- 31591","160407":"KOTA AGUNG -- 31462","160408":"PULAUPINANG -- 31461","160409":"MERAPI BARAT -- 31471","160410":"LAHAT -- 31414","160412":"PAJAR BULAN -- 31356","160415":"MULAK ULU -- 31453","160416":"KIKIM SELATAN -- 31452","160417":"KIKIM TIMUR -- 31452","160418":"KIKIM TENGAH -- 31452","160419":"KIKIM BARAT -- 31452","160420":"PSEKSU -- 31419","160421":"GUMAY TALANG -- 31419","160422":"PAGAR GUNUNG -- 31461","160423":"MERAPI TIMUR -- 31471","160424":"TANJUNG SAKTI PUMI -- 31581","160425":"GUMAY ULU -- 31461","160426":"MERAPI SELATAN -- 31471","160427":"TANJUNGTEBAT -- 31462","160428":"MUARAPAYANG -- 31591","160429":"SUKAMERINDU -- 31356","160501":"TUGUMULYO -- 31662","160502":"MUARA LAKITAN -- 31666","160503":"MUARA KELINGI -- 31663","160508":"JAYALOKA -- 31665","160509":"MUARA BELITI -- 31661","160510":"STL ULU TERAWAS -- 30771","160511":"SELANGIT -- 31625","160512":"MEGANG SAKTI -- 31657","160513":"PURWODADI -- 31668","160514":"BTS. ULU -- -","160518":"TIANG PUMPUNG KEPUNGUT -- 31661","160519":"SUMBER HARTA -- 30771","160520":"TUAH NEGERI -- 31663","160521":"SUKA KARYA -- 31665","160601":"SEKAYU -- 30711","160602":"LAIS -- 30757","160603":"SUNGAI KERUH -- 30757","160604":"BATANG HARI LEKO -- 30755","160605":"SANGA DESA -- 30759","160606":"BABAT TOMAN -- 30752","160607":"SUNGAI LILIN -- 30755","160608":"KELUANG -- 30754","160609":"BAYUNG LENCIR -- 30756","160610":"PLAKAT TINGGI -- 30758","160611":"LALAN -- 30758","160612":"TUNGKAL JAYA -- 30756","160613":"LAWANG WETAN -- 30752","160614":"BABAT SUPAT -- 30755","160701":"BANYUASIN I -- 30962","160702":"BANYUASIN II -- 30953","160703":"BANYUASIN III -- 30953","160704":"PULAU RIMAU -- 30959","160705":"BETUNG -- 30958","160706":"RAMBUTAN -- 30967","160707":"MUARA PADANG -- 30975","160708":"MUARA TELANG -- 30974","160709":"MAKARTI JAYA -- 30972","160710":"TALANG KELAPA -- 30961","160711":"RANTAU BAYUR -- 30968","160712":"TANJUNG LAGO -- 30961","160713":"MUARA SUGIHAN -- 30975","160714":"AIR SALEK -- 30975","160715":"TUNGKAL ILIR -- 30959","160716":"SUAK TAPEH -- 30958","160717":"SEMBAWA -- 30953","160718":"SUMBER MARGA TELANG -- 30974","160719":"AIR KUMBANG -- 30962","160801":"MARTAPURA -- 32315","160802":"BUAY MADANG -- 32361","160803":"BELITANG -- 32385","160804":"CEMPAKA -- 32384","160805":"BUAY PEMUKA PELIUNG -- -","160806":"MADANG SUKU II -- 32366","160807":"MADANG SUKU I -- 32362","160808":"SEMENDAWAI SUKU III -- 32386","160809":"BELITANG II -- 32383","160810":"BELITANG III -- 32385","160811":"BUNGA MAYANG -- 32381","160812":"BUAY MADANG TIMUR -- 32361","160813":"MADANG SUKU III -- 32366","160814":"SEMENDAWAI BARAT -- 32184","160815":"SEMENDAWAI TIMUR -- 32185","160816":"JAYAPURA -- 32381","160817":"BELITANG JAYA -- 32385","160818":"BELITANG MADANG RAYA -- 32362","160819":"BELITANG MULYA -- 32383","160820":"BUAY PEMUKA BANGSA RAJA -- 32361","160901":"MUARA DUA -- 32272","160902":"PULAU BERINGIN -- 32273","160903":"BANDING AGUNG -- 32274","160904":"MUARA DUA KISAM -- 32272","160905":"SIMPANG -- 32264","160906":"BUAY SANDANG AJI -- 32277","160907":"BUAY RUNJUNG -- 32278","160908":"MEKAKAU ILIR -- 32276","160909":"BUAY PEMACA -- 32265","160910":"KISAM TINGGI -- 32279","160911":"KISAM ILIR -- 32272","160912":"BUAY PEMATANG RIBU RANAU TENGAH -- 32274","160913":"WARKUK RANAU SELATAN -- 32274","160914":"RUNJUNG AGUNG -- 32278","160915":"SUNGAI ARE -- 32273","160916":"SINDANG DANAU -- 32273","160917":"BUANA PEMACA -- 32264","160918":"TIGA DIHAJI -- 32277","160919":"BUAY RAWAN -- 32211","161001":"MUARA KUANG -- 30865","161002":"TANJUNG BATU -- 30664","161003":"TANJUNG RAJA -- 30661","161004":"INDRALAYA -- 30862","161005":"PEMULUTAN -- 30653","161006":"RANTAU ALAI -- 30866","161007":"INDRALAYA UTARA -- 30862","161008":"INDRALAYA SELATAN -- 30862","161009":"PEMULUTAN SELATAN -- 30653","161010":"PEMULUTAN BARAT -- 30653","161011":"RANTAU PANJANG -- 30661","161012":"SUNGAI PINANG -- 30661","161013":"KANDIS -- 30867","161014":"RAMBANG KUANG -- 30869","161015":"LUBUK KELIAT -- 30868","161016":"PAYARAMAN -- 30664","161101":"MUARA PINANG -- 31592","161102":"PENDOPO -- 31593","161103":"ULU MUSI -- 31594","161104":"TEBING TINGGI -- 31453","161105":"LINTANG KANAN -- 31593","161106":"TALANG PADANG -- 31596","161107":"PASEMAH AIR KERUH -- 31595","161108":"SIKAP DALAM -- 31594","161109":"SALING -- 31453","161110":"PENDOPO BARAT -- 31593","161201":"TALANG UBI -- 31214","161202":"PENUKAL UTARA -- 31315","161203":"PENUKAL -- 31315","161204":"ABAB -- 31315","161205":"TANAH ABANG -- 31314","161301":"RUPIT -- 31654","161302":"RAWAS ULU -- 31656","161303":"NIBUNG -- 31667","161304":"RAWAS ILIR -- 31655","161305":"KARANG DAPO -- 31658","161306":"KARANG JAYA -- 31654","161307":"ULU RAWAS -- 31669","167101":"ILIR BARAT II -- 30141","167102":"SEBERANG ULU I -- 30257","167103":"SEBERANG ULU II -- 30267","167104":"ILIR BARAT I -- 30136","167105":"ILIR TIMUR I -- 30117","167106":"ILIR TIMUR II -- 30117","167107":"SUKARAMI -- 30151","167108":"SAKO -- 30163","167109":"KEMUNING -- 30127","167110":"KALIDONI -- 30114","167111":"BUKIT KECIL -- 30132","167112":"GANDUS -- 30147","167113":"KERTAPATI -- 30259","167114":"PLAJU -- 30268","167115":"ALANG-ALANG LEBAR -- 30154","167116":"SEMATANG BORANG -- 30161","167201":"PAGAR ALAM UTARA -- 31513","167202":"PAGAR ALAM SELATAN -- 31526","167203":"DEMPO UTARA -- 31521","167204":"DEMPO SELATAN -- 31521","167205":"DEMPO TENGAH -- 31521","167305":"LUBUK LINGGAU TIMUR II -- -","167306":"LUBUK LINGGAU BARAT II -- -","167307":"LUBUK LINGGAU SELATAN II -- -","167308":"LUBUK LINGGAU UTARA II -- -","167401":"PRABUMULIH BARAT -- 31122","167402":"PRABUMULIH TIMUR -- 31117","167403":"CAMBAI -- 31141","167404":"RAMBANG KPK TENGAH -- -","167405":"PRABUMULIH UTARA -- 31121","167406":"PRABUMULIH SELATAN -- 31124","170101":"KEDURANG -- 38553","170102":"SEGINIM -- 38552","170103":"PINO -- 38571","170104":"MANNA -- 38571","170105":"KOTA MANNA -- 38511","170106":"PINO RAYA -- 38571","170107":"KEDURANG ILIR -- 38553","170108":"AIR NIPIS -- 38571","170109":"ULU MANNA -- 38571","170110":"BUNGA MAS -- 38511","170111":"PASAR MANNA -- 38518","170206":"KOTA PADANG -- 39183","170207":"PADANG ULAK TANDING -- 39182","170208":"SINDANG KELINGI -- 39153","170209":"CURUP -- 39125","170210":"BERMANI ULU -- 39152","170211":"SELUPU REJANG -- 39153","170216":"CURUP UTARA -- 39125","170217":"CURUP TIMUR -- 39115","170218":"CURUP SELATAN -- 39125","170219":"CURUP TENGAH -- 39125","170220":"BINDURIANG -- 39182","170221":"SINDANG BELITI ULU -- 39182","170222":"SINDANG DATARAN -- -","170223":"SINDANG BELITI ILIR -- 39183","170224":"BERMANI ULU RAYA -- 39152","170301":"ENGGANO -- 38387","170306":"KERKAP -- 38374","170307":"KOTA ARGA MAKMUR -- -","170308":"GIRI MULYA -- -","170309":"PADANG JAYA -- 38657","170310":"LAIS -- 38653","170311":"BATIK NAU -- 38656","170312":"KETAHUN -- 38361","170313":"NAPAL PUTIH -- 38363","170314":"PUTRI HIJAU -- 38326","170315":"AIR BESI -- 38575","170316":"AIR NAPAL -- 38373","170319":"HULU PALIK -- 38374","170320":"AIR PADANG -- 38653","170321":"ARMA JAYA -- 38611","170323":"ULOK KUPAI -- 38363","170401":"KINAL -- 38962","170402":"TANJUNG KEMUNING -- 38955","170403":"KAUR UTARA -- 38956","170404":"KAUR TENGAH -- 38961","170405":"KAUR SELATAN -- 38963","170406":"MAJE -- 38965","170407":"NASAL -- 38964","170408":"SEMIDANG GUMAY -- -","170409":"KELAM TENGAH -- 38955","170410":"LUAS -- 38961","170411":"MUARA SAHUNG -- 38961","170412":"TETAP -- 38963","170413":"LUNGKANG KULE -- 38956","170414":"PADANG GUCI HILIR -- 38956","170415":"PADANG GUCI HULU -- 38956","170501":"SUKARAJA -- 38877","170502":"SELUMA -- 38883","170503":"TALO -- 38886","170504":"SEMIDANG ALAS -- 38873","170505":"SEMIDANG ALAS MARAS -- 38875","170506":"AIR PERIUKAN -- 38881","170507":"LUBUK SANDI -- 38882","170508":"SELUMA BARAT -- 38883","170509":"SELUMA TIMUR -- 38885","170510":"SELUMA UTARA -- 38884","170511":"SELUMA SELATAN -- 38878","170512":"TALO KECIL -- 38888","170513":"ULU TALO -- 38886","170514":"ILIR TALO -- 38887","170601":"LUBUK PINANG -- 38767","170602":"KOTA MUKOMUKO -- 38765","170603":"TERAS TERUNJAM -- 38768","170604":"PONDOK SUGUH -- 38766","170605":"IPUH -- 38764","170606":"MALIN DEMAN -- 38764","170607":"AIR RAMI -- 38764","170608":"TERAMANG JAYA -- 38766","170609":"SELAGAN RAYA -- 38768","170610":"PENARIK -- 38768","170611":"XIV KOTO -- 38765","170612":"V KOTO -- 38765","170613":"AIR MAJUNTO -- 38767","170614":"AIR DIKIT -- 38765","170615":"SUNGAI RUMBAI -- 38766","170701":"LEBONG UTARA -- 39264","170702":"LEBONG ATAS -- 39265","170703":"LEBONG TENGAH -- 39263","170704":"LEBONG SELATAN -- 39262","170705":"RIMBO PENGADANG -- 39261","170706":"TOPOS -- 39262","170707":"BINGIN KUNING -- 39262","170708":"LEBONG SAKTI -- 39267","170709":"PELABAI -- 39265","170710":"AMEN -- 39264","170711":"URAM JAYA -- 39268","170712":"PINANG BELAPIS -- 39269","170801":"BERMANI ILIR -- 39374","170802":"UJAN MAS -- 39371","170803":"TEBAT KARAI -- 39373","170804":"KEPAHIANG -- 39372","170805":"MERIGI -- 38383","170806":"KEBAWETAN -- 39372","170807":"SEBERANG MUSI -- 39373","170808":"MUARA KEMUMU -- 39374","170901":"KARANG TINGGI -- 38382","170902":"TALANG EMPAT -- 38385","170903":"PONDOK KELAPA -- 38371","170904":"PEMATANG TIGA -- 38372","170905":"PAGAR JATI -- 38383","170906":"TABA PENANJUNG -- 38386","170907":"MERIGI KELINDANG -- 38386","170908":"MERIGI SAKTI -- 38383","170909":"PONDOK KUBANG -- 38375","170910":"BANG HAJI -- 38372","177101":"SELEBAR -- 38214","177102":"GADING CEMPAKA -- 38221","177103":"TELUK SEGARA -- 38118","177104":"MUARA BANGKA HULU -- 38121","177105":"KAMPUNG MELAYU -- 38215","177106":"RATU AGUNG -- 38223","177107":"RATU SAMBAN -- 38222","177108":"SUNGAI SERUT -- 38119","177109":"SINGARAN PATI -- 38229","180104":"NATAR -- 35362","180105":"TANJUNG BINTANG -- 35361","180106":"KALIANDA -- 35551","180107":"SIDOMULYO -- 35353","180108":"KATIBUNG -- 35452","180109":"PENENGAHAN -- 35592","180110":"PALAS -- 35594","180113":"JATI AGUNG -- 35365","180114":"KETAPANG -- 35596","180115":"SRAGI -- 35597","180116":"RAJA BASA -- 35552","180117":"CANDIPURO -- 35356","180118":"MERBAU MATARAM -- 35357","180121":"BAKAUHENI -- 35592","180122":"TANJUNG SARI -- 35361","180123":"WAY SULAN -- 35452","180124":"WAY PANJI -- 35353","180201":"KALIREJO -- 34174","180202":"BANGUN REJO -- 34173","180203":"PADANG RATU -- 34175","180204":"GUNUNG SUGIH -- 34161","180205":"TRIMURJO -- 34172","180206":"PUNGGUR -- 34152","180207":"TERBANGGI BESAR -- 34163","180208":"SEPUTIH RAMAN -- 34155","180209":"RUMBIA -- 34157","180210":"SEPUTIH BANYAK -- 34156","180211":"SEPUTIH MATARAM -- 34164","180212":"SEPUTIH SURABAYA -- 34158","180213":"TERUSAN NUNYAI -- 34167","180214":"BUMI RATU NUBAN -- 34161","180215":"BEKRI -- 34162","180216":"SEPUTIH AGUNG -- 34166","180217":"WAY PANGUBUAN -- 35213","180218":"BANDAR MATARAM -- 34169","180219":"PUBIAN -- 34176","180220":"SELAGAI LINGGA -- 34176","180221":"ANAK TUHA -- 34161","180222":"SENDANG AGUNG -- 34174","180223":"KOTA GAJAH -- 34153","180224":"BUMI NABUNG -- 34168","180225":"WAY SEPUTIH -- 34179","180226":"BANDAR SURABAYA -- 34159","180227":"ANAK RATU AJI -- 35513","180228":"PUTRA RUMBIA -- 34157","180301":"BUKIT KEMUNING -- 34556","180302":"KOTABUMI -- 34511","180303":"SUNGKAI SELATAN -- 34554","180304":"TANJUNG RAJA -- 34557","180305":"ABUNG TIMUR -- 34583","180306":"ABUNG BARAT -- 34558","180307":"ABUNG SELATAN -- 34581","180308":"SUNGKAI UTARA -- 34555","180309":"KOTABUMI UTARA -- 34511","180310":"KOTABUMI SELATAN -- 34511","180311":"ABUNG TENGAH -- 34582","180312":"ABUNG TINGGI -- 34556","180313":"ABUNG SEMULI -- 34581","180314":"ABUNG SURAKARTA -- 34581","180315":"MUARA SUNGKAI -- 34559","180316":"BUNGA MAYANG -- 34555","180317":"HULU SUNGKAI -- 34555","180318":"SUNGKAI TENGAH -- 34555","180319":"ABUNG PEKURUN -- 34582","180320":"SUNGKAI JAYA -- 34554","180321":"SUNGKAI BARAT -- 34558","180322":"ABUNG KUNANG -- 34558","180323":"BLAMBANGAN PAGAR -- 34581","180404":"BALIK BUKIT -- 34811","180405":"SUMBER JAYA -- 34871","180406":"BELALAU -- 34872","180407":"WAY TENONG -- 34884","180408":"SEKINCAU -- 34885","180409":"SUOH -- 34882","180410":"BATU BRAK -- 34881","180411":"SUKAU -- 34879","180415":"GEDUNG SURIAN -- 34871","180418":"KEBUN TEBU -- 34871","180419":"AIR HITAM -- 34876","180420":"PAGAR DEWA -- 34885","180421":"BATU KETULIS -- 34872","180422":"LUMBOK SEMINUNG -- 34879","180423":"BANDAR NEGERI SUOH -- 34882","180502":"MENGGALA -- 34613","180506":"GEDUNG AJI -- 34681","180508":"BANJAR AGUNG -- 34682","180511":"GEDUNG MENENG -- 34596","180512":"RAWA JITU SELATAN -- 34596","180513":"PENAWAR TAMA -- 34595","180518":"RAWA JITU TIMUR -- 34596","180520":"BANJAR MARGO -- 34682","180522":"RAWA PITU -- 34595","180523":"PENAWAR AJI -- 34595","180525":"DENTE TELADAS -- 34596","180526":"MERAKSA AJI -- 34681","180527":"GEDUNG AJI BARU -- 34595","180601":"KOTA AGUNG -- 35384","180602":"TALANG PADANG -- 35377","180603":"WONOSOBO -- 35686","180604":"PULAU PANGGUNG -- 35679","180609":"CUKUH BALAK -- 35683","180611":"PUGUNG -- 35675","180612":"SEMAKA -- 35386","180613":"SUMBER REJO -- -","180615":"ULU BELU -- 35377","180616":"PEMATANG SAWA -- 35382","180617":"KLUMBAYAN -- -","180618":"KOTA AGUNG BARAT -- 35384","180619":"KOTA AGUNG TIMUR -- 35384","180620":"GISTING -- 35378","180621":"GUNUNG ALIP -- 35379","180624":"LIMAU -- 35613","180625":"BANDAR NEGERI SEMUONG -- 35686","180626":"AIR NANINGAN -- 35679","180627":"BULOK -- 35682","180628":"KLUMBAYAN BARAT -- -","180701":"SUKADANA -- 34194","180702":"LABUHAN MARINGGAI -- 34198","180703":"JABUNG -- 34384","180704":"PEKALONGAN -- 34391","180705":"SEKAMPUNG -- 34385","180706":"BATANGHARI -- 34381","180707":"WAY JEPARA -- 34396","180708":"PURBOLINGGO -- 34373","180709":"RAMAN UTARA -- 34371","180710":"METRO KIBANG -- 34331","180711":"MARGA TIGA -- 34386","180712":"SEKAMPUNG UDIK -- 34385","180713":"BATANGHARI NUBAN -- 34372","180714":"BUMI AGUNG -- 34763","180715":"BANDAR SRIBHAWONO -- -","180716":"MATARAM BARU -- 34199","180717":"MELINTING -- 34377","180718":"GUNUNG PELINDUNG -- 34388","180719":"PASIR SAKTI -- 34387","180720":"WAWAY KARYA -- 34376","180721":"LABUHAN RATU -- 35149","180722":"BRAJA SELEBAH -- -","180723":"WAY BUNGUR -- 34373","180724":"MARGA SEKAMPUNG -- 34384","180801":"BLAMBANGAN UMPU -- 34764","180802":"KASUI -- 34765","180803":"BANJIT -- 34766","180804":"BARADATU -- 34761","180805":"BAHUGA -- 34763","180806":"PAKUAN RATU -- 34762","180807":"NEGERI AGUNG -- 34769","180808":"WAY TUBA -- 34767","180809":"REBANG TANGKAS -- 34767","180810":"GUNUNG LABUHAN -- 34768","180811":"NEGARA BATIN -- 34769","180812":"NEGERI BESAR -- 34769","180813":"BUAY BAHUGA -- 34767","180814":"BUMI AGUNG -- 34763","180901":"GEDONG TATAAN -- 35366","180902":"NEGERI KATON -- 35353","180903":"TEGINENENG -- 35363","180904":"WAY LIMA -- 35367","180905":"PADANG CERMIN -- 35451","180906":"PUNDUH PIDADA -- 35453","180907":"KEDONDONG -- 35368","180908":"MARGA PUNDUH -- 35453","180909":"WAY KHILAU -- 35368","181001":"PRINGSEWU -- 35373","181002":"GADING REJO -- 35372","181003":"AMBARAWA -- 35376","181004":"PARDASUKA -- 35682","181005":"PAGELARAN -- 35376","181006":"BANYUMAS -- 35373","181007":"ADILUWIH -- 35674","181008":"SUKOHARJO -- 35674","181009":"PAGELARAN UTARA -- 35376","181101":"MESUJI -- 34697","181102":"MESUJI TIMUR -- 34697","181103":"RAWA JITU UTARA -- 34696","181104":"WAY SERDANG -- 34684","181105":"SIMPANG PEMATANG -- 34698","181106":"PANCA JAYA -- 34698","181107":"TANJUNG RAYA -- 34598","181201":"TULANG BAWANG TENGAH -- 34693","181202":"TUMIJAJAR -- 34594","181203":"TULANG BAWANG UDIK -- 34691","181204":"GUNUNG TERANG -- 34683","181205":"GUNUNG AGUNG -- 34683","181206":"WAY KENANGA -- 34388","181207":"LAMBU KIBANG -- 34388","181208":"PAGAR DEWA -- 34885","181301":"PESISIR TENGAH -- 34874","181302":"PESISIR SELATAN -- 34875","181303":"LEMONG -- 34877","181304":"PESISIR UTARA -- 34876","181305":"KARYA PENGGAWA -- 34878","181306":"PULAUPISANG -- 34876","181307":"WAY KRUI -- 34874","181308":"KRUI SELATAN -- 34874","181309":"NGAMBUR -- 34883","181310":"BENGKUNAT -- 34883","181311":"BENGKUNAT BELIMBING -- 34883","187101":"KEDATON -- 35141","187102":"SUKARAME -- 35131","187103":"TANJUNGKARANG BARAT -- 35151","187104":"PANJANG -- 35245","187105":"TANJUNGKARANG TIMUR -- 35121","187106":"TANJUNGKARANG PUSAT -- 35116","187107":"TELUKBETUNG SELATAN -- 35222","187108":"TELUKBETUNG BARAT -- 35238","187109":"TELUKBETUNG UTARA -- 35214","187110":"RAJABASA -- 35552","187111":"TANJUNG SENANG -- 35142","187112":"SUKABUMI -- 35122","187113":"KEMILING -- 35158","187114":"LABUHAN RATU -- 35149","187115":"WAY HALIM -- 35136","187116":"LANGKAPURA -- 35155","187117":"ENGGAL -- 34613","187118":"KEDAMAIAN -- 35122","187119":"TELUKBETUNG TIMUR -- 35235","187120":"BUMI WARAS -- 35228","187201":"METRO PUSAT -- 34113","187202":"METRO UTARA -- 34117","187203":"METRO BARAT -- 34114","187204":"METRO TIMUR -- 34112","187205":"METRO SELATAN -- 34119","190101":"SUNGAILIAT -- 33211","190102":"BELINYU -- 33253","190103":"MERAWANG -- 33172","190104":"MENDO BARAT -- 33173","190105":"PEMALI -- 33255","190106":"BAKAM -- 33252","190107":"RIAU SILIP -- 33253","190108":"PUDING BESAR -- 33179","190201":"TANJUNG PANDAN -- 33411","190202":"MEMBALONG -- 33452","190203":"SELAT NASIK -- 33481","190204":"SIJUK -- 33414","190205":"BADAU -- 33451","190301":"TOBOALI -- 33783","190302":"LEPAR PONGOK -- 33791","190303":"AIR GEGAS -- 33782","190304":"SIMPANG RIMBA -- 33777","190305":"PAYUNG -- 33778","190306":"TUKAK SADAI -- 33783","190307":"PULAUBESAR -- 33778","190308":"KEPULAUAN PONGOK -- 33791","190401":"KOBA -- 33681","190402":"PANGKALAN BARU -- 33684","190403":"SUNGAI SELAN -- 33675","190404":"SIMPANG KATIS -- 33674","190405":"NAMANG -- 33681","190406":"LUBUK BESAR -- 33681","190501":"MENTOK -- 33351","190502":"SIMPANG TERITIP -- 33366","190503":"JEBUS -- 33362","190504":"KELAPA -- 33364","190505":"TEMPILANG -- 33365","190506":"PARITTIGA -- 33362","190601":"MANGGAR -- 33512","190602":"GANTUNG -- 33562","190603":"DENDANG -- 33561","190604":"KELAPA KAMPIT -- 33571","190605":"DAMAR -- 33571","190606":"SIMPANG RENGGIANG -- 33562","190607":"SIMPANG PESAK -- 33561","197101":"BUKITINTAN -- 33149","197102":"TAMAN SARI -- 33121","197103":"PANGKAL BALAM -- 33113","197104":"RANGKUI -- 33135","197105":"GERUNGGANG -- 33123","197106":"GABEK -- 33111","197107":"GIRIMAYA -- 33141","210104":"GUNUNG KIJANG -- 29151","210106":"BINTAN TIMUR -- 29151","210107":"BINTAN UTARA -- 29152","210108":"TELUK BINTAN -- 29133","210109":"TAMBELAN -- 29193","210110":"TELOK SEBONG -- -","210112":"TOAPAYA -- 29151","210113":"MANTANG -- 29151","210114":"BINTAN PESISIR -- 29151","210115":"SERI KUALA LOBAM -- -","210201":"MORO -- 29663","210202":"KUNDUR -- 29662","210203":"KARIMUN -- 29661","210204":"MERAL -- 29664","210205":"TEBING -- 29663","210206":"BURU -- 29664","210207":"KUNDUR UTARA -- 29662","210208":"KUNDUR BARAT -- 29662","210209":"DURAI -- 29664","210210":"MERAL BARAT -- 29664","210211":"UNGAR -- 29662","210212":"BELAT -- 29662","210304":"MIDAI -- 29784","210305":"BUNGURAN BARAT -- 29782","210306":"SERASAN -- 29781","210307":"BUNGURAN TIMUR -- 29783","210308":"BUNGURAN UTARA -- 29783","210309":"SUBI -- 29781","210310":"PULAU LAUT -- 29783","210311":"PULAU TIGA -- 29783","210315":"BUNGURAN TIMUR LAUT -- 29783","210316":"BUNGURAN TENGAH -- 29783","210401":"SINGKEP -- 29875","210402":"LINGGA -- 29874","210403":"SENAYANG -- 29873","210404":"SINGKEP BARAT -- 29875","210405":"LINGGA UTARA -- 29874","210406":"SINGKEP PESISIR -- 29871","210407":"LINGGA TIMUR -- 29872","210408":"SELAYAR -- 29872","210409":"SINGKEP SELATAN -- -","210501":"SIANTAN -- 29783","210502":"PALMATAK -- 29783","210503":"SIANTAN TIMUR -- 29791","210504":"SIANTAN SELATAN -- 29791","210505":"JEMAJA TIMUR -- 29792","210506":"JEMAJA -- 29792","210507":"SIANTAN TENGAH -- 29783","217101":"BELAKANG PADANG -- 29413","217102":"BATU AMPAR -- 29452","217103":"SEKUPANG -- 29427","217104":"NONGSA -- 29466","217105":"BULANG -- 29474","217106":"LUBUK BAJA -- 29432","217107":"SEI BEDUK -- -","217108":"GALANG -- 29483","217109":"BENGKONG -- 29458","217110":"BATAM KOTA -- 29431","217111":"SAGULUNG -- 29439","217112":"BATU AJI -- 29438","217201":"TANJUNG PINANG BARAT -- 29111","217202":"TANJUNG PINANG TIMUR -- 29122","217203":"TANJUNG PINANG KOTA -- 29115","217204":"BUKIT BESTARI -- 29124","310101":"KEPULAUAN SERIBU UTARA -- 14540","310102":"KEPULAUAN SERIBU SELATAN. -- -","317101":"GAMBIR -- 10150","317102":"SAWAH BESAR -- 10720","317103":"KEMAYORAN -- 10640","317104":"SENEN -- 10460","317105":"CEMPAKA PUTIH -- 10520","317106":"MENTENG -- 10330","317107":"TANAH ABANG -- 10210","317108":"JOHAR BARU -- 10530","317201":"PENJARINGAN -- 14470","317202":"TANJUNG PRIOK -- 14320","317203":"KOJA -- 14210","317204":"CILINCING -- 14120","317205":"PADEMANGAN -- 14430","317206":"KELAPA GADING -- 14240","317301":"CENGKARENG -- 11730","317302":"GROGOL PETAMBURAN -- 11450","317303":"TAMAN SARI -- 11120","317304":"TAMBORA -- 11330","317305":"KEBON JERUK -- 11510","317306":"KALIDERES -- 11840","317307":"PAL MERAH -- 11430","317308":"KEMBANGAN -- 11640","317401":"TEBET -- 12840","317402":"SETIABUDI -- 12980","317403":"MAMPANG PRAPATAN -- 12730","317404":"PASAR MINGGU -- 12560","317405":"KEBAYORAN LAMA -- 12230","317406":"CILANDAK -- 12430","317407":"KEBAYORAN BARU -- 12150","317408":"PANCORAN -- 12770","317409":"JAGAKARSA -- 12630","317410":"PESANGGRAHAN -- 12330","317501":"MATRAMAN -- 13130","317502":"PULOGADUNG -- 13240","317503":"JATINEGARA -- 13310","317504":"KRAMATJATI -- 13530","317505":"PASAR REBO -- 13780","317506":"CAKUNG -- 13910","317507":"DUREN SAWIT -- 13440","317508":"MAKASAR -- 13620","317509":"CIRACAS -- 13720","317510":"CIPAYUNG -- 13890","320101":"CIBINONG -- 43271","320102":"GUNUNG PUTRI -- 16969","320103":"CITEUREUP -- 16810","320104":"SUKARAJA -- 16710","320105":"BABAKAN MADANG -- 16810","320106":"JONGGOL -- 16830","320107":"CILEUNGSI -- 16820","320108":"CARIU -- 16840","320109":"SUKAMAKMUR -- 16830","320110":"PARUNG -- 43357","320111":"GUNUNG SINDUR -- 16340","320112":"KEMANG -- 16310","320113":"BOJONG GEDE -- 16920","320114":"LEUWILIANG -- 16640","320115":"CIAMPEA -- 16620","320116":"CIBUNGBULANG -- 16630","320117":"PAMIJAHAN -- 16810","320118":"RUMPIN -- 16350","320119":"JASINGA -- 16670","320120":"PARUNG PANJANG -- 16360","320121":"NANGGUNG -- 16650","320122":"CIGUDEG -- 16660","320123":"TENJO -- 16370","320124":"CIAWI -- 16720","320125":"CISARUA -- 45355","320126":"MEGAMENDUNG -- 16770","320127":"CARINGIN -- 43154","320128":"CIJERUK -- 16740","320129":"CIOMAS -- 16610","320130":"DRAMAGA -- 16680","320131":"TAMANSARI -- 46196","320132":"KLAPANUNGGAL -- 16710","320133":"CISEENG -- 16120","320134":"RANCA BUNGUR -- 16310","320135":"SUKAJAYA -- 16660","320136":"TANJUNGSARI -- 16840","320137":"TAJURHALANG -- 16320","320138":"CIGOMBONG -- 16110","320139":"LEUWISADENG -- 16640","320140":"TENJOLAYA -- 16370","320201":"PELABUHANRATU -- -","320202":"SIMPENAN -- 43361","320203":"CIKAKAK -- 43365","320204":"BANTARGADUNG -- 43363","320205":"CISOLOK -- 43366","320206":"CIKIDANG -- 43367","320207":"LENGKONG -- 40262","320208":"JAMPANG TENGAH -- 43171","320209":"WARUNGKIARA -- 43362","320210":"CIKEMBAR -- 43157","320211":"CIBADAK -- 43351","320212":"NAGRAK -- 43356","320213":"PARUNGKUDA -- 43357","320214":"BOJONGGENTENG -- 43353","320215":"PARAKANSALAK -- 43355","320216":"CICURUG -- 43359","320217":"CIDAHU -- 43358","320218":"KALAPANUNGGAL -- 43354","320219":"KABANDUNGAN -- 43368","320220":"WALURAN -- 43175","320221":"JAMPANG KULON -- 43178","320222":"CIEMAS -- 43177","320223":"KALIBUNDER -- 43185","320224":"SURADE -- 43179","320225":"CIBITUNG -- 43172","320226":"CIRACAP -- 43176","320227":"GUNUNGGURUH -- 43156","320228":"CICANTAYAN -- 43155","320229":"CISAAT -- 43152","320230":"KADUDAMPIT -- 43153","320231":"CARINGIN -- 43154","320232":"SUKABUMI -- 43151","320233":"SUKARAJA -- 16710","320234":"KEBONPEDES -- 43194","320235":"CIREUNGHAS -- 43193","320236":"SUKALARANG -- 43191","320237":"PABUARAN -- 41262","320238":"PURABAYA -- 43187","320239":"NYALINDUNG -- 43196","320240":"GEGERBITUNG -- 43197","320241":"SAGARANTEN -- 43181","320242":"CURUGKEMBAR -- 43182","320243":"CIDOLOG -- 46352","320244":"CIDADAP -- 43183","320245":"TEGALBULEUD -- 43186","320246":"CIMANGGU -- 43178","320247":"CIAMBAR -- 43356","320301":"CIANJUR -- 43211","320302":"WARUNGKONDANG -- 43261","320303":"CIBEBER -- 43262","320304":"CILAKU -- 43285","320305":"CIRANJANG -- 43282","320306":"BOJONGPICUNG -- 43283","320307":"KARANGTENGAH -- 43281","320308":"MANDE -- 43292","320309":"SUKALUYU -- 43284","320310":"PACET -- 43253","320311":"CUGENANG -- 43252","320312":"CIKALONGKULON -- 43291","320313":"SUKARESMI -- 43254","320314":"SUKANAGARA -- 43264","320315":"CAMPAKA -- 41181","320316":"TAKOKAK -- 43265","320317":"KADUPANDAK -- 43268","320318":"PAGELARAN -- 43266","320319":"TANGGEUNG -- 43267","320320":"CIBINONG -- 43271","320321":"SINDANGBARANG -- 43272","320322":"AGRABINTA -- 43273","320323":"CIDAUN -- 43275","320324":"NARINGGUL -- 43274","320325":"CAMPAKAMULYA -- 43269","320326":"CIKADU -- 43284","320327":"GEKBRONG -- 43261","320328":"CIPANAS -- 43253","320329":"CIJATI -- 43284","320330":"LELES -- 44119","320331":"HAURWANGI -- 43283","320332":"PASIRKUDA -- 43267","320405":"CILEUNYI -- 40626","320406":"CIMENYAN -- -","320407":"CILENGKRANG -- 40615","320408":"BOJONGSOANG -- 40288","320409":"MARGAHAYU -- 40226","320410":"MARGAASIH -- 40214","320411":"KATAPANG -- 40921","320412":"DAYEUHKOLOT -- 40239","320413":"BANJARAN -- 40377","320414":"PAMEUNGPEUK -- 44175","320415":"PANGALENGAN -- 40378","320416":"ARJASARI -- 40379","320417":"CIMAUNG -- 40374","320425":"CICALENGKA -- 40395","320426":"NAGREG -- 40215","320427":"CIKANCUNG -- 40396","320428":"RANCAEKEK -- 40394","320429":"CIPARAY -- 40223","320430":"PACET -- 43253","320431":"KERTASARI -- 40386","320432":"BALEENDAH -- 40375","320433":"MAJALAYA -- 41371","320434":"SOLOKANJERUK -- 40376","320435":"PASEH -- 45381","320436":"IBUN -- 43185","320437":"SOREANG -- 40914","320438":"PASIRJAMBU -- 40972","320439":"CIWIDEY -- 40973","320440":"RANCABALI -- 40973","320444":"CANGKUANG -- 40238","320446":"KUTAWARINGIN -- 40911","320501":"GARUT KOTA -- 44113","320502":"KARANGPAWITAN -- 44182","320503":"WANARAJA -- 44183","320504":"TAROGONG KALER -- 44151","320505":"TAROGONG KIDUL -- 44151","320506":"BANYURESMI -- 44191","320507":"SAMARANG -- 44161","320508":"PASIRWANGI -- 44161","320509":"LELES -- 44119","320510":"KADUNGORA -- 44153","320511":"LEUWIGOONG -- 44192","320512":"CIBATU -- 44185","320513":"KERSAMANAH -- 44185","320514":"MALANGBONG -- 44188","320515":"SUKAWENING -- 44184","320516":"KARANGTENGAH -- 43281","320517":"BAYONGBONG -- 44162","320518":"CIGEDUG -- 44116","320519":"CILAWU -- 44181","320520":"CISURUPAN -- 44163","320521":"SUKARESMI -- 43254","320522":"CIKAJANG -- 44171","320523":"BANJARWANGI -- 44172","320524":"SINGAJAYA -- 44173","320525":"CIHURIP -- 44173","320526":"PEUNDEUY -- 41272","320527":"PAMEUNGPEUK -- 44175","320528":"CISOMPET -- 44174","320529":"CIBALONG -- 46185","320530":"CIKELET -- 44177","320531":"BUNGBULANG -- 44165","320532":"MEKARMUKTI -- 44165","320533":"PAKENJENG -- 44164","320534":"PAMULIHAN -- 45365","320535":"CISEWU -- 44166","320536":"CARINGIN -- 43154","320537":"TALEGONG -- 44167","320538":"BL. LIMBANGAN -- -","320539":"SELAAWI -- 44187","320540":"CIBIUK -- 44193","320541":"PANGATIKAN -- 44183","320542":"SUCINARAJA -- 44115","320601":"CIPATUJAH -- 46187","320602":"KARANGNUNGGAL -- 46186","320603":"CIKALONG -- 46195","320604":"PANCATENGAH -- 46194","320605":"CIKATOMAS -- 46193","320606":"CIBALONG -- 46185","320607":"PARUNGPONTENG -- 46185","320608":"BANTARKALONG -- 46187","320609":"BOJONGASIH -- 46475","320610":"CULAMEGA -- 46188","320611":"BOJONGGAMBIR -- 46475","320612":"SODONGHILIR -- 46473","320613":"TARAJU -- 46474","320614":"SALAWU -- 46471","320615":"PUSPAHIANG -- 46471","320616":"TANJUNGJAYA -- 46184","320617":"SUKARAJA -- 16710","320618":"SALOPA -- 46192","320619":"JATIWARAS -- 46185","320620":"CINEAM -- 46198","320621":"KARANG JAYA -- 46198","320622":"MANONJAYA -- 46197","320623":"GUNUNG TANJUNG -- 46418","320624":"SINGAPARNA -- 46418","320625":"MANGUNREJA -- 46462","320626":"SUKARAME -- 46461","320627":"CIGALONTANG -- 46463","320628":"LEUWISARI -- 46464","320629":"PADAKEMBANG -- 46466","320630":"SARIWANGI -- 46465","320631":"SUKARATU -- 46415","320632":"CISAYONG -- 46153","320633":"SUKAHENING -- 46155","320634":"RAJAPOLAH -- 46155","320635":"JAMANIS -- 46175","320636":"CIAWI -- 16720","320637":"KADIPATEN -- 45452","320638":"PAGERAGEUNG -- 46158","320639":"SUKARESIK -- 46418","320701":"CIAMIS -- 46217","320702":"CIKONENG -- 46261","320703":"CIJEUNGJING -- 46271","320704":"SADANANYA -- 46256","320705":"CIDOLOG -- 46352","320706":"CIHAURBEUTI -- 46262","320707":"PANUMBANGAN -- 46263","320708":"PANJALU -- 46264","320709":"KAWALI -- 46253","320710":"PANAWANGAN -- 46255","320711":"CIPAKU -- 46252","320712":"JATINAGARA -- 46273","320713":"RAJADESA -- 46254","320714":"SUKADANA -- 46272","320715":"RANCAH -- 46387","320716":"TAMBAKSARI -- 46388","320717":"LAKBOK -- 46385","320718":"BANJARSARI -- 46383","320719":"PAMARICAN -- 46382","320729":"CIMARAGAS -- 46381","320730":"CISAGA -- 46386","320731":"SINDANGKASIH -- 46268","320732":"BAREGBEG -- 46274","320733":"SUKAMANTRI -- 46264","320734":"LUMBUNG -- 46258","320735":"PURWADADI -- 46385","320801":"KADUGEDE -- 45561","320802":"CINIRU -- 45565","320803":"SUBANG -- 45586","320804":"CIWARU -- 45583","320805":"CIBINGBIN -- 45587","320806":"LURAGUNG -- 45581","320807":"LEBAKWANGI -- 45574","320808":"GARAWANGI -- 45571","320809":"KUNINGAN -- 45514","320810":"CIAWIGEBANG -- 45591","320811":"CIDAHU -- 43358","320812":"JALAKSANA -- 45554","320813":"CILIMUS -- 45556","320814":"MANDIRANCAN -- 45558","320815":"SELAJAMBE -- 45566","320816":"KRAMATMULYA -- 45553","320817":"DARMA -- 45562","320818":"CIGUGUR -- 45552","320819":"PASAWAHAN -- 45559","320820":"NUSAHERANG -- 45563","320821":"CIPICUNG -- 45592","320822":"PANCALANG -- 45557","320823":"JAPARA -- 45555","320824":"CIMAHI -- 40521","320825":"CILEBAK -- 45585","320826":"HANTARA -- 45564","320827":"KALIMANGGIS -- 45594","320828":"CIBEUREUM -- 46196","320829":"KARANG KANCANA -- 45584","320830":"MALEBER -- 45575","320831":"SINDANG AGUNG -- 45573","320832":"CIGANDAMEKAR -- 45556","320901":"WALED -- 45187","320902":"CILEDUG -- 45188","320903":"LOSARI -- 45192","320904":"PABEDILAN -- 45193","320905":"BABAKAN -- 40223","320906":"KARANGSEMBUNG -- 45186","320907":"LEMAHABANG -- 45183","320908":"SUSUKAN LEBAK -- 45185","320909":"SEDONG -- 45189","320910":"ASTANAJAPURA -- 45181","320911":"PANGENAN -- 45182","320912":"MUNDU -- 45173","320913":"BEBER -- 45172","320914":"TALUN -- 45171","320915":"SUMBER -- 45612","320916":"DUKUPUNTANG -- 45652","320917":"PALIMANAN -- 45161","320918":"PLUMBON -- 45155","320919":"WERU -- 45154","320920":"KEDAWUNG -- 45153","320921":"GUNUNG JATI -- 45151","320922":"KAPETAKAN -- 45152","320923":"KLANGENAN -- 45156","320924":"ARJAWINANGUN -- 45162","320925":"PANGURAGAN -- 45163","320926":"CIWARINGIN -- 45167","320927":"SUSUKAN -- 45166","320928":"GEGESIK -- 45164","320929":"KALIWEDI -- 45165","320930":"GEBANG -- 17151","320931":"DEPOK -- 45155","320932":"PASALEMAN -- 45187","320933":"PABUARAN -- 41262","320934":"KARANGWARENG -- 45186","320935":"TENGAH TANI -- 45153","320936":"PLERED -- 41162","320937":"GEMPOL -- 45161","320938":"GREGED -- 45172","320939":"SURANENGGALA -- 45152","320940":"JAMBLANG -- 45156","321001":"LEMAHSUGIH -- 45465","321002":"BANTARUJEG -- 45464","321003":"CIKIJING -- 45466","321004":"TALAGA -- 45463","321005":"ARGAPURA -- 45462","321006":"MAJA -- 16417","321007":"MAJALENGKA -- 45419","321008":"SUKAHAJI -- 45471","321009":"RAJAGALUH -- 45472","321010":"LEUWIMUNDING -- 45473","321011":"JATIWANGI -- 45454","321012":"DAWUAN -- 45453","321013":"KADIPATEN -- 45452","321014":"KERTAJATI -- 45457","321015":"JATITUJUH -- 45458","321016":"LIGUNG -- 45456","321017":"SUMBERJAYA -- 45468","321018":"PANYINGKIRAN -- 45459","321019":"PALASAH -- 45475","321020":"CIGASONG -- 45476","321021":"SINDANGWANGI -- 45474","321022":"BANJARAN -- 40377","321023":"CINGAMBUL -- 45467","321024":"KASOKANDEL -- 45453","321025":"SINDANG -- 45227","321026":"MALAUSMA -- 45464","321101":"WADO -- 45373","321102":"JATINUNGGAL -- 45376","321103":"DARMARAJA -- 45372","321104":"CIBUGEL -- 45375","321105":"CISITU -- 45363","321106":"SITURAJA -- 45371","321107":"CONGGEANG -- 45391","321108":"PASEH -- 45381","321109":"SURIAN -- 45393","321110":"BUAHDUA -- 45392","321111":"TANJUNGSARI -- 16840","321112":"SUKASARI -- 41254","321113":"PAMULIHAN -- 45365","321114":"CIMANGGUNG -- 45364","321115":"JATINANGOR -- 45363","321116":"RANCAKALONG -- 45361","321117":"SUMEDANG SELATAN -- 45311","321118":"SUMEDANG UTARA -- 45321","321119":"GANEAS -- 45356","321120":"TANJUNGKERTA -- 45354","321121":"TANJUNGMEDAR -- 45354","321122":"CIMALAKA -- 45353","321123":"CISARUA -- 45355","321124":"TOMO -- 45382","321125":"UJUNGJAYA -- 45383","321126":"JATIGEDE -- 45377","321201":"HAURGEULIS -- 45264","321202":"KROYA -- 45265","321203":"GABUSWETAN -- 45263","321204":"CIKEDUNG -- 45262","321205":"LELEA -- 45261","321206":"BANGODUA -- 45272","321207":"WIDASARI -- 45271","321208":"KERTASEMAYA -- 45274","321209":"KRANGKENG -- 45284","321210":"KARANGAMPEL -- 45283","321211":"JUNTINYUAT -- 45282","321212":"SLIYEG -- 45281","321213":"JATIBARANG -- 45273","321214":"BALONGAN -- 45217","321215":"INDRAMAYU -- 45214","321216":"SINDANG -- 45227","321217":"CANTIGI -- 45258","321218":"LOHBENER -- 45252","321219":"ARAHAN -- 45365","321220":"LOSARANG -- 45253","321221":"KANDANGHAUR -- 45254","321222":"BONGAS -- 45255","321223":"ANJATAN -- 45256","321224":"SUKRA -- 45257","321225":"GANTAR -- 45264","321226":"TRISI -- 45262","321227":"SUKAGUMIWANG -- 45274","321228":"KEDOKAN BUNDER -- 45283","321229":"PASEKAN -- 45219","321230":"TUKDANA -- 45272","321231":"PATROL -- 45257","321301":"SAGALAHERANG -- 41282","321302":"CISALAK -- 41283","321303":"SUBANG -- 45586","321304":"KALIJATI -- 41271","321305":"PABUARAN -- 41262","321306":"PURWADADI -- 46385","321307":"PAGADEN -- 41252","321308":"BINONG -- 43271","321309":"CIASEM -- 41256","321310":"PUSAKANAGARA -- 41255","321311":"PAMANUKAN -- 41254","321312":"JALANCAGAK -- 41281","321313":"BLANAKAN -- 41259","321314":"TANJUNGSIANG -- 41284","321315":"COMPRENG -- 41258","321316":"PATOKBEUSI -- 41263","321317":"CIBOGO -- 41285","321318":"CIPUNAGARA -- 41257","321319":"CIJAMBE -- 41286","321320":"CIPEUNDUEY -- -","321321":"LEGONKULON -- 41254","321322":"CIKAUM -- 41253","321323":"SERANGPANJANG -- 41282","321324":"SUKASARI -- 41254","321325":"TAMBAKDAHAN -- 41253","321326":"KASOMALANG -- 41283","321327":"DAWUAN -- 45453","321328":"PAGADEN BARAT -- 41252","321329":"CIATER -- 41281","321330":"PUSAKAJAYA -- 41255","321401":"PURWAKARTA -- 41113","321402":"CAMPAKA -- 41181","321403":"JATILUHUR -- 41161","321404":"PLERED -- 41162","321405":"SUKATANI -- 17630","321406":"DARANGDAN -- 41163","321407":"MANIIS -- 41166","321408":"TEGALWARU -- 41165","321409":"WANAYASA -- 41174","321410":"PASAWAHAN -- 45559","321411":"BOJONG -- 40232","321412":"BABAKANCIKAO -- 41151","321413":"BUNGURSARI -- 46151","321414":"CIBATU -- 44185","321415":"SUKASARI -- 41254","321416":"PONDOKSALAM -- 41115","321417":"KIARAPEDES -- 41175","321501":"KARAWANG BARAT -- 41311","321502":"PANGKALAN -- 41362","321503":"TELUKJAMBE TIMUR -- 41361","321504":"CIAMPEL -- 41363","321505":"KLARI -- 41371","321506":"RENGASDENGKLOK -- 41352","321507":"KUTAWALUYA -- 41358","321508":"BATUJAYA -- 41354","321509":"TIRTAJAYA -- 41357","321510":"PEDES -- 43194","321511":"CIBUAYA -- 41356","321512":"PAKISJAYA -- 41355","321513":"CIKAMPEK -- 41373","321514":"JATISARI -- 41374","321515":"CILAMAYA WETAN -- 41384","321516":"TIRTAMULYA -- 41372","321517":"TELAGASARI -- -","321518":"RAWAMERTA -- 41382","321519":"LEMAHABANG -- 45183","321520":"TEMPURAN -- 41385","321521":"MAJALAYA -- 41371","321522":"JAYAKERTA -- 41352","321523":"CILAMAYA KULON -- 41384","321524":"BANYUSARI -- 41374","321525":"KOTA BARU -- 41374","321526":"KARAWANG TIMUR -- 41314","321527":"TELUKJAMBE BARAT -- 41361","321528":"TEGALWARU -- 41165","321529":"PURWASARI -- 41373","321530":"CILEBAR -- 41353","321601":"TARUMAJAYA -- 17216","321602":"BABELAN -- 17610","321603":"SUKAWANGI -- 17620","321604":"TAMBELANG -- 17620","321605":"TAMBUN UTARA -- 17510","321606":"TAMBUN SELATAN -- 17510","321607":"CIBITUNG -- 43172","321608":"CIKARANG BARAT -- 17530","321609":"CIKARANG UTARA -- 17530","321610":"KARANG BAHAGIA -- 17530","321611":"CIKARANG TIMUR -- 17530","321612":"KEDUNG WARINGIN -- 17540","321613":"PEBAYURAN -- 17710","321614":"SUKAKARYA -- 17630","321615":"SUKATANI -- 17630","321616":"CABANGBUNGIN -- 17720","321617":"MUARAGEMBONG -- 17730","321618":"SETU -- 17320","321619":"CIKARANG SELATAN -- 17530","321620":"CIKARANG PUSAT -- 17530","321621":"SERANG BARU -- 17330","321622":"CIBARUSAH -- 17340","321623":"BOJONGMANGU -- 17352","321701":"LEMBANG -- 40391","321702":"PARONGPONG -- 40559","321703":"CISARUA -- 45355","321704":"CIKALONGWETAN -- 40556","321705":"CIPEUNDEUY -- 41272","321706":"NGAMPRAH -- 40552","321707":"CIPATAT -- 40554","321708":"PADALARANG -- 40553","321709":"BATUJAJAR -- 40561","321710":"CIHAMPELAS -- 40562","321711":"CILILIN -- 40562","321712":"CIPONGKOR -- 40564","321713":"RONGGA -- 40566","321714":"SINDANGKERTA -- 40563","321715":"GUNUNGHALU -- 40565","321716":"SAGULING -- 40561","321801":"PARIGI -- 46393","321802":"CIJULANG -- 46394","321803":"CIMERAK -- 46395","321804":"CIGUGUR -- 45552","321805":"LANGKAPLANCAR -- 46391","321806":"MANGUNJAYA -- 46371","321807":"PADAHERANG -- 46384","321808":"KALIPUCANG -- 46397","321809":"PANGANDARAN -- 46396","321810":"SIDAMULIH -- 46365","327101":"BOGOR SELATAN -- 16133","327102":"BOGOR TIMUR -- 16143","327103":"BOGOR TENGAH -- 16126","327104":"BOGOR BARAT -- 16116","327105":"BOGOR UTARA -- 16153","327106":"TANAH SAREAL -- -","327201":"GUNUNG PUYUH -- 43123","327202":"CIKOLE -- 43113","327203":"CITAMIANG -- 43142","327204":"WARUDOYONG -- 43132","327205":"BAROS -- 43161","327206":"LEMBURSITU -- 43168","327207":"CIBEUREUM -- 46196","327301":"SUKASARI -- 41254","327302":"COBLONG -- 40131","327303":"BABAKAN CIPARAY -- 40223","327304":"BOJONGLOA KALER -- 40232","327305":"ANDIR -- 40184","327306":"CICENDO -- 40172","327307":"SUKAJADI -- 40162","327308":"CIDADAP -- 43183","327309":"BANDUNG WETAN -- 40114","327310":"ASTANA ANYAR -- 40241","327311":"REGOL -- 40254","327312":"BATUNUNGGAL -- 40275","327313":"LENGKONG -- 40262","327314":"CIBEUNYING KIDUL -- 40121","327315":"BANDUNG KULON -- 40212","327316":"KIARACONDONG -- 40283","327317":"BOJONGLOA KIDUL -- 40239","327318":"CIBEUNYING KALER -- 40191","327319":"SUMUR BANDUNG -- 40117","327320":"ANTAPANI -- 40291","327321":"BANDUNG KIDUL -- 40266","327322":"BUAHBATU -- 40287","327323":"RANCASARI -- 40292","327324":"ARCAMANIK -- 40293","327325":"CIBIRU -- 40614","327326":"UJUNG BERUNG -- 40611","327327":"GEDEBAGE -- 40294","327328":"PANYILEUKAN -- 40614","327329":"CINAMBO -- 40294","327330":"MANDALAJATI -- 40195","327401":"KEJAKSAN -- 45121","327402":"LEMAH WUNGKUK -- 45114","327403":"HARJAMUKTI -- 45145","327404":"PEKALIPAN -- 45115","327405":"KESAMBI -- 45133","327501":"BEKASI TIMUR -- 17111","327502":"BEKASI BARAT -- 17136","327503":"BEKASI UTARA -- 17123","327504":"BEKASI SELATAN -- 17146","327505":"RAWA LUMBU -- 17117","327506":"MEDAN SATRIA -- 17143","327507":"BANTAR GEBANG -- 17151","327508":"PONDOK GEDE -- 17412","327509":"JATIASIH -- 17422","327510":"JATI SEMPURNA -- -","327511":"MUSTIKA JAYA -- 17155","327512":"PONDOK MELATI -- 17414","327601":"PANCORAN MAS -- 16432","327602":"CIMANGGIS -- 16452","327603":"SAWANGAN -- 16519","327604":"LIMO -- 16512","327605":"SUKMAJAYA -- 16417","327606":"BEJI -- 16422","327607":"CIPAYUNG -- 16436","327608":"CILODONG -- 16414","327609":"CINERE -- 16514","327610":"TAPOS -- 16458","327611":"BOJONGSARI -- 16516","327701":"CIMAHI SELATAN -- 40531","327702":"CIMAHI TENGAH -- 40521","327703":"CIMAHI UTARA -- 40513","327801":"CIHIDEUNG -- 46122","327802":"CIPEDES -- 46133","327803":"TAWANG -- 46114","327804":"INDIHIANG -- 46151","327805":"KAWALU -- 46182","327806":"CIBEUREUM -- 46196","327807":"TAMANSARI -- 46196","327808":"MANGKUBUMI -- 46181","327809":"BUNGURSARI -- 46151","327810":"PURBARATU -- 46196","327901":"BANJAR -- 46312","327902":"PATARUMAN -- 46326","327903":"PURWAHARJA -- 46332","327904":"LANGENSARI -- 46325","330101":"KEDUNGREJA -- 53263","330102":"KESUGIHAN -- 53274","330103":"ADIPALA -- 53271","330104":"BINANGUN -- 53281","330105":"NUSAWUNGU -- 53283","330106":"KROYA -- 53282","330107":"MAOS -- 53272","330108":"JERUKLEGI -- 53252","330109":"KAWUNGANTEN -- 53253","330110":"GANDRUNGMANGU -- 53254","330111":"SIDAREJA -- 53261","330112":"KARANGPUCUNG -- 53255","330113":"CIMANGGU -- 53256","330114":"MAJENANG -- 53257","330115":"WANAREJA -- 53265","330116":"DAYEUHLUHUR -- 53266","330117":"SAMPANG -- 53273","330118":"CIPARI -- 53262","330119":"PATIMUAN -- 53264","330120":"BANTARSARI -- 53281","330121":"CILACAP SELATAN -- 53211","330122":"CILACAP TENGAH -- 53222","330123":"CILACAP UTARA -- 53231","330124":"KAMPUNG LAUT -- 53253","330201":"LUMBIR -- 53177","330202":"WANGON -- 53176","330203":"JATILAWANG -- 53174","330204":"RAWALO -- 53173","330205":"KEBASEN -- 53172","330206":"KEMRANJEN -- 53194","330207":"SUMPIUH -- 53195","330208":"TAMBAK -- 59174","330209":"SOMAGEDE -- 53193","330210":"KALIBAGOR -- 53191","330211":"BANYUMAS -- 53192","330212":"PATIKRAJA -- 53171","330213":"PURWOJATI -- 53175","330214":"AJIBARANG -- 53163","330215":"GUMELAR -- 53165","330216":"PEKUNCEN -- 53164","330217":"CILONGOK -- 53162","330218":"KARANGLEWAS -- 53161","330219":"SOKARAJA -- 53181","330220":"KEMBARAN -- 53182","330221":"SUMBANG -- 53183","330222":"BATURRADEN -- -","330223":"KEDUNGBANTENG -- 53152","330224":"PURWOKERTO SELATAN -- 53146","330225":"PURWOKERTO BARAT -- 53133","330226":"PURWOKERTO TIMUR -- 53113","330227":"PURWOKERTO UTARA -- 53121","330301":"KEMANGKON -- 53381","330302":"BUKATEJA -- 53382","330303":"KEJOBONG -- 53392","330304":"KALIGONDANG -- 53391","330305":"PURBALINGGA -- 53316","330306":"KALIMANAH -- 53371","330307":"KUTASARI -- 53361","330308":"MREBET -- 53352","330309":"BOBOTSARI -- 53353","330310":"KARANGREJA -- 53357","330311":"KARANGANYAR -- 59582","330312":"KARANGMONCOL -- 53355","330313":"REMBANG -- 53356","330314":"BOJONGSARI -- 53362","330315":"PADAMARA -- 53372","330316":"PENGADEGAN -- 53393","330317":"KARANGJAMBU -- 53357","330318":"KERTANEGARA -- 53354","330401":"SUSUKAN -- 50777","330402":"PURWOREJA KLAMPOK -- -","330403":"MANDIRAJA -- 53473","330404":"PURWANEGARA -- -","330405":"BAWANG -- 53471","330406":"BANJARNEGARA -- 53418","330407":"SIGALUH -- 53481","330408":"MADUKARA -- 53482","330409":"BANJARMANGU -- 53452","330410":"WANADADI -- 53461","330411":"RAKIT -- 53463","330412":"PUNGGELAN -- 53462","330413":"KARANGKOBAR -- 53453","330414":"PAGENTAN -- 53455","330415":"PEJAWARAN -- 53454","330416":"BATUR -- 53456","330417":"WANAYASA -- 53457","330418":"KALIBENING -- 53458","330419":"PANDANARUM -- 53458","330420":"PAGEDONGAN -- 53418","330501":"AYAH -- 54473","330502":"BUAYAN -- 54474","330503":"PURING -- 54383","330504":"PETANAHAN -- 54382","330505":"KLIRONG -- 54381","330506":"BULUSPESANTREN -- 54391","330507":"AMBAL -- 54392","330508":"MIRIT -- 54395","330509":"PREMBUN -- 54394","330510":"KUTOWINANGUN -- 54393","330511":"ALIAN -- 56153","330512":"KEBUMEN -- 54317","330513":"PEJAGOAN -- 54361","330514":"SRUWENG -- 54362","330515":"ADIMULYO -- 54363","330516":"KUWARASAN -- 54366","330517":"ROWOKELE -- 54472","330518":"SEMPOR -- 54421","330519":"GOMBONG -- 54416","330520":"KARANGANYAR -- 59582","330521":"KARANGGAYAM -- 54365","330522":"SADANG -- 54353","330523":"BONOROWO -- 54395","330524":"PADURESO -- 54394","330525":"PONCOWARNO -- 54393","330526":"KARANGSAMBUNG -- 54353","330601":"GRABAG -- 54265","330602":"NGOMBOL -- 54172","330603":"PURWODADI -- 54173","330604":"BAGELEN -- 54174","330605":"KALIGESING -- 54175","330606":"PURWOREJO -- 54118","330607":"BANYUURIP -- 54171","330608":"BAYAN -- 54224","330609":"KUTOARJO -- 54211","330610":"BUTUH -- 54264","330611":"PITURUH -- 54263","330612":"KEMIRI -- 54262","330613":"BRUNO -- 54261","330614":"GEBANG -- 54191","330615":"LOANO -- 54181","330616":"BENER -- 54183","330701":"WADASLINTANG -- 56365","330702":"KEPIL -- 56374","330703":"SAPURAN -- 56373","330704":"KALIWIRO -- 56364","330705":"LEKSONO -- 56362","330706":"SELOMERTO -- 56361","330707":"KALIKAJAR -- 56372","330708":"KERTEK -- 56371","330709":"WONOSOBO -- 56318","330710":"WATUMALANG -- 56352","330711":"MOJOTENGAH -- 56351","330712":"GARUNG -- 56353","330713":"KEJAJAR -- 56354","330714":"SUKOHARJO -- 57551","330715":"KALIBAWANG -- 56373","330801":"SALAMAN -- 56162","330802":"BOROBUDUR -- 56553","330803":"NGLUWAR -- 56485","330804":"SALAM -- 56162","330805":"SRUMBUNG -- 56483","330806":"DUKUN -- 56482","330807":"SAWANGAN -- 56481","330808":"MUNTILAN -- 56415","330809":"MUNGKID -- 56512","330810":"MERTOYUDAN -- 56172","330811":"TEMPURAN -- 56161","330812":"KAJORAN -- 56163","330813":"KALIANGKRIK -- 56153","330814":"BANDONGAN -- 56151","330815":"CANDIMULYO -- 56191","330816":"PAKIS -- 56193","330817":"NGABLAK -- 56194","330818":"GRABAG -- 54265","330819":"TEGALREJO -- 56192","330820":"SECANG -- 56195","330821":"WINDUSARI -- 56152","330901":"SELO -- 56361","330902":"AMPEL -- 52364","330903":"CEPOGO -- 57362","330904":"MUSUK -- 57331","330905":"BOYOLALI -- 57313","330906":"MOJOSONGO -- 57322","330907":"TERAS -- 57372","330908":"SAWIT -- 57374","330909":"BANYUDONO -- 57373","330910":"SAMBI -- 57376","330911":"NGEMPLAK -- 57375","330912":"NOGOSARI -- 57378","330913":"SIMO -- 57377","330914":"KARANGGEDE -- 57381","330915":"KLEGO -- 57385","330916":"ANDONG -- 57384","330917":"KEMUSU -- 57383","330918":"WONOSEGORO -- 57382","330919":"JUWANGI -- 57391","331001":"PRAMBANAN -- 57454","331002":"GANTIWARNO -- 57455","331003":"WEDI -- 57461","331004":"BAYAT -- 57462","331005":"CAWAS -- 57463","331006":"TRUCUK -- 57467","331007":"KEBONARUM -- 57486","331008":"JOGONALAN -- 57452","331009":"MANISRENGGO -- 57485","331010":"KARANGNONGKO -- 57483","331011":"CEPER -- 57465","331012":"PEDAN -- 57468","331013":"KARANGDOWO -- 57464","331014":"JUWIRING -- 57472","331015":"WONOSARI -- 57473","331016":"DELANGGU -- 57471","331017":"POLANHARJO -- 57474","331018":"KARANGANOM -- 57475","331019":"TULUNG -- 57482","331020":"JATINOM -- 57481","331021":"KEMALANG -- 57484","331022":"NGAWEN -- 58254","331023":"KALIKOTES -- 57451","331024":"KLATEN UTARA -- 57438","331025":"KLATEN TENGAH -- 57414","331026":"KLATEN SELATAN -- 57425","331101":"WERU -- 57562","331102":"BULU -- 54391","331103":"TAWANGSARI -- 57561","331104":"SUKOHARJO -- 57551","331105":"NGUTER -- 57571","331106":"BENDOSARI -- 57528","331107":"POLOKARTO -- 57555","331108":"MOJOLABAN -- 57554","331109":"GROGOL -- 57552","331110":"BAKI -- 57556","331111":"GATAK -- 57557","331112":"KARTASURA -- 57169","331201":"PRACIMANTORO -- 57664","331202":"GIRITONTRO -- 57678","331203":"GIRIWOYO -- 57675","331204":"BATUWARNO -- 57674","331205":"TIRTOMOYO -- 57672","331206":"NGUNTORONADI -- 57671","331207":"BATURETNO -- 57673","331208":"EROMOKO -- 57663","331209":"WURYANTORO -- 57661","331210":"MANYARAN -- 57662","331211":"SELOGIRI -- 57652","331212":"WONOGIRI -- 57615","331213":"NGADIROJO -- 57681","331214":"SIDOHARJO -- 57281","331215":"JATIROTO -- 57692","331216":"KISMANTORO -- 57696","331217":"PURWANTORO -- 57695","331218":"BULUKERTO -- 57697","331219":"SLOGOHIMO -- 57694","331220":"JATISRONO -- 57691","331221":"JATIPURNO -- 57693","331222":"GIRIMARTO -- 57683","331223":"KARANGTENGAH -- 59561","331224":"PARANGGUPITO -- 57678","331225":"PUHPELEM -- 57698","331301":"JATIPURO -- 57784","331302":"JATIYOSO -- 57785","331303":"JUMAPOLO -- 57783","331304":"JUMANTONO -- 57782","331305":"MATESIH -- 57781","331306":"TAWANGMANGU -- 57792","331307":"NGARGOYOSO -- 57793","331308":"KARANGPANDAN -- 57791","331309":"KARANGANYAR -- 59582","331310":"TASIKMADU -- 57722","331311":"JATEN -- 57731","331312":"COLOMADU -- 57171","331313":"GONDANGREJO -- 57188","331314":"KEBAKKRAMAT -- 57762","331315":"MOJOGEDANG -- 57752","331316":"KERJO -- 57753","331317":"JENAWI -- 57794","331401":"KALIJAMBE -- 57275","331402":"PLUPUH -- 57283","331403":"MASARAN -- 57282","331404":"KEDAWUNG -- 57292","331405":"SAMBIREJO -- 57293","331406":"GONDANG -- 53391","331407":"SAMBUNGMACAN -- 57253","331408":"NGRAMPAL -- 57252","331409":"KARANGMALANG -- 57222","331410":"SRAGEN -- 57216","331411":"SIDOHARJO -- 57281","331412":"TANON -- 57277","331413":"GEMOLONG -- 57274","331414":"MIRI -- 57276","331415":"SUMBERLAWANG -- 57272","331416":"MONDOKAN -- 57271","331417":"SUKODONO -- 57263","331418":"GESI -- 57262","331419":"TANGEN -- 57261","331420":"JENAR -- 57256","331501":"KEDUNGJATI -- 58167","331502":"KARANGRAYUNG -- 58163","331503":"PENAWANGAN -- 58161","331504":"TOROH -- 58171","331505":"GEYER -- 58172","331506":"PULOKULON -- 58181","331507":"KRADENAN -- 58182","331508":"GABUS -- 59173","331509":"NGARINGAN -- 58193","331510":"WIROSARI -- 58192","331511":"TAWANGHARJO -- 58191","331512":"GROBOGAN -- 58152","331513":"PURWODADI -- 54173","331514":"BRATI -- 58153","331515":"KLAMBU -- 58154","331516":"GODONG -- 58162","331517":"GUBUG -- 58164","331518":"TEGOWANU -- 58165","331519":"TANGGUNGHARJO -- 58166","331601":"JATI -- 53174","331602":"RANDUBLATUNG -- 58382","331603":"KRADENAN -- 58182","331604":"KEDUNGTUBAN -- 58381","331605":"CEPU -- 58311","331606":"SAMBONG -- 58371","331607":"JIKEN -- 58372","331608":"JEPON -- 58261","331609":"BLORA -- 58219","331610":"TUNJUNGAN -- 58252","331611":"BANJAREJO -- 58253","331612":"NGAWEN -- 58254","331613":"KUNDURAN -- 58255","331614":"TODANAN -- 58256","331615":"BOGOREJO -- 58262","331616":"JAPAH -- 58257","331701":"SUMBER -- 59253","331702":"BULU -- 54391","331703":"GUNEM -- 59263","331704":"SALE -- 59265","331705":"SARANG -- 59274","331706":"SEDAN -- 59264","331707":"PAMOTAN -- 59261","331708":"SULANG -- 59254","331709":"KALIORI -- 59252","331710":"REMBANG -- 53356","331711":"PANCUR -- 59262","331712":"KRAGAN -- 59273","331713":"SLUKE -- 59272","331714":"LASEM -- 59271","331801":"SUKOLILO -- 59172","331802":"KAYEN -- 59171","331803":"TAMBAKROMO -- 59174","331804":"WINONG -- 59181","331805":"PUCAKWANGI -- 59183","331806":"JAKEN -- 59184","331807":"BATANGAN -- 59186","331808":"JUWANA -- 59185","331809":"JAKENAN -- 59182","331810":"PATI -- 59114","331811":"GABUS -- 59173","331812":"MARGOREJO -- 59163","331813":"GEMBONG -- 59162","331814":"TLOGOWUNGU -- 59161","331815":"WEDARIJAKSA -- 59152","331816":"MARGOYOSO -- 59154","331817":"GUNUNGWUNGKAL -- 59156","331818":"CLUWAK -- 59157","331819":"TAYU -- 59155","331820":"DUKUHSETI -- 59158","331821":"TRANGKIL -- 59153","331901":"KALIWUNGU -- 59332","331902":"KOTA KUDUS -- -","331903":"JATI -- 53174","331904":"UNDAAN -- 59372","331905":"MEJOBO -- 59381","331906":"JEKULO -- 59382","331907":"BAE -- 59325","331908":"GEBOG -- 59333","331909":"DAWE -- 59353","332001":"KEDUNG -- 51173","332002":"PECANGAAN -- 59462","332003":"WELAHAN -- 59464","332004":"MAYONG -- 59465","332005":"BATEALIT -- 59461","332006":"JEPARA -- 59432","332007":"MLONGGO -- 59452","332008":"BANGSRI -- 59453","332009":"KELING -- 59454","332010":"KARIMUN JAWA -- 59455","332011":"TAHUNAN -- 59422","332012":"NALUMSARI -- 59466","332013":"KALINYAMATAN -- 59462","332014":"KEMBANG -- 59453","332015":"PAKIS AJI -- 59452","332016":"DONOROJO -- 59454","332101":"MRANGGEN -- 59567","332102":"KARANGAWEN -- 59566","332103":"GUNTUR -- 59565","332104":"SAYUNG -- 59563","332105":"KARANGTENGAH -- 59561","332106":"WONOSALAM -- 59571","332107":"DEMPET -- 59573","332108":"GAJAH -- 59581","332109":"KARANGANYAR -- 59582","332110":"MIJEN -- 59583","332111":"DEMAK -- 59517","332112":"BONANG -- 59552","332113":"WEDUNG -- 59554","332114":"KEBONAGUNG -- 59583","332201":"GETASAN -- 50774","332202":"TENGARAN -- 50775","332203":"SUSUKAN -- 50777","332204":"SURUH -- 50776","332205":"PABELAN -- 50771","332206":"TUNTANG -- 50773","332207":"BANYUBIRU -- 50664","332208":"JAMBU -- 50663","332209":"SUMOWONO -- 50662","332210":"AMBARAWA -- 50614","332211":"BAWEN -- 50661","332212":"BRINGIN -- 50772","332213":"BERGAS -- 50552","332215":"PRINGAPUS -- 50214","332216":"BANCAK -- 50182","332217":"KALIWUNGU -- 59332","332218":"UNGARAN BARAT -- 50517","332219":"UNGARAN TIMUR -- 50519","332220":"BANDUNGAN -- 50614","332301":"BULU -- 54391","332302":"TEMBARAK -- 56261","332303":"TEMANGGUNG -- 56211","332304":"PRINGSURAT -- 56272","332305":"KALORAN -- 56282","332306":"KANDANGAN -- 56281","332307":"KEDU -- 51173","332308":"PARAKAN -- 56254","332309":"NGADIREJO -- 56255","332310":"JUMO -- 56256","332311":"TRETEP -- 56259","332312":"CANDIROTO -- 56257","332313":"KRANGGAN -- 56271","332314":"TLOGOMULYO -- 56263","332315":"SELOPAMPANG -- 56262","332316":"BANSARI -- 56265","332317":"KLEDUNG -- 56264","332318":"BEJEN -- 56258","332319":"WONOBOYO -- 56266","332320":"GEMAWANG -- 56283","332401":"PLANTUNGAN -- 51362","332402":"PAGERUYUNG -- -","332403":"SUKOREJO -- 51363","332404":"PATEAN -- 51364","332405":"SINGOROJO -- 51382","332406":"LIMBANGAN -- 51383","332407":"BOJA -- 51381","332408":"KALIWUNGU -- 59332","332409":"BRANGSONG -- 51371","332410":"PEGANDON -- 51357","332411":"GEMUH -- 51356","332412":"WELERI -- 51355","332413":"CEPIRING -- 51352","332414":"PATEBON -- 51351","332415":"KENDAL -- 51312","332416":"ROWOSARI -- 51354","332417":"KANGKUNG -- 51353","332418":"RINGINARUM -- 51356","332419":"NGAMPEL -- 51357","332420":"KALIWUNGU SELATAN -- 51372","332501":"WONOTUNGGAL -- 51253","332502":"BANDAR -- 51254","332503":"BLADO -- 51255","332504":"REBAN -- 51273","332505":"BAWANG -- 53471","332506":"TERSONO -- 51272","332507":"GRINGSING -- 51281","332508":"LIMPUNG -- 51271","332509":"SUBAH -- 51262","332510":"TULIS -- 51261","332511":"BATANG -- 59186","332512":"WARUNGASEM -- 51252","332513":"KANDEMAN -- 51261","332514":"PECALUNGAN -- 51262","332515":"BANYUPUTIH -- 51281","332601":"KANDANGSERANG -- 51163","332602":"PANINGGARAN -- 51164","332603":"LEBAKBARANG -- 51183","332604":"PETUNGKRIYONO -- 51193","332605":"TALUN -- 51192","332606":"DORO -- 51191","332607":"KARANGANYAR -- 59582","332608":"KAJEN -- 51161","332609":"KESESI -- 51162","332610":"SRAGI -- 51155","332611":"BOJONG -- 51156","332612":"WONOPRINGGO -- 51181","332613":"KEDUNGWUNI -- 51173","332614":"BUARAN -- 51171","332615":"TIRTO -- 57672","332616":"WIRADESA -- 51152","332617":"SIWALAN -- 51137","332618":"KARANGDADAP -- 51174","332619":"WONOKERTO -- 51153","332701":"MOGA -- 52354","332702":"PULOSARI -- 52355","332703":"BELIK -- 52356","332704":"WATUKUMPUL -- 52357","332705":"BODEH -- 52365","332706":"BANTARBOLANG -- 52352","332707":"RANDUDONGKAL -- 52353","332708":"PEMALANG -- 52319","332709":"TAMAN -- 52361","332710":"PETARUKAN -- 52362","332711":"AMPELGADING -- 52364","332712":"COMAL -- 52363","332713":"ULUJAMI -- 52371","332714":"WARUNGPRING -- 52354","332801":"MARGASARI -- 52463","332802":"BUMIJAWA -- 52466","332803":"BOJONG -- 51156","332804":"BALAPULANG -- 52464","332805":"PAGERBARANG -- 52462","332806":"LEBAKSIU -- 52461","332807":"JATINEGARA -- 52473","332808":"KEDUNGBANTENG -- 53152","332809":"PANGKAH -- 52471","332810":"SLAWI -- 52419","332811":"ADIWERNA -- 52194","332812":"TALANG -- 52193","332813":"DUKUHTURI -- 52192","332814":"TARUB -- 52184","332815":"KRAMAT -- 57762","332816":"SURADADI -- -","332817":"WARUREJA -- -","332818":"DUKUHWARU -- 52451","332901":"SALEM -- 52275","332902":"BANTARKAWUNG -- 52274","332903":"BUMIAYU -- 52273","332904":"PAGUYANGAN -- 52276","332905":"SIRAMPOG -- 52272","332906":"TONJONG -- 52271","332907":"JATIBARANG -- 52261","332908":"WANASARI -- 52252","332909":"BREBES -- 52216","332910":"SONGGOM -- 52266","332911":"KERSANA -- 52264","332912":"LOSARI -- 52255","332913":"TANJUNG -- 52254","332914":"BULAKAMBA -- 52253","332915":"LARANGAN -- 52262","332916":"KETANGGUNGAN -- 52263","332917":"BANJARHARJO -- 52265","337101":"MAGELANG SELATAN -- 56123","337102":"MAGELANG UTARA -- 56114","337103":"MAGELANG TENGAH -- 56121","337201":"LAWEYAN -- 57149","337202":"SERENGAN -- 57156","337203":"PASAR KLIWON -- 57144","337204":"JEBRES -- 57122","337205":"BANJARSARI -- 57137","337301":"SIDOREJO -- 50715","337302":"TINGKIR -- 50743","337303":"ARGOMULYO -- 50736","337304":"SIDOMUKTI -- 50722","337401":"SEMARANG TENGAH -- 50138","337402":"SEMARANG UTARA -- 50175","337403":"SEMARANG TIMUR -- 50126","337404":"GAYAMSARI -- 50248","337405":"GENUK -- 50115","337406":"PEDURUNGAN -- 50246","337407":"SEMARANG SELATAN -- 50245","337408":"CANDISARI -- 50257","337409":"GAJAHMUNGKUR -- 50235","337410":"TEMBALANG -- 50277","337411":"BANYUMANIK -- 50264","337412":"GUNUNGPATI -- 50223","337413":"SEMARANG BARAT -- 50141","337414":"MIJEN -- 59583","337415":"NGALIYAN -- 50211","337416":"TUGU -- 50151","337501":"PEKALONGAN BARAT -- 51119","337502":"PEKALONGAN TIMUR -- 51129","337503":"PEKALONGAN UTARA -- 51143","337504":"PEKALONGAN SELATAN -- 51139","337601":"TEGAL BARAT -- 52115","337602":"TEGAL TIMUR -- 52124","337603":"TEGAL SELATAN -- 52137","337604":"MARGADANA -- 52147","340101":"TEMON -- 55654","340102":"WATES -- 55651","340103":"PANJATAN -- 55655","340104":"GALUR -- 55661","340105":"LENDAH -- 55663","340106":"SENTOLO -- 55664","340107":"PENGASIH -- 55652","340108":"KOKAP -- 55653","340109":"GIRIMULYO -- 55674","340110":"NANGGULAN -- 55671","340111":"SAMIGALUH -- 55673","340112":"KALIBAWANG -- 55672","340201":"SRANDAKAN -- 55762","340202":"SANDEN -- 55763","340203":"KRETEK -- 55772","340204":"PUNDONG -- 55771","340205":"BAMBANG LIPURO -- 55764","340206":"PANDAK -- 55761","340207":"PAJANGAN -- 55751","340208":"BANTUL -- 55711","340209":"JETIS -- 55231","340210":"IMOGIRI -- 55782","340211":"DLINGO -- 55783","340212":"BANGUNTAPAN -- 55198","340213":"PLERET -- 55791","340214":"PIYUNGAN -- 55792","340215":"SEWON -- 55188","340216":"KASIHAN -- 55184","340217":"SEDAYU -- 55752","340301":"WONOSARI -- 55811","340302":"NGLIPAR -- 55852","340303":"PLAYEN -- 55861","340304":"PATUK -- 55862","340305":"PALIYAN -- 55871","340306":"PANGGANG -- 55872","340307":"TEPUS -- 55881","340308":"SEMANU -- 55893","340309":"KARANGMOJO -- 55891","340310":"PONJONG -- 55892","340311":"RONGKOP -- 55883","340312":"SEMIN -- 55854","340313":"NGAWEN -- 55853","340314":"GEDANGSARI -- 55863","340315":"SAPTOSARI -- 55871","340316":"GIRISUBO -- 55883","340317":"TANJUNGSARI -- 55881","340318":"PURWOSARI -- 55872","340401":"GAMPING -- 55294","340402":"GODEAN -- 55264","340403":"MOYUDAN -- 55563","340404":"MINGGIR -- 55562","340405":"SEYEGAN -- 55561","340406":"MLATI -- 55285","340407":"DEPOK -- 55281","340408":"BERBAH -- 55573","340409":"PRAMBANAN -- 55572","340410":"KALASAN -- 55571","340411":"NGEMPLAK -- 55584","340412":"NGAGLIK -- 55581","340413":"SLEMAN -- 55515","340414":"TEMPEL -- 55552","340415":"TURI -- 55551","340416":"PAKEM -- 55582","340417":"CANGKRINGAN -- 55583","347101":"TEGALREJO -- 55243","347102":"JETIS -- 55231","347103":"GONDOKUSUMAN -- 55225","347104":"DANUREJAN -- 55211","347105":"GEDONGTENGEN -- 55272","347106":"NGAMPILAN -- 55261","347107":"WIROBRAJAN -- 55253","347108":"MANTRIJERON -- 55142","347109":"KRATON -- 55132","347110":"GONDOMANAN -- 55122","347111":"PAKUALAMAN -- 55111","347112":"MERGANGSAN -- 55153","347113":"UMBULHARJO -- 55163","347114":"KOTAGEDE -- 55172","350101":"DONOROJO -- 63554","350102":"PRINGKUKU -- 63552","350103":"PUNUNG -- 63553","350104":"PACITAN -- 63516","350105":"KEBONAGUNG -- 63561","350106":"ARJOSARI -- 63581","350107":"NAWANGAN -- 63584","350108":"BANDAR -- 61462","350109":"TEGALOMBO -- 63582","350110":"TULAKAN -- 63571","350111":"NGADIROJO -- 63572","350112":"SUDIMORO -- 63573","350201":"SLAHUNG -- 63463","350202":"NGRAYUN -- 63464","350203":"BUNGKAL -- 63462","350204":"SAMBIT -- 63474","350205":"SAWOO -- 63475","350206":"SOOKO -- 63482","350207":"PULUNG -- 63481","350208":"MLARAK -- 63472","350209":"JETIS -- 61352","350210":"SIMAN -- 62164","350211":"BALONG -- 61173","350212":"KAUMAN -- 66261","350213":"BADEGAN -- 63455","350214":"SAMPUNG -- 63454","350215":"SUKOREJO -- 63453","350216":"BABADAN -- 63491","350217":"PONOROGO -- 63419","350218":"JENANGAN -- 63492","350219":"NGEBEL -- 63493","350220":"JAMBON -- 63456","350221":"PUDAK -- 63418","350301":"PANGGUL -- 66364","350302":"MUNJUNGAN -- 66365","350303":"PULE -- 66362","350304":"DONGKO -- 66363","350305":"TUGU -- 66318","350306":"KARANGAN -- 63257","350307":"KAMPAK -- 66373","350308":"WATULIMO -- 66382","350309":"BENDUNGAN -- 66351","350310":"GANDUSARI -- 66187","350311":"TRENGGALEK -- 66318","350312":"POGALAN -- 66371","350313":"DURENAN -- 66381","350314":"SURUH -- 66362","350401":"TULUNGAGUNG -- 66218","350402":"BOYOLANGU -- 66233","350403":"KEDUNGWARU -- 66229","350404":"NGANTRU -- 66252","350405":"KAUMAN -- 66261","350406":"PAGERWOJO -- 66262","350407":"SENDANG -- 66254","350408":"KARANGREJO -- 66253","350409":"GONDANG -- 67174","350410":"SUMBERGEMPOL -- 66291","350411":"NGUNUT -- 66292","350412":"PUCANGLABAN -- 66284","350413":"REJOTANGAN -- 66293","350414":"KALIDAWIR -- 66281","350415":"BESUKI -- 66275","350416":"CAMPURDARAT -- 66272","350417":"BANDUNG -- 66274","350418":"PAKEL -- 66273","350419":"TANGGUNGGUNUNG -- 66283","350501":"WONODADI -- 66155","350502":"UDANAWU -- 66154","350503":"SRENGAT -- 66152","350504":"KADEMANGAN -- 66161","350505":"BAKUNG -- 66163","350506":"PONGGOK -- 66153","350507":"SANANKULON -- 66151","350508":"WONOTIRTO -- 66173","350509":"NGLEGOK -- 66181","350510":"KANIGORO -- 66171","350511":"GARUM -- 66182","350512":"SUTOJAYAN -- 66172","350513":"PANGGUNGREJO -- 66174","350514":"TALUN -- 66183","350515":"GANDUSARI -- 66187","350516":"BINANGUN -- 62293","350517":"WLINGI -- 66184","350518":"DOKO -- 66186","350519":"KESAMBEN -- 61484","350520":"WATES -- 64174","350521":"SELOREJO -- 66192","350522":"SELOPURO -- 66184","350601":"SEMEN -- 64161","350602":"MOJO -- 61382","350603":"KRAS -- 64172","350604":"NGADILUWIH -- 64171","350605":"KANDAT -- 64173","350606":"WATES -- 64174","350607":"NGANCAR -- 64291","350608":"PUNCU -- 64292","350609":"PLOSOKLATEN -- 64175","350610":"GURAH -- 64181","350611":"PAGU -- 64183","350612":"GAMPENGREJO -- 64182","350613":"GROGOL -- 64151","350614":"PAPAR -- 64153","350615":"PURWOASRI -- 64154","350616":"PLEMAHAN -- 64155","350617":"PARE -- 65166","350618":"KEPUNG -- 64293","350619":"KANDANGAN -- 64294","350620":"TAROKAN -- 64152","350621":"KUNJANG -- 64156","350622":"BANYAKAN -- 64157","350623":"RINGINREJO -- 64176","350624":"KAYEN KIDUL -- 64183","350625":"NGASEM -- 62154","350626":"BADAS -- 64221","350701":"DONOMULYO -- 65167","350702":"PAGAK -- 65168","350703":"BANTUR -- 65179","350704":"SUMBERMANJING WETAN -- 65176","350705":"DAMPIT -- 65181","350706":"AMPELGADING -- 65183","350707":"PONCOKUSUMO -- 65157","350708":"WAJAK -- 65173","350709":"TUREN -- 65175","350710":"GONDANGLEGI -- 65174","350711":"KALIPARE -- 65166","350712":"SUMBERPUCUNG -- 65165","350713":"KEPANJEN -- 65163","350714":"BULULAWANG -- 65171","350715":"TAJINAN -- 65172","350716":"TUMPANG -- 65156","350717":"JABUNG -- 65155","350718":"PAKIS -- 65154","350719":"PAKISAJI -- 65162","350720":"NGAJUNG -- 65164","350721":"WAGIR -- 65158","350722":"DAU -- 65151","350723":"KARANG PLOSO -- 65152","350724":"SINGOSARI -- 65153","350725":"LAWANG -- 65171","350726":"PUJON -- 65391","350727":"NGANTANG -- 65392","350728":"KASEMBON -- 65393","350729":"GEDANGAN -- 61254","350730":"TIRTOYUDO -- 65183","350731":"KROMENGAN -- 65165","350732":"WONOSARI -- 65164","350733":"PAGELARAN -- 65174","350801":"TEMPURSARI -- 67375","350802":"PRONOJIWO -- 67374","350803":"CANDIPURO -- 67373","350804":"PASIRIAN -- 67372","350805":"TEMPEH -- 67371","350806":"KUNIR -- 67292","350807":"YOSOWILANGUN -- 67382","350808":"ROWOKANGKUNG -- 67359","350809":"TEKUNG -- 67381","350810":"LUMAJANG -- 67316","350811":"PASRUJAMBE -- 67361","350812":"SENDURO -- 67361","350813":"GUCIALIT -- 67353","350814":"PADANG -- 67352","350815":"SUKODONO -- 61258","350816":"KEDUNGJAJANG -- 67358","350817":"JATIROTO -- 67355","350818":"RANDUAGUNG -- 67354","350819":"KLAKAH -- 67356","350820":"RANUYOSO -- 67357","350821":"SUMBERSUKO -- 67316","350901":"JOMBANG -- 61419","350902":"KENCONG -- 68167","350903":"SUMBERBARU -- 68156","350904":"GUMUKMAS -- 68165","350905":"UMBULSARI -- 68166","350906":"TANGGUL -- 61272","350907":"SEMBORO -- 68157","350908":"PUGER -- 68164","350909":"BANGSALSARI -- 68154","350910":"BALUNG -- 68161","350911":"WULUHAN -- 68162","350912":"AMBULU -- 68172","350913":"RAMBIPUJI -- 68152","350914":"PANTI -- 68153","350915":"SUKORAMBI -- 68151","350916":"JENGGAWAH -- 68171","350917":"AJUNG -- 68175","350918":"TEMPUREJO -- 68173","350919":"KALIWATES -- 68131","350920":"PATRANG -- 68118","350921":"SUMBERSARI -- 68125","350922":"ARJASA -- 69491","350923":"MUMBULSARI -- 68174","350924":"PAKUSARI -- 68181","350925":"JELBUK -- 68192","350926":"MAYANG -- 62184","350927":"KALISAT -- 68193","350928":"LEDOKOMBO -- 68196","350929":"SUKOWONO -- 68194","350930":"SILO -- 68184","350931":"SUMBERJAMBE -- 68195","351001":"PESANGGARAN -- 68488","351002":"BANGOREJO -- 68487","351003":"PURWOHARJO -- 68483","351004":"TEGALDLIMO -- 68484","351005":"MUNCAR -- 68472","351006":"CLURING -- 68482","351007":"GAMBIRAN -- 68486","351008":"SRONO -- 68471","351009":"GENTENG -- 69482","351010":"GLENMORE -- 68466","351011":"KALIBARU -- 68467","351012":"SINGOJURUH -- 68464","351013":"ROGOJAMPI -- 68462","351014":"KABAT -- 68461","351015":"GLAGAH -- 68431","351016":"BANYUWANGI -- 68419","351017":"GIRI -- 68424","351018":"WONGSOREJO -- 68453","351019":"SONGGON -- 68463","351020":"SEMPU -- 68468","351021":"KALIPURO -- 68455","351022":"SILIRAGUNG -- 68488","351023":"TEGALSARI -- 68485","351024":"LICIN -- 68454","351101":"MAESAN -- 68262","351102":"TAMANAN -- 68263","351103":"TLOGOSARI -- 68272","351104":"SUKOSARI -- 68287","351105":"PUJER -- 68271","351106":"GRUJUGAN -- 68261","351107":"CURAHDAMI -- 68251","351108":"TENGGARANG -- 68281","351109":"WONOSARI -- 65164","351110":"TAPEN -- 68283","351111":"BONDOWOSO -- 68214","351112":"WRINGIN -- 68252","351113":"TEGALAMPEL -- 68291","351114":"KLABANG -- 68284","351115":"CERMEE -- 68286","351116":"PRAJEKAN -- 68285","351117":"PAKEM -- 68253","351118":"SUMBERWRINGIN -- 68287","351119":"SEMPOL -- 68288","351120":"BINAKAL -- 68251","351121":"TAMAN KROCOK -- 68291","351122":"BOTOLINGGO -- 68284","351123":"JAMBESARI DARUS SHOLAH -- 68261","351201":"JATIBANTENG -- 68357","351202":"BESUKI -- 66275","351203":"SUBOH -- 68354","351204":"MLANDINGAN -- 68353","351205":"KENDIT -- 68352","351206":"PANARUKAN -- 68351","351207":"SITUBONDO -- 68311","351208":"PANJI -- 68321","351209":"MANGARAN -- 68363","351210":"KAPONGAN -- 68362","351211":"ARJASA -- 69491","351212":"JANGKAR -- 68372","351213":"ASEMBAGUS -- 68373","351214":"BANYUPUTIH -- 68374","351215":"SUMBERMALANG -- 68355","351216":"BANYUGLUGUR -- 68359","351217":"BUNGATAN -- 68358","351301":"SUKAPURA -- 67254","351302":"SUMBER -- 68355","351303":"KURIPAN -- 67262","351304":"BANTARAN -- 67261","351305":"LECES -- 67273","351306":"BANYUANYAR -- 67275","351307":"TIRIS -- 67287","351308":"KRUCIL -- 67288","351309":"GADING -- 65183","351310":"PAKUNIRAN -- 67292","351311":"KOTAANYAR -- 67293","351312":"PAITON -- 67291","351313":"BESUK -- 67283","351314":"KRAKSAAN -- 67282","351315":"KREJENGAN -- 67284","351316":"PEJARAKAN -- -","351317":"MARON -- 67276","351318":"GENDING -- 67272","351319":"DRINGU -- 67271","351320":"TEGALSIWALAN -- 67274","351321":"SUMBERASIH -- 67251","351322":"WONOMERTO -- 67253","351323":"TONGAS -- 67252","351324":"LUMBANG -- 67183","351401":"PURWODADI -- 67163","351402":"TUTUR -- 67165","351403":"PUSPO -- 67176","351404":"LUMBANG -- 67183","351405":"PASREPAN -- 67175","351406":"KEJAYAN -- 67172","351407":"WONOREJO -- 67173","351408":"PURWOSARI -- 67162","351409":"SUKOREJO -- 63453","351410":"PRIGEN -- 67157","351411":"PANDAAN -- 67156","351412":"GEMPOL -- 66291","351413":"BEJI -- 67154","351414":"BANGIL -- 62364","351415":"REMBANG -- 60179","351416":"KRATON -- 67151","351417":"POHJENTREK -- 67171","351418":"GONDANGWETAN -- 67174","351419":"WINONGAN -- 67182","351420":"GRATI -- 67184","351421":"NGULING -- 67185","351422":"LEKOK -- 67186","351423":"REJOSO -- 67181","351424":"TOSARI -- 67177","351501":"TARIK -- 61265","351502":"PRAMBON -- 64484","351503":"KREMBUNG -- 61275","351504":"PORONG -- 61274","351505":"JABON -- 61276","351506":"TANGGULANGIN -- 61272","351507":"CANDI -- 61271","351508":"SIDOARJO -- 61225","351509":"TULANGAN -- 61273","351510":"WONOAYU -- 61261","351511":"KRIAN -- 61262","351512":"BALONGBENDO -- 61263","351513":"TAMAN -- 63137","351514":"SUKODONO -- 61258","351515":"BUDURAN -- 61252","351516":"GEDANGAN -- 61254","351517":"SEDATI -- 61253","351518":"WARU -- 69353","351601":"JATIREJO -- 61373","351602":"GONDANG -- 67174","351603":"PACET -- 61374","351604":"TRAWAS -- 61375","351605":"NGORO -- 61473","351606":"PUNGGING -- 61384","351607":"KUTOREJO -- 61383","351608":"MOJOSARI -- 61382","351609":"DLANGGU -- 61371","351610":"BANGSAL -- 68154","351611":"PURI -- 61363","351612":"TROWULAN -- 61362","351613":"SOOKO -- 63482","351614":"GEDEG -- 61351","351615":"KEMLAGI -- 61353","351616":"JETIS -- 61352","351617":"DAWARBLANDONG -- 61354","351618":"MOJOANYAR -- 61364","351701":"PERAK -- 61461","351702":"GUDO -- 61463","351703":"NGORO -- 61473","351704":"BARENG -- 61474","351705":"WONOSALAM -- 61476","351706":"MOJOAGUNG -- 61482","351707":"MOJOWARNO -- 61475","351708":"DIWEK -- 61471","351709":"JOMBANG -- 61419","351710":"PETERONGAN -- 61481","351711":"SUMOBITO -- 61483","351712":"KESAMBEN -- 61484","351713":"TEMBELANG -- 61452","351714":"PLOSO -- 65152","351715":"PLANDAAN -- 61456","351716":"KABUH -- 61455","351717":"KUDU -- 61454","351718":"BANDARKEDUNGMULYO -- 61462","351719":"JOGOROTO -- 61485","351720":"MEGALUH -- 61457","351721":"NGUSIKAN -- 61454","351801":"SAWAHAN -- 63162","351802":"NGETOS -- 64474","351803":"BERBEK -- 64473","351804":"LOCERET -- 64471","351805":"PACE -- 64472","351806":"PRAMBON -- 64484","351807":"NGRONGGOT -- 64395","351808":"KERTOSONO -- 64311","351809":"PATIANROWO -- 64391","351810":"BARON -- 64394","351811":"TANJUNGANOM -- 64482","351812":"SUKOMORO -- 64481","351813":"NGANJUK -- 64419","351814":"BAGOR -- 64461","351815":"WILANGAN -- 64462","351816":"REJOSO -- 67181","351817":"GONDANG -- 67174","351818":"NGLUYU -- 64452","351819":"LENGKONG -- 64393","351820":"JATIKALEN -- 64392","351901":"KEBON SARI -- 63173","351902":"DOLOPO -- 63174","351903":"GEGER -- 63171","351904":"DAGANGAN -- 63172","351905":"KARE -- 63182","351906":"GEMARANG -- 63156","351907":"WUNGU -- 63181","351908":"MADIUN -- 63151","351909":"JIWAN -- 63161","351910":"BALEREJO -- 63152","351911":"MEJAYAN -- 63153","351912":"SARADAN -- 63155","351913":"PILANGKENCENG -- 63154","351914":"SAWAHAN -- 63162","351915":"WONOASRI -- 63157","352001":"PONCOL -- 63362","352002":"PARANG -- 63371","352003":"LEMBEYAN -- 63372","352004":"TAKERAN -- 63383","352005":"KAWEDANAN -- 63382","352006":"MAGETAN -- 63319","352007":"PLAOSAN -- 63361","352008":"PANEKAN -- 63352","352009":"SUKOMORO -- 64481","352010":"BENDO -- 61263","352011":"MAOSPATI -- 63392","352012":"BARAT -- 63395","352013":"KARANGREJO -- 66253","352014":"KARAS -- 63395","352015":"KARTOHARJO -- 63395","352016":"NGARIBOYO -- 63351","352017":"NGUNTORONADI -- 63383","352018":"SIDOREJO -- 63319","352101":"SINE -- 63264","352102":"NGRAMBE -- 63263","352103":"JOGOROGO -- 63262","352104":"KENDAL -- 63261","352105":"GENENG -- 63271","352106":"KWADUNGAN -- 63283","352107":"KARANGJATI -- 63284","352108":"PADAS -- 63281","352109":"NGAWI -- 63218","352110":"PARON -- 63253","352111":"KEDUNGGALAR -- 63254","352112":"WIDODAREN -- 63256","352113":"MANTINGAN -- 63261","352114":"PANGKUR -- 63282","352115":"BRINGIN -- 63285","352116":"PITU -- 63252","352117":"KARANGANYAR -- 63257","352118":"GERIH -- 63271","352119":"KASREMAN -- 63281","352201":"NGRAHO -- 62165","352202":"TAMBAKREJO -- 62166","352203":"NGAMBON -- 62167","352204":"NGASEM -- 62154","352205":"BUBULAN -- 62172","352206":"DANDER -- 62171","352207":"SUGIHWARAS -- 62183","352208":"KEDUNGADEM -- 62195","352209":"KEPOH BARU -- 62194","352210":"BAURENO -- 62192","352211":"KANOR -- 62193","352212":"SUMBEREJO -- -","352213":"BALEN -- 62182","352214":"KAPAS -- 62181","352215":"BOJONEGORO -- 62118","352216":"KALITIDU -- 62152","352217":"MALO -- 62153","352218":"PURWOSARI -- 67162","352219":"PADANGAN -- 62162","352220":"KASIMAN -- 62164","352221":"TEMAYANG -- 62184","352222":"MARGOMULYO -- 62168","352223":"TRUCUK -- 62155","352224":"SUKOSEWU -- 62183","352225":"KEDEWAN -- 62164","352226":"GONDANG -- 67174","352227":"SEKAR -- 62167","352228":"GAYAM -- 62154","352301":"KENDURUAN -- 62363","352302":"JATIROGO -- 62362","352303":"BANGILAN -- 62364","352304":"BANCAR -- 62354","352305":"SENORI -- 62365","352306":"TAMBAKBOYO -- 62353","352307":"SINGGAHAN -- 62361","352308":"KEREK -- 62356","352309":"PARENGAN -- 62366","352310":"MONTONG -- 62357","352311":"SOKO -- 62372","352312":"JENU -- 62352","352313":"MERAKURAK -- 62355","352314":"RENGEL -- 62371","352315":"SEMANDING -- 62381","352316":"TUBAN -- 62318","352317":"PLUMPANG -- 62382","352318":"PALANG -- 62391","352319":"WIDANG -- 62383","352320":"GRABAGAN -- 62371","352401":"SUKORAME -- 62276","352402":"BLULUK -- 62274","352403":"MODO -- 62275","352404":"NGIMBANG -- 62273","352405":"BABAT -- 62271","352406":"KEDUNGPRING -- 62272","352407":"BRONDONG -- 62263","352408":"LAREN -- 62262","352409":"SEKARAN -- 62261","352410":"MADURAN -- 62261","352411":"SAMBENG -- 62284","352412":"SUGIO -- 62256","352413":"PUCUK -- 62257","352414":"PACIRAN -- 62264","352415":"SOLOKURO -- 62265","352416":"MANTUP -- 62283","352417":"SUKODADI -- 62253","352418":"KARANGGENENG -- 62254","352419":"KEMBANGBAHU -- 62282","352420":"KALITENGAH -- 62255","352421":"TURI -- 62252","352422":"LAMONGAN -- 62212","352423":"TIKUNG -- 62281","352424":"KARANGBINANGUN -- 62293","352425":"DEKET -- 62291","352426":"GLAGAH -- 68431","352427":"SARIREJO -- 62281","352501":"DUKUN -- 61155","352502":"BALONGPANGGANG -- 61173","352503":"PANCENG -- 61156","352504":"BENJENG -- 61172","352505":"DUDUKSAMPEYAN -- 61162","352506":"WRINGINANOM -- 61176","352507":"UJUNGPANGKAH -- 61154","352508":"KEDAMEAN -- 61175","352509":"SIDAYU -- 61153","352510":"MANYAR -- 61151","352511":"CERME -- 68286","352512":"BUNGAH -- 61152","352513":"MENGANTI -- 61174","352514":"KEBOMAS -- 61124","352515":"DRIYOREJO -- 61177","352516":"GRESIK -- 61114","352517":"SANGKAPURA -- 61181","352518":"TAMBAK -- 62166","352601":"BANGKALAN -- 69112","352602":"SOCAH -- 69161","352603":"BURNEH -- 69121","352604":"KAMAL -- 69162","352605":"AROSBAYA -- 69151","352606":"GEGER -- 63171","352607":"KLAMPIS -- 69153","352608":"SEPULU -- 69154","352609":"TANJUNG BUMI -- 69156","352610":"KOKOP -- 69155","352611":"KWANYAR -- 69163","352612":"LABANG -- 69163","352613":"TANAH MERAH -- 69172","352614":"TRAGAH -- 69165","352615":"BLEGA -- 69174","352616":"MODUNG -- 69166","352617":"KONANG -- 69175","352618":"GALIS -- 69382","352701":"SRESEH -- 69273","352702":"TORJUN -- 69271","352703":"SAMPANG -- 69216","352704":"CAMPLONG -- 69281","352705":"OMBEN -- 69291","352706":"KEDUNGDUNG -- 69252","352707":"JRENGIK -- 69272","352708":"TAMBELANGAN -- 69253","352709":"BANYUATES -- 69263","352710":"ROBATAL -- 69254","352711":"SOKOBANAH -- 69262","352712":"KETAPANG -- 69261","352713":"PANGARENGAN -- 69271","352714":"KARANGPENANG -- 69254","352801":"TLANAKAN -- 69371","352802":"PADEMAWU -- 69323","352803":"GALIS -- 69382","352804":"PAMEKASAN -- 69317","352805":"PROPPO -- 69363","352806":"PALENGAAN -- -","352807":"PEGANTENAN -- 69361","352808":"LARANGAN -- 69383","352809":"PAKONG -- 69352","352810":"WARU -- 69353","352811":"BATUMARMAR -- 69354","352812":"KADUR -- 69355","352813":"PASEAN -- 69356","352901":"KOTA SUMENEP -- 69417","352902":"KALIANGET -- 69471","352903":"MANDING -- 62381","352904":"TALANGO -- 69481","352905":"BLUTO -- 69466","352906":"SARONGGI -- 69467","352907":"LENTENG -- 69461","352908":"GILI GINTING -- 69482","352909":"GULUK-GULUK -- -","352910":"GANDING -- 69462","352911":"PRAGAAN -- 69465","352912":"AMBUNTEN -- 69455","352913":"PASONGSONGAN -- 69457","352914":"DASUK -- 69454","352915":"RUBARU -- 69456","352916":"BATANG BATANG -- 69473","352917":"BATU PUTIH -- 69453","352918":"DUNGKEK -- 69474","352919":"GAPURA -- 69472","352920":"GAYAM -- 62154","352921":"NONGGUNONG -- 69484","352922":"RAAS -- 69485","352923":"MASALEMBU -- 69492","352924":"ARJASA -- 69491","352925":"SAPEKEN -- 69493","352926":"BATUAN -- 69451","352927":"KANGAYAN -- 69491","357101":"MOJOROTO -- 64118","357102":"KOTA -- 64129","357103":"PESANTREN -- 64133","357201":"KEPANJENKIDUL -- 66116","357202":"SUKOREJO -- 63453","357203":"SANANWETAN -- 66133","357301":"BLIMBING -- 65126","357302":"KLOJEN -- 65116","357303":"KEDUNGKANDANG -- 65132","357304":"SUKUN -- 65148","357305":"LOWOKWARU -- 65144","357401":"KADEMANGAN -- 66161","357402":"WONOASIH -- 67233","357403":"MAYANGAN -- 67217","357404":"KANIGARAN -- 67212","357405":"KEDOPAK -- 67229","357501":"GADINGREJO -- 67138","357502":"PURWOREJO -- 67116","357503":"BUGUL KIDUL -- 67128","357504":"PANGGUNGREJO -- 66174","357601":"PRAJURIT KULON -- 61327","357602":"MAGERSARI -- 61314","357701":"KARTOHARJO -- 63395","357702":"MANGUHARJO -- 63122","357703":"TAMAN -- 63137","357801":"KARANGPILANG -- 60221","357802":"WONOCOLO -- 60239","357803":"RUNGKUT -- 60293","357804":"WONOKROMO -- 60241","357805":"TEGALSARI -- 68485","357806":"SAWAHAN -- 63162","357807":"GENTENG -- 69482","357808":"GUBENG -- 60286","357809":"SUKOLILO -- 60117","357810":"TAMBAKSARI -- 60138","357811":"SIMOKERTO -- 60141","357812":"PABEAN CANTIKAN -- 60161","357813":"BUBUTAN -- 60174","357814":"TANDES -- 60186","357815":"KREMBANGAN -- 60179","357816":"SEMAMPIR -- 60151","357817":"KENJERAN -- 60127","357818":"LAKARSANTRI -- 60214","357819":"BENOWO -- 60199","357820":"WIYUNG -- 60227","357821":"DUKUHPAKIS -- 60225","357822":"GAYUNGAN -- 60234","357823":"JAMBANGAN -- 60232","357824":"TENGGILIS MEJOYO -- 60292","357825":"GUNUNG ANYAR -- 60294","357826":"MULYOREJO -- 60113","357827":"SUKOMANUNGGAL -- 60189","357828":"ASEM ROWO -- 60182","357829":"BULAK -- 60124","357830":"PAKAL -- 60197","357831":"SAMBIKEREP -- 60195","357901":"BATU -- 69453","357902":"BUMIAJI -- 65334","357903":"JUNREJO -- 65326","360101":"SUMUR -- 42283","360102":"CIMANGGU -- 42284","360103":"CIBALIUNG -- 42285","360104":"CIKEUSIK -- 42286","360105":"CIGEULIS -- 42282","360106":"PANIMBANG -- 42281","360107":"ANGSANA -- 42277","360108":"MUNJUL -- 42276","360109":"PAGELARAN -- 42265","360110":"BOJONG -- 42274","360111":"PICUNG -- 42275","360112":"LABUAN -- 42264","360113":"MENES -- 42262","360114":"SAKETI -- 42273","360115":"CIPEUCANG -- 42272","360116":"JIPUT -- 42263","360117":"MANDALAWANGI -- 42261","360118":"CIMANUK -- 42271","360119":"KADUHEJO -- 42253","360120":"BANJAR -- 42252","360121":"PANDEGLANG -- 42219","360122":"CADASARI -- 42251","360123":"CISATA -- 42273","360124":"PATIA -- 42265","360125":"KARANG TANJUNG -- 42251","360126":"CIKEDAL -- 42271","360127":"CIBITUNG -- 42285","360128":"CARITA -- 42264","360129":"SUKARESMI -- 42265","360130":"MEKARJAYA -- 42271","360131":"SINDANGRESMI -- 42276","360132":"PULOSARI -- 42273","360133":"KORONCONG -- 42251","360134":"MAJASARI -- 42214","360135":"SOBANG -- 42281","360201":"MALINGPING -- 42391","360202":"PANGGARANGAN -- 42392","360203":"BAYAH -- 42393","360204":"CIPANAS -- 42372","360205":"MUNCANG -- 42364","360206":"LEUWIDAMAR -- 42362","360207":"BOJONGMANIK -- 42363","360208":"GUNUNGKENCANA -- 42354","360209":"BANJARSARI -- 42355","360210":"CILELES -- 42353","360211":"CIMARGA -- 42361","360212":"SAJIRA -- 42371","360213":"MAJA -- 42381","360214":"RANGKASBITUNG -- 42317","360215":"WARUNGGUNUNG -- 42352","360216":"CIJAKU -- 42395","360217":"CIKULUR -- 42356","360218":"CIBADAK -- 42357","360219":"CIBEBER -- 42426","360220":"CILOGRANG -- 42393","360221":"WANASALAM -- 42396","360222":"SOBANG -- 42281","360223":"CURUG BITUNG -- 42381","360224":"KALANGANYAR -- 42312","360225":"LEBAKGEDONG -- 42372","360226":"CIHARA -- 42392","360227":"CIRINTEN -- 42363","360228":"CIGEMLONG -- -","360301":"BALARAJA -- 15610","360302":"JAYANTI -- 15610","360303":"TIGARAKSA -- 15720","360304":"JAMBE -- 15720","360305":"CISOKA -- 15730","360306":"KRESEK -- 15620","360307":"KRONJO -- 15550","360308":"MAUK -- 15530","360309":"KEMIRI -- 15530","360310":"SUKADIRI -- 15530","360311":"RAJEG -- 15540","360312":"PASAR KEMIS -- 15560","360313":"TELUKNAGA -- 15510","360314":"KOSAMBI -- 15212","360315":"PAKUHAJI -- 15570","360316":"SEPATAN -- 15520","360317":"CURUG -- 15810","360318":"CIKUPA -- 15710","360319":"PANONGAN -- 15710","360320":"LEGOK -- 15820","360322":"PAGEDANGAN -- 15336","360323":"CISAUK -- 15344","360327":"SUKAMULYA -- 15610","360328":"KELAPA DUA -- 15810","360329":"SINDANG JAYA -- 15540","360330":"SEPATAN TIMUR -- 15520","360331":"SOLEAR -- 15730","360332":"GUNUNG KALER -- 15620","360333":"MEKAR BARU -- 15550","360405":"KRAMATWATU -- 42161","360406":"WARINGINKURUNG -- 42453","360407":"BOJONEGARA -- 42454","360408":"PULO AMPEL -- 42455","360409":"CIRUAS -- 42182","360411":"KRAGILAN -- 42184","360412":"PONTANG -- 42192","360413":"TIRTAYASA -- 42193","360414":"TANARA -- 42194","360415":"CIKANDE -- 42186","360416":"KIBIN -- 42185","360417":"CARENANG -- 42195","360418":"BINUANG -- 42196","360419":"PETIR -- 42172","360420":"TUNJUNG TEJA -- 42174","360422":"BAROS -- 42173","360423":"CIKEUSAL -- 42175","360424":"PAMARAYAN -- 42176","360425":"KOPO -- 42178","360426":"JAWILAN -- 42177","360427":"CIOMAS -- 42164","360428":"PABUARAN -- 42163","360429":"PADARINCANG -- 42168","360430":"ANYAR -- 42166","360431":"CINANGKA -- 42167","360432":"MANCAK -- 42165","360433":"GUNUNG SARI -- 42163","360434":"BANDUNG -- 42176","367101":"TANGERANG -- 15118","367102":"JATIUWUNG -- 15133","367103":"BATUCEPER -- 15122","367104":"BENDA -- 15123","367105":"CIPONDOH -- 15148","367106":"CILEDUG -- 15153","367107":"KARAWACI -- 15115","367108":"PERIUK -- 15132","367109":"CIBODAS -- 15138","367110":"NEGLASARI -- 15121","367111":"PINANG -- 15142","367112":"KARANG TENGAH -- 15157","367113":"LARANGAN -- 15155","367201":"CIBEBER -- 42426","367202":"CILEGON -- 42419","367203":"PULOMERAK -- 42431","367204":"CIWANDAN -- 42441","367205":"JOMBANG -- 42413","367206":"GEROGOL -- 42438","367207":"PURWAKARTA -- 42433","367208":"CITANGKIL -- 42441","367301":"SERANG -- 42111","367302":"KASEMEN -- 42191","367303":"WALANTAKA -- 42183","367304":"CURUG -- 15810","367305":"CIPOCOK JAYA -- 42122","367306":"TAKTAKAN -- 42162","367401":"SERPONG -- 15310","367402":"SERPONG UTARA -- 15323","367403":"PONDOK AREN -- 15223","367404":"CIPUTAT -- 15412","367405":"CIPUTAT TIMUR -- 15412","367406":"PAMULANG -- 15415","367407":"SETU -- 15315","510101":"NEGARA -- 82212","510102":"MENDOYO -- 82261","510103":"PEKUTATAN -- 82262","510104":"MELAYA -- 82252","510105":"JEMBRANA -- 82218","510201":"SELEMADEG -- 82162","510202":"SALAMADEG TIMUR -- 82162","510203":"SALEMADEG BARAT -- 82162","510204":"KERAMBITAN -- 82161","510205":"TABANAN -- 82112","510206":"KEDIRI -- 82121","510207":"MARGA -- 82181","510208":"PENEBEL -- 82152","510209":"BATURITI -- 82191","510210":"PUPUAN -- 82163","510301":"KUTA -- 82262","510302":"MENGWI -- 80351","510303":"ABIANSEMAL -- 80352","510304":"PETANG -- 80353","510305":"KUTA SELATAN -- 80361","510306":"KUTA UTARA -- 80361","510401":"SUKAWATI -- 80582","510402":"BLAHBATUH -- 80581","510403":"GIANYAR -- 80515","510404":"TAMPAKSIRING -- 80552","510405":"UBUD -- 80571","510406":"TEGALALLANG -- -","510407":"PAYANGAN -- 80572","510501":"NUSA PENIDA -- 80771","510502":"BANJARANGKAN -- 80752","510503":"KLUNGKUNG -- 80716","510504":"DAWAN -- 80761","510601":"SUSUT -- 80661","510602":"BANGLI -- 80614","510603":"TEMBUKU -- 80671","510604":"KINTAMANI -- 80652","510701":"RENDANG -- 80863","510702":"SIDEMEN -- 80864","510703":"MANGGIS -- 80871","510704":"KARANGASEM -- 80811","510705":"ABANG -- 80852","510706":"BEBANDEM -- 80861","510707":"SELAT -- 80862","510708":"KUBU -- 80853","510801":"GEROKGAK -- 81155","510802":"SERIRIT -- 81153","510803":"BUSUNG BIU -- 81154","510804":"BANJAR -- 80752","510805":"SUKASADA -- 81161","510806":"BULELENG -- 81119","510807":"SAWAN -- 81171","510808":"KUBUTAMBAHAN -- 81172","510809":"TEJAKULA -- 81173","517101":"DENPASAR SELATAN -- 80225","517102":"DENPASAR TIMUR -- 80234","517103":"DENPASAR BARAT -- 80112","517104":"DENPASAR UTARA -- 80231","520101":"GERUNG -- 83363","520102":"KEDIRI -- 83362","520103":"NARMADA -- 83371","520107":"SEKOTONG -- 83365","520108":"LABUAPI -- 83361","520109":"GUNUNGSARI -- 83351","520112":"LINGSAR -- 83371","520113":"LEMBAR -- 83364","520114":"BATU LAYAR -- 83355","520115":"KURIPAN -- 83362","520201":"PRAYA -- 83511","520202":"JONGGAT -- 83561","520203":"BATUKLIANG -- 83552","520204":"PUJUT -- 83573","520205":"PRAYA BARAT -- 83572","520206":"PRAYA TIMUR -- 83581","520207":"JANAPRIA -- 83554","520208":"PRINGGARATA -- 83562","520209":"KOPANG -- 83553","520210":"PRAYA TENGAH -- 83582","520211":"PRAYA BARAT DAYA -- 83571","520212":"BATUKLIANG UTARA -- 83552","520301":"KERUAK -- 83672","520302":"SAKRA -- 83671","520303":"TERARA -- 83663","520304":"SIKUR -- 83662","520305":"MASBAGIK -- 83661","520306":"SUKAMULIA -- 83652","520307":"SELONG -- 83618","520308":"PRINGGABAYA -- 83654","520309":"AIKMEL -- 83653","520310":"SAMBELIA -- 83656","520311":"MONTONG GADING -- 83663","520312":"PRINGGASELA -- 83661","520313":"SURALAGA -- 83652","520314":"WANASABA -- 83653","520315":"SEMBALUN -- 83656","520316":"SUWELA -- 83654","520317":"LABUHAN HAJI -- 83614","520318":"SAKRA TIMUR -- 83671","520319":"SAKRA BARAT -- 83671","520320":"JEROWARU -- 83672","520402":"LUNYUK -- 84373","520405":"ALAS -- 84353","520406":"UTAN -- 84352","520407":"BATU LANTEH -- 84361","520408":"SUMBAWA -- 84314","520409":"MOYO HILIR -- 84381","520410":"MOYO HULU -- 84371","520411":"ROPANG -- 84372","520412":"LAPE -- 84382","520413":"PLAMPANG -- 84383","520414":"EMPANG -- 84384","520417":"ALAS BARAT -- 84353","520418":"LABUHAN BADAS -- 84316","520419":"LABANGKA -- 84383","520420":"BUER -- 84353","520421":"RHEE -- 84352","520422":"UNTER IWES -- 84316","520423":"MOYO UTARA -- 84381","520424":"MARONGE -- 84383","520425":"TARANO -- 84384","520426":"LOPOK -- 84382","520427":"LENANGGUAR -- 84372","520428":"ORONG TELU -- 84373","520429":"LANTUNG -- 84372","520501":"DOMPU -- 84211","520502":"KEMPO -- 84261","520503":"HU'U -- -","520504":"KILO -- 84252","520505":"WOJA -- 84251","520506":"PEKAT -- 84261","520507":"MANGGALEWA -- -","520508":"PAJO -- 84272","520601":"MONTA -- 84172","520602":"BOLO -- 84161","520603":"WOHA -- 84171","520604":"BELO -- 84173","520605":"WAWO -- 84181","520606":"SAPE -- 84182","520607":"WERA -- 84152","520608":"DONGGO -- 84162","520609":"SANGGAR -- 84191","520610":"AMBALAWI -- 84153","520611":"LANGGUDU -- 84181","520612":"LAMBU -- 84182","520613":"MADAPANGGA -- 84111","520614":"TAMBORA -- 84191","520615":"SOROMANDI -- 84162","520616":"PARADO -- 84172","520617":"LAMBITU -- 84181","520618":"PALIBELO -- 84173","520701":"JEREWEH -- 84456","520702":"TALIWANG -- 84455","520703":"SETELUK -- 84454","520704":"SEKONGKANG -- 84457","520705":"BRANG REA -- 84458","520706":"POTO TANO -- 84454","520707":"BRANG ENE -- 84455","520708":"MALUK -- 84456","520801":"TANJUNG -- 83352","520802":"GANGGA -- 83353","520803":"KAYANGAN -- 83353","520804":"BAYAN -- 83354","520805":"PEMENANG -- 83352","527101":"AMPENAN -- 83114","527102":"MATARAM -- 83121","527103":"CAKRANEGARA -- 83239","527104":"SEKARBELA -- 83116","527105":"SELAPRANG -- 83125","527106":"SANDUBAYA -- 83237","527201":"RASANAE BARAT -- 84119","527202":"RASANAE TIMUR -- 84119","527203":"ASAKOTA -- 84119","527204":"RABA -- 83671","527205":"MPUNDA -- 84119","530104":"SEMAU -- 85353","530105":"KUPANG BARAT -- 85351","530106":"KUPANG TIMUR -- 85362","530107":"SULAMU -- 85368","530108":"KUPANG TENGAH -- 85361","530109":"AMARASI -- 85367","530110":"FATULEU -- 85363","530111":"TAKARI -- 85369","530112":"AMFOANG SELATAN -- 85364","530113":"AMFOANG UTARA -- 85365","530116":"NEKAMESE -- 85391","530117":"AMARASI BARAT -- 85367","530118":"AMARASI SELATAN -- 85367","530119":"AMARASI TIMUR -- 85367","530120":"AMABI OEFETO TIMUR -- 85363","530121":"AMFOANG BARAT DAYA -- 85364","530122":"AMFOANG BARAT LAUT -- 85364","530123":"SEMAU SELATAN -- 85353","530124":"TAEBENU -- 85361","530125":"AMABI OEFETO -- 85363","530126":"AMFOANG TIMUR -- 85364","530127":"FATULEU BARAT -- 85223","530128":"FATULEU TENGAH -- 85223","530130":"AMFOANG TENGAH -- 85364","530201":"KOTA SOE -- 85519","530202":"MOLLO SELATAN -- 85561","530203":"MOLLO UTARA -- 85552","530204":"AMANUBAN TIMUR -- 85572","530205":"AMANUBAN TENGAH -- 85571","530206":"AMANUBAN SELATAN -- 85562","530207":"AMANUBAN BARAT -- 85551","530208":"AMANATUN SELATAN -- 85573","530209":"AMANATUN UTARA -- 85574","530210":"KI'E -- -","530211":"KUANFATU -- 85563","530212":"FATUMNASI -- 85561","530213":"POLEN -- 85561","530214":"BATU PUTIH -- 85562","530215":"BOKING -- 85573","530216":"TOIANAS -- 85574","530217":"NUNKOLO -- 85573","530218":"OENINO -- 85572","530219":"KOLBANO -- 85563","530220":"KOT OLIN -- 85575","530221":"KUALIN -- 85562","530222":"MOLLO BARAT -- 85561","530223":"KOK BAUN -- 85574","530224":"NOEBANA -- 85573","530225":"SANTIAN -- 85573","530226":"NOEBEBA -- 85562","530227":"KUATNANA -- 85551","530228":"FAUTMOLO -- 85572","530229":"FATUKOPA -- 85572","530230":"MOLLO TENGAH -- 85561","530231":"TOBU -- 85552","530232":"NUNBENA -- 85561","530301":"MIOMAFO TIMUR -- -","530302":"MIOMAFO BARAT -- -","530303":"BIBOKI SELATAN -- 85681","530304":"NOEMUTI -- 85661","530305":"KOTA KEFAMENANU -- 85614","530306":"BIBOKI UTARA -- 85682","530307":"BIBOKI ANLEU -- 85613","530308":"INSANA -- 85671","530309":"INSANA UTARA -- 85671","530310":"NOEMUTI TIMUR -- 85661","530311":"MIOMAFFO TENGAH -- 85661","530312":"MUSI -- 85661","530313":"MUTIS -- 85661","530314":"BIKOMI SELATAN -- 85651","530315":"BIKOMI TENGAH -- 85651","530316":"BIKOMI NILULAT -- 85651","530317":"BIKOMI UTARA -- 85651","530318":"NAIBENU -- 85651","530319":"INSANA FAFINESU -- 85671","530320":"INSANA BARAT -- 85671","530321":"INSANA TENGAH -- 85671","530322":"BIBOKI TAN PAH -- 85681","530323":"BIBOKI MOENLEU -- 85681","530324":"BIBOKI FEOTLEU -- 85682","530401":"LAMAKNEN -- 85772","530402":"TASIFETOTIMUR -- 85771","530403":"RAIHAT -- 85773","530404":"TASIFETO BARAT -- 85752","530405":"KAKULUK MESAK -- 85752","530412":"KOTA ATAMBUA -- -","530413":"RAIMANUK -- 85761","530417":"LASIOLAT -- 85771","530418":"LAMAKNEN SELATAN -- 85772","530421":"ATAMBUA BARAT -- 85715","530422":"ATAMBUA SELATAN -- 85717","530423":"NANAET DUABESI -- 85752","530501":"TELUK MUTIARA -- 85819","530502":"ALOR BARAT LAUT -- 85851","530503":"ALOR BARAT DAYA -- 85861","530504":"ALOR SELATAN -- 85871","530505":"ALOR TIMUR -- 85872","530506":"PANTAR -- 85881","530507":"ALOR TENGAH UTARA -- 85871","530508":"ALOR TIMUR LAUT -- 85872","530509":"PANTAR BARAT -- 85881","530510":"KABOLA -- 85851","530511":"PULAU PURA -- 85851","530512":"MATARU -- 85861","530513":"PUREMAN -- 85872","530514":"PANTAR TIMUR -- 85881","530515":"LEMBUR -- 85871","530516":"PANTAR TENGAH -- 85881","530517":"PANTAR BARU LAUT -- -","530601":"WULANGGITANG -- 86253","530602":"TITEHENA -- 86253","530603":"LARANTUKA -- 86219","530604":"ILE MANDIRI -- 86211","530605":"TANJUNG BUNGA -- 86252","530606":"SOLOR BARAT -- 86272","530607":"SOLOR TIMUR -- 86271","530608":"ADONARA BARAT -- 86262","530609":"WOTAN ULUMANDO -- -","530610":"ADONARA TIMUR -- 86261","530611":"KELUBAGOLIT -- 86262","530612":"WITIHAMA -- 86262","530613":"ILE BOLENG -- 86253","530614":"DEMON PAGONG -- 86219","530615":"LEWOLEMA -- 86252","530616":"ILE BURA -- 86253","530617":"ADONARA -- 86262","530618":"ADONARA TENGAH -- 86262","530619":"SOLOR SELATAN -- 86271","530701":"PAGA -- 86153","530702":"MEGO -- 86113","530703":"LELA -- 86516","530704":"NITA -- 86152","530705":"ALOK -- 86111","530706":"PALUE -- 86111","530707":"NELLE -- 86116","530708":"TALIBURA -- 86183","530709":"WAIGETE -- 86183","530710":"KEWAPANTE -- 86181","530711":"BOLA -- 85851","530712":"MAGEPANDA -- 86152","530713":"WAIBLAMA -- 86183","530714":"ALOK BARAT -- 86115","530715":"ALOK TIMUR -- 86111","530716":"KOTING -- 86116","530717":"TANA WAWO -- 86153","530718":"HEWOKLOANG -- 86181","530719":"KANGAE -- 86181","530720":"DORENG -- 86171","530721":"MAPITARA -- 86171","530801":"NANGAPANDA -- 86352","530802":"PULAU ENDE -- 86362","530803":"ENDE -- 86362","530804":"ENDE SELATAN -- 86313","530805":"NDONA -- 86361","530806":"DETUSOKO -- 86371","530807":"WEWARIA -- 86353","530808":"WOLOWARU -- 86372","530809":"WOLOJITA -- 86382","530810":"MAUROLE -- 86381","530811":"MAUKARO -- 86352","530812":"LIO TIMUR -- 86361","530813":"KOTA BARU -- 86111","530814":"KELIMUTU -- 86318","530815":"DETUKELI -- 86371","530816":"NDONA TIMUR -- 86361","530817":"NDORI -- 86372","530818":"ENDE UTARA -- 86319","530819":"ENDE TENGAH -- 86319","530820":"ENDE TIMUR -- 86361","530821":"LEPEMBUSU KELISOKE -- 86374","530901":"AIMERE -- 86452","530902":"GOLEWA -- 86461","530906":"BAJAWA -- 86413","530907":"SOA -- 86419","530909":"RIUNG -- 86419","530912":"JEREBUU -- 86452","530914":"RIUNG BARAT -- 86419","530915":"BAJAWA UTARA -- 86413","530916":"WOLOMEZE -- 86419","530918":"GOLEWA SELATAN -- 86461","530919":"GOLEWA BARAT -- 86461","530920":"INERIE -- 86452","531001":"WAE RII -- 86591","531003":"RUTENG -- 86516","531005":"SATAR MESE -- 86561","531006":"CIBAL -- 86591","531011":"REOK -- 86592","531012":"LANGKE REMBONG -- 86519","531013":"SATAR MESE BARAT -- 86561","531014":"RAHONG UTARA -- 86516","531015":"LELAK -- 86516","531016":"REOK BARAT -- 86592","531017":"CIBAL BARAT -- 86591","531101":"KOTA WAINGAPU -- 87112","531102":"HAHARU -- 87153","531103":"LEWA -- 86461","531104":"NGGAHA ORI ANGU -- 87152","531105":"TABUNDUNG -- 87161","531106":"PINU PAHAR -- 87161","531107":"PANDAWAI -- 87171","531108":"UMALULU -- 87181","531109":"RINDI -- 87181","531110":"PAHUNGA LODU -- 87182","531111":"WULLA WAIJELU -- -","531112":"PABERIWAI -- 87171","531113":"KARERA -- 87172","531114":"KAHAUNGU ETI -- 87171","531115":"MATAWAI LA PAWU -- -","531116":"KAMBERA -- 87114","531117":"KAMBATA MAPAMBUHANG -- 87171","531118":"LEWA TIDAHU -- 87152","531119":"KATALA HAMU LINGU -- 87152","531120":"KANATANG -- 87153","531121":"NGADU NGALA -- 87172","531122":"MAHU -- 87171","531204":"TANA RIGHU -- 87257","531210":"LOLI -- 87284","531211":"WANOKAKA -- 87272","531212":"LAMBOYA -- 87271","531215":"KOTA WAIKABUBAK -- 87217","531218":"LABOYA BARAT -- -","531301":"NAGA WUTUNG -- 86684","531302":"ATADEI -- 86685","531303":"ILE APE -- 86683","531304":"LEBATUKAN -- 86681","531305":"NUBATUKAN -- 86682","531306":"OMESURI -- 86691","531307":"BUYASURI -- 86692","531308":"WULANDONI -- 86685","531309":"ILE APE TIMUR -- 86683","531401":"ROTE BARAT DAYA -- 85982","531402":"ROTE BARAT LAUT -- 85981","531403":"LOBALAIN -- 85912","531404":"ROTE TENGAH -- 85972","531405":"PANTAI BARU -- 85973","531406":"ROTE TIMUR -- 85974","531407":"ROTE BARAT -- 85982","531408":"ROTE SELATAN -- 85972","531409":"NDAO NUSE -- 85983","531410":"LANDU LEKO -- 85974","531501":"MACANG PACAR -- 86756","531502":"KUWUS -- 86752","531503":"LEMBOR -- 86753","531504":"SANO NGGOANG -- 86757","531505":"KOMODO -- 86754","531506":"BOLENG -- 86754","531507":"WELAK -- 86753","531508":"NDOSO -- 86752","531509":"LEMBOR SELATAN -- 86753","531510":"MBELILING -- 86757","531601":"AESESA -- 86472","531602":"NANGARORO -- 86464","531603":"BOAWAE -- 86462","531604":"MAUPONGGO -- 86463","531605":"WOLOWAE -- 86472","531606":"KEO TENGAH -- 86464","531607":"AESESA SELATAN -- 86472","531701":"KATIKU TANA -- 87282","531702":"UMBU RATU NGGAY BARAT -- 87282","531703":"MAMBORO -- 87258","531704":"UMBU RATU NGGAY -- 87282","531705":"KATIKU TANA SELATAN -- 87282","531801":"LOURA -- 87254","531802":"WEWEWA UTARA -- 87252","531803":"WEWEWA TIMUR -- 87252","531804":"WEWEWA BARAT -- 87253","531805":"WEWEWA SELATAN -- 87263","531806":"KODI BANGEDO -- 87262","531807":"KODI -- 87262","531808":"KODI UTARA -- 87261","531809":"KOTA TAMBOLAKA -- 87255","531810":"WEWEWA TENGAH -- 87252","531811":"KODI BALAGHAR -- 87262","531901":"BORONG -- 86571","531902":"POCO RANAKA -- 86583","531903":"LAMBA LEDA -- 86582","531904":"SAMBI RAMPAS -- 86584","531905":"ELAR -- 86581","531906":"KOTA KOMBA -- 86572","531907":"RANA MESE -- 86571","531908":"POCO RANAKA TIMUR -- 86583","531909":"ELAR SELATAN -- 86581","532001":"SABU BARAT -- 85391","532002":"SABU TENGAH -- 85392","532003":"SABU TIMUR -- 85392","532004":"SABU LIAE -- 85391","532005":"HAWU MEHARA -- 85391","532006":"RAIJUA -- 85393","532101":"MALAKA TENGAH -- 85762","532102":"MALAKA BARAT -- 85763","532103":"WEWIKU -- 85763","532104":"WELIMAN -- 85763","532105":"RINHAT -- 85764","532106":"IO KUFEU -- 85765","532107":"SASITAMEAN -- 85765","532108":"LAENMANEN -- 85718","532109":"MALAKA TIMUR -- 85761","532110":"KOBALIMA TIMUR -- 85766","532111":"KOBALIMA -- 85766","532112":"BOTIN LEOBELE -- 85765","537101":"ALAK -- 85231","537102":"MAULAFA -- 85148","537103":"KELAPA LIMA -- 85228","537104":"OEBOBO -- 85116","537105":"KOTA RAJA -- 85119","537106":"KOTA LAMA -- 85229","610101":"SAMBAS -- 79462","610102":"TELUK KERAMAT -- 79465","610103":"JAWAI -- 79454","610104":"TEBAS -- 79461","610105":"PEMANGKAT -- 79453","610106":"SEJANGKUNG -- 79463","610107":"SELAKAU -- 79452","610108":"PALOH -- 79466","610109":"SAJINGAN BESAR -- 79467","610110":"SUBAH -- 79417","610111":"GALING -- 79453","610112":"TEKARANG -- 79465","610113":"SEMPARUK -- 79453","610114":"SAJAD -- 79462","610115":"SEBAWI -- 79462","610116":"JAWAI SELATAN -- 79154","610117":"TANGARAN -- 79465","610118":"SALATIGA -- 79453","610119":"SELAKAU TIMUR -- 79452","610201":"MEMPAWAH HILIR -- 78914","610206":"TOHO -- 78361","610207":"SUNGAI PINYUH -- 78353","610208":"SIANTAN -- 78351","610212":"SUNGAI KUNYIT -- 78371","610215":"SEGEDONG -- 78351","610216":"ANJONGAN -- 78353","610217":"SADANIANG -- 78361","610218":"MEMPAWAH TIMUR -- 78917","610301":"KAPUAS -- 78516","610302":"MUKOK -- 78581","610303":"NOYAN -- 78554","610304":"JANGKANG -- 78591","610305":"BONTI -- 78552","610306":"BEDUAI -- 78555","610307":"SEKAYAM -- 78556","610308":"KEMBAYAN -- 78553","610309":"PARINDU -- 78561","610310":"TAYAN HULU -- 78562","610311":"TAYAN HILIR -- 78564","610312":"BALAI -- 78563","610313":"TOBA -- 78572","610320":"MELIAU -- 78571","610321":"ENTIKONG -- 78557","610401":"MATAN HILIR UTARA -- 78813","610402":"MARAU -- 78863","610403":"MANIS MATA -- 78864","610404":"KENDAWANGAN -- 78862","610405":"SANDAI -- 78871","610407":"SUNGAI LAUR -- 78872","610408":"SIMPANG HULU -- 78854","610411":"NANGA TAYAP -- 78873","610412":"MATAN HILIR SELATAN -- 78822","610413":"TUMBANG TITI -- 78874","610414":"JELAI HULU -- 78876","610416":"DELTA PAWAN -- 78813","610417":"MUARA PAWAN -- 78813","610418":"BENUA KAYONG -- 78822","610419":"HULU SUNGAI -- 78871","610420":"SIMPANG DUA -- 78854","610421":"AIR UPAS -- 78863","610422":"SINGKUP -- 78863","610424":"PEMAHAN -- 78874","610425":"SUNGAI MELAYU RAYAK -- 78874","610501":"SINTANG -- 78617","610502":"TEMPUNAK -- 78661","610503":"SEPAUK -- 78662","610504":"KETUNGAU HILIR -- 78652","610505":"KETUNGAU TENGAH -- 78653","610506":"KETUNGAU HULU -- 78654","610507":"DEDAI -- 78691","610508":"KAYAN HILIR -- 78693","610509":"KAYAN HULU -- 78694","610514":"SERAWAI -- 78683","610515":"AMBALAU -- 78684","610519":"KELAM PERMAI -- 78656","610520":"SUNGAI TEBELIAN -- 78655","610521":"BINJAI HULU -- 78663","610601":"PUTUSSIBAU UTARA -- 78716","610602":"BIKA -- 78753","610603":"EMBALOH HILIR -- 78754","610604":"EMBALOH HULU -- 78755","610605":"BUNUT HILIR -- 78761","610606":"BUNUT HULU -- 78762","610607":"JONGKONG -- 78763","610608":"HULU GURUNG -- 78764","610609":"SELIMBAU -- 78765","610610":"SEMITAU -- 78771","610611":"SEBERUANG -- 78772","610612":"BATANG LUPAR -- 78766","610613":"EMPANANG -- 78768","610614":"BADAU -- 78767","610615":"SILAT HILIR -- 78773","610616":"SILAT HULU -- 78774","610617":"PUTUSSIBAU SELATAN -- 78714","610618":"KALIS -- 78756","610619":"BOYAN TANJUNG -- 78758","610620":"MENTEBAH -- 78757","610621":"PENGKADAN -- 78759","610622":"SUHAID -- 78775","610623":"PURING KENCANA -- 78769","610701":"SUNGAI RAYA -- 78391","610702":"SAMALANTAN -- 79281","610703":"LEDO -- 79284","610704":"BENGKAYANG -- 79211","610705":"SELUAS -- 79285","610706":"SANGGAU LEDO -- 79284","610707":"JAGOI BABANG -- 79286","610708":"MONTERADO -- 79181","610709":"TERIAK -- 79214","610710":"SUTI SEMARANG -- 79283","610711":"CAPKALA -- 79271","610712":"SIDING -- 79286","610713":"LUMAR -- 79283","610714":"SUNGAI BETUNG -- 79211","610715":"SUNGAI RAYA KEPULAUAN -- 79271","610716":"LEMBAH BAWANG -- 79281","610717":"TUJUH BELAS -- 79251","610801":"NGABANG -- 79357","610802":"MEMPAWAH HULU -- 79363","610803":"MENJALIN -- 79362","610804":"MANDOR -- 79355","610805":"AIR BESAR -- 79365","610806":"MENYUKE -- 79364","610807":"SENGAH TEMILA -- 79356","610808":"MERANTI -- 79366","610809":"KUALA BEHE -- 79367","610810":"SEBANGKI -- 79358","610811":"JELIMPO -- 79357","610812":"BANYUKE HULU -- 79364","610813":"SOMPAK -- 79363","610901":"SEKADAU HILIR -- 79582","610902":"SEKADAU HULU -- 79583","610903":"NANGA TAMAN -- 79584","610904":"NANGA MAHAP -- 79585","610905":"BELITANG HILIR -- 79586","610906":"BELITANG HULU -- 79587","610907":"BELITANG -- 79587","611001":"BELIMBING -- 79671","611002":"NANGA PINOH -- 79672","611003":"ELLA HILIR -- 79681","611004":"MENUKUNG -- 79682","611005":"SAYAN -- 79673","611006":"TANAH PINOH -- 79674","611007":"SOKAN -- 79675","611008":"PINOH UTARA -- 79672","611009":"PINOH SELATAN -- 79672","611010":"BELIMBING HULU -- 79671","611011":"TANAH PINOH BARAT -- 79674","611101":"SUKADANA -- 78852","611102":"SIMPANG HILIR -- 78853","611103":"TELUK BATANG -- 78856","611105":"SEPONTI -- 78857","611106":"KEPULAUAN KARIMATA -- 78855","611201":"SUNGAI RAYA -- 78391","611202":"KUALA MANDOR B -- -","611203":"SUNGAI AMBAWANG -- 78393","611204":"TERENTANG -- 78392","611205":"BATU AMPAR -- 78385","611206":"KUBU -- 78384","611207":"RASAU JAYA -- 78382","611208":"TELUK PAKEDAI -- -","611209":"SUNGAI KAKAP -- 78381","617101":"PONTIANAK SELATAN -- 78121","617102":"PONTIANAK TIMUR -- 78233","617103":"PONTIANAK BARAT -- 78114","617104":"PONTIANAK UTARA -- 78244","617105":"PONTIANAK KOTA -- 78117","617106":"PONTIANAK TENGGARA -- 78124","617201":"SINGKAWANG TENGAH -- 79114","617202":"SINGKAWANG BARAT -- 79124","617203":"SINGKAWANG TIMUR -- 79251","617204":"SINGKAWANG UTARA -- 79151","617205":"SINGKAWANG SELATAN -- 79163","620101":"KUMAI -- 74181","620102":"ARUT SELATAN -- 74113","620103":"KOTAWARINGIN LAMA -- 74161","620104":"ARUT UTARA -- 74152","620105":"PANGKALAN LADA -- 74184","620106":"PANGKALAN BANTENG -- 74183","620201":"KOTA BESI -- 74353","620202":"CEMPAGA -- 74354","620203":"MENTAYA HULU -- 74356","620204":"PARENGGEAN -- 74355","620205":"BAAMANG -- 74312","620206":"MENTAWA BARU KETAPANG -- -","620207":"MENTAYA HILIR UTARA -- 74361","620208":"MENTAYA HILIR SELATAN -- 74363","620209":"PULAU HANAUT -- 74362","620210":"ANTANG KALANG -- 74352","620211":"TELUK SAMPIT -- 74363","620212":"SERANAU -- 74315","620213":"CEMPAGA HULU -- 74354","620214":"TELAWANG -- 74353","620215":"BUKIT SANTUAI -- -","620216":"TUALAN HULU -- 74355","620217":"TELAGA ANTANG -- 74352","620301":"SELAT -- 74113","620302":"KAPUAS HILIR -- 73525","620303":"KAPUAS TIMUR -- 73581","620304":"KAPUAS KUALA -- 73583","620305":"KAPUAS BARAT -- 73552","620306":"PULAU PETAK -- 73592","620307":"KAPUAS MURUNG -- 73593","620308":"BASARANG -- 73564","620309":"MANTANGAI -- 73553","620310":"TIMPAH -- 73554","620311":"KAPUAS TENGAH -- 73555","620312":"KAPUAS HULU -- 74581","620313":"TAMBAN CATUR -- 73583","620314":"PASAK TALAWANG -- 73555","620315":"MANDAU TALAWANG -- 73555","620316":"DADAHUP -- 73593","620317":"BATAGUH -- 73516","620401":"JENAMAS -- 73763","620402":"DUSUN HILIR -- 73762","620403":"KARAU KUALA -- 73761","620404":"DUSUN UTARA -- 73752","620405":"GN. BINTANG AWAI -- -","620406":"DUSUN SELATAN -- 73713","620501":"MONTALLAT -- 73861","620502":"GUNUNG TIMANG -- 73862","620503":"GUNUNG PUREI -- 73871","620504":"TEWEH TIMUR -- 73881","620505":"TEWEH TENGAH -- 73814","620506":"LAHEI -- 73852","620508":"TEWEH SELATAN -- 73814","620509":"LAHEI BARAT -- 73852","620601":"KAMPIANG -- -","620602":"KATINGAN HILIR -- 74413","620603":"TEWANG SANGALANG GARING -- -","620604":"PULAU MALAN -- 74453","620605":"KATINGAN TENGAH -- 74454","620606":"SANAMAN MANTIKEI -- 74455","620607":"MARIKIT -- 74456","620608":"KATINGAN HULU -- 74457","620609":"MENDAWAI -- 74463","620610":"KATINGAN KUALA -- 74463","620611":"TASIK PAYAWAN -- 74461","620612":"PETAK MALAI -- 74455","620613":"BUKIT RAYA -- 74457","620701":"SERUYAN HILIR -- 74215","620702":"SERUYAN TENGAH -- 74281","620703":"DANAU SEMBULUH -- 74261","620704":"HANAU -- 74362","620705":"SERUYAN HULU -- 74291","620706":"SERUYAN HILIR TIMUR -- 74215","620707":"SERUYAN RAYA -- 74261","620708":"DANAU SELULUK -- 74271","620709":"BATU AMPAR -- 74281","620710":"SULING TAMBUN -- 74291","620801":"SUKAMARA -- 74172","620802":"JELAI -- 74171","620803":"BALAI RIAM -- 74173","620804":"PANTAI LUNCI -- 74171","620805":"PERMATA KECUBUNG -- 74173","620901":"LAMANDAU -- 74663","620902":"DELANG -- 74664","620903":"BULIK -- 74162","620904":"BULIK TIMUR -- 74162","620905":"MENTHOBI RAYA -- 74162","620906":"SEMATU JAYA -- 74162","620907":"BELANTIKAN RAYA -- 74663","620908":"BATANG KAWA -- 74664","621001":"SEPANG SIMIN -- 74571","621002":"KURUN -- 74511","621003":"TEWAH -- 74552","621004":"KAHAYAN HULU UTARA -- 74553","621005":"RUNGAN -- 74561","621006":"MANUHING -- 74562","621007":"MIHING RAYA -- 74571","621008":"DAMANG BATU -- 74553","621009":"MIRI MANASA -- 74553","621010":"RUNGAN HULU -- 74561","621011":"MAHUNING RAYA -- -","621101":"PANDIH BATU -- 74871","621102":"KAHAYAN KUALA -- 74872","621103":"KAHAYAN TENGAH -- 74862","621104":"BANAMA TINGANG -- 74863","621105":"KAHAYAN HILIR -- 74813","621106":"MALIKU -- 74873","621107":"JABIREN -- 74816","621108":"SEBANGAU KUALA -- 74874","621201":"MURUNG -- 73911","621202":"TANAH SIANG -- 73961","621203":"LAUNG TUHUP -- 73991","621204":"PERMATA INTAN -- 73971","621205":"SUMBER BARITO -- 73981","621206":"BARITO TUHUP RAYA -- 73991","621207":"TANAH SIANG SELATAN -- 73961","621208":"SUNGAI BABUAT -- 73971","621209":"SERIBU RIAM -- 73981","621210":"UUT MURUNG -- 73981","621301":"DUSUN TIMUR -- 73618","621302":"BANUA LIMA -- -","621303":"PATANGKEP TUTUI -- 73671","621304":"AWANG -- 73681","621305":"DUSUN TENGAH -- 73652","621306":"PEMATANG KARAU -- 73653","621307":"PAJU EPAT -- 73617","621308":"RAREN BATUAH -- 73652","621309":"PAKU -- 73652","621310":"KARUSEN JANANG -- 73652","627101":"PAHANDUT -- 73111","627102":"BUKIT BATU -- 73224","627103":"JEKAN RAYA -- 73112","627104":"SABANGAU -- -","627105":"RAKUMPIT -- 73229","630101":"TAKISUNG -- 70861","630102":"JORONG -- 70881","630103":"PELAIHARI -- 70815","630104":"KURAU -- 70853","630105":"BATI BATI -- -","630106":"PANYIPATAN -- 70871","630107":"KINTAP -- 70883","630108":"TAMBANG ULANG -- 70854","630109":"BATU AMPAR -- 70882","630110":"BAJUIN -- 70815","630111":"BUMI MAKMUR -- 70853","630201":"PULAUSEMBILAN -- 72181","630202":"PULAULAUT BARAT -- 72153","630203":"PULAULAUT SELATAN -- 72154","630204":"PULAULAUT TIMUR -- 72152","630205":"PULAUSEBUKU -- 72155","630206":"PULAULAUT UTARA -- 72115","630207":"KELUMPANG SELATAN -- 72161","630208":"KELUMPANG HULU -- 72162","630209":"KELUMPANG TENGAH -- 72164","630210":"KELUMPANG UTARA -- 72165","630211":"PAMUKAN SELATAN -- 72168","630212":"SAMPANAHAN -- 72166","630213":"PAMUKAN UTARA -- 72169","630214":"HAMPANG -- 72163","630215":"SUNGAIDURIAN -- 72167","630216":"PULAULAUT TENGAH -- 72156","630217":"KELUMPANG HILIR -- 72161","630218":"KELUMPANG BARAT -- 72164","630219":"PAMUKAN BARAT -- 72169","630220":"PULAULAUT KEPULAUAN -- 72154","630221":"PULAULAUT TANJUNG SELAYAR -- 72153","630301":"ALUH ALUH -- -","630302":"KERTAK HANYAR -- 70654","630303":"GAMBUT -- 70652","630304":"SUNGAI TABUK -- 70653","630305":"MARTAPURA -- 70617","630306":"KARANG INTAN -- 70661","630307":"ASTAMBUL -- 70671","630308":"SIMPANG EMPAT -- 70673","630309":"PENGAROM -- -","630310":"SUNGAI PINANG -- 70675","630311":"ARANIO -- 70671","630312":"MATARAMAN -- 70672","630313":"BERUNTUNG BARU -- 70655","630314":"MARTAPURA BARAT -- 70618","630315":"MARTAPURA TIMUR -- 70617","630316":"SAMBUNG MAKMUR -- 70674","630317":"PARAMASAN -- -","630318":"TELAGA BAUNTUNG -- 70673","630319":"TATAH MAKMUR -- 70654","630401":"TABUNGANEN -- 70567","630402":"TAMBAN -- 70854","630403":"ANJIR PASAR -- 70565","630404":"ANJIR MUARA -- 70564","630405":"ALALAK -- 70582","630406":"MANDASTANA -- 70581","630407":"RANTAU BADAUH -- 70561","630408":"BELAWANG -- 70563","630409":"CERBON -- 70571","630410":"BAKUMPAI -- 70513","630411":"KURIPAN -- 70552","630412":"TABUKAN -- 70553","630413":"MEKARSARI -- 70568","630414":"BARAMBAI -- 70562","630415":"MARABAHAN -- 70511","630416":"WANARAYA -- 70562","630417":"JEJANGKIT -- 70581","630501":"BINUANG -- 71183","630502":"TAPIN SELATAN -- 71181","630503":"TAPIN TENGAH -- 71161","630504":"TAPIN UTARA -- 71114","630505":"CANDI LARAS SELATAN -- 71162","630506":"CANDI LARAS UTARA -- 71171","630507":"BAKARANGAN -- 71152","630508":"PIANI -- 71191","630509":"BUNGUR -- 71153","630510":"LOKPAIKAT -- 71154","630511":"SALAM BABARIS -- 71185","630512":"HATUNGUN -- 71184","630601":"SUNGAI RAYA -- 71271","630602":"PADANG BATUNG -- 71281","630603":"TELAGA LANGSAT -- 71292","630604":"ANGKINANG -- 71291","630605":"KANDANGAN -- 71213","630606":"SIMPUR -- 71261","630607":"DAHA SELATAN -- 71252","630608":"DAHA UTARA -- 71253","630609":"KALUMPANG -- 71262","630610":"LOKSADO -- 71282","630611":"DAHA BARAT -- 71252","630701":"HARUYAN -- 71363","630702":"BATU BENAWA -- 71371","630703":"LABUAN AMAS SELATAN -- 71361","630704":"LABUAN AMAS UTARA -- 71362","630705":"PANDAWAN -- 71352","630706":"BARABAI -- 71315","630707":"BATANG ALAI SELATAN -- 71381","630708":"BATANG ALAI UTARA -- 71391","630709":"HANTAKAN -- 71372","630710":"BATANG ALAI TIMUR -- 71382","630711":"LIMPASU -- 71391","630801":"DANAU PANGGANG -- 71453","630802":"BABIRIK -- 71454","630803":"SUNGAI PANDAN -- 71455","630804":"AMUNTAI SELATAN -- 71452","630805":"AMUNTAI TENGAH -- 71419","630806":"AMUNTAI UTARA -- 71471","630807":"BANJANG -- 71416","630808":"HAUR GADING -- 71471","630809":"PAMINGGIR -- 71453","630810":"SUNGAI TABUKAN -- 71455","630901":"BANUA LAWAS -- 71553","630902":"KELUA -- 71552","630903":"TANTA -- 71561","630904":"TANJUNG -- 71514","630905":"HARUAI -- 71572","630906":"MURUNG PUDAK -- 71571","630907":"MUARA UYA -- 71573","630908":"MUARA HARUS -- 71555","630909":"PUGAAN -- 71554","630910":"UPAU -- 71575","630911":"JARO -- 71574","630912":"BINTANG ARA -- 71572","631001":"BATU LICIN -- 72271","631002":"KUSAN HILIR -- 72273","631003":"SUNGAI LOBAN -- 72274","631004":"SATUI -- 72275","631005":"KUSAN HULU -- 72272","631006":"SIMPANG EMPAT -- 70673","631007":"KARANG BINTANG -- 72211","631008":"MANTEWE -- 72211","631009":"ANGSANA -- 72275","631010":"KURANJI -- 72272","631101":"JUAI -- 71665","631102":"HALONG -- 71666","631103":"AWAYAN -- 71664","631104":"BATU MANDI -- 71663","631105":"LAMPIHONG -- 71661","631106":"PARINGIN -- 71662","631107":"PARINGIN SELATAN -- 71662","631108":"TEBING TINGGI -- 71664","637101":"BANJARMASIN SELATAN -- 70245","637102":"BANJARMASIN TIMUR -- 70239","637103":"BANJARMASIN BARAT -- 70245","637104":"BANJARMASIN UTARA -- 70126","637105":"BANJARMASIN TENGAH -- 70114","637202":"LANDASAN ULIN -- 70724","637203":"CEMPAKA -- 70732","637204":"BANJARBARU UTARA -- 70714","637205":"BANJARBARU SELATAN -- 70713","637206":"LIANG ANGGANG -- 70722","640101":"BATU SOPANG -- 76252","640102":"TANJUNG HARAPAN -- 76261","640103":"PASIR BALENGKONG -- -","640104":"TANAH GROGOT -- 76251","640105":"KUARO -- 76281","640106":"LONG IKIS -- 76282","640107":"MUARA KOMAM -- 76253","640108":"LONG KALI -- 76283","640109":"BATU ENGAU -- 76261","640110":"MUARA SAMU -- 76252","640201":"MUARA MUNTAI -- 75562","640202":"LOA KULU -- 75571","640203":"LOA JANAN -- 75391","640204":"ANGGANA -- 75381","640205":"MUARA BADAK -- 75382","640206":"TENGGARONG -- 75572","640207":"SEBULU -- 75552","640208":"KOTA BANGUN -- 75561","640209":"KENOHAN -- 75564","640210":"KEMBANG JANGGUT -- 75557","640211":"MUARA KAMAN -- 75553","640212":"TABANG -- 75561","640213":"SAMBOJA -- 75274","640214":"MUARA JAWA -- 75265","640215":"SANGA SANGA -- -","640216":"TENGGARONG SEBERANG -- 75572","640217":"MARANG KAYU -- 75385","640218":"MUARA WIS -- 75559","640301":"KELAY -- 77362","640302":"TALISAYAN -- 77372","640303":"SAMBALIUNG -- 77371","640304":"SEGAH -- 77361","640305":"TANJUNG REDEB -- 77312","640306":"GUNUNG TABUR -- 77352","640307":"PULAU DERAWAN -- 77381","640308":"BIDUK-BIDUK -- 77373","640309":"TELUK BAYUR -- 77352","640310":"TABALAR -- 77372","640311":"MARATUA -- 77381","640312":"BATU PUTIH -- 77373","640313":"BIATAN -- 77372","640705":"LONG IRAM -- 75766","640706":"MELAK -- 75765","640707":"BARONG TONGKOK -- 75776","640708":"DAMAI -- 75777","640709":"MUARA LAWA -- 75775","640710":"MUARA PAHU -- 75774","640711":"JEMPANG -- 75773","640712":"BONGAN -- 75772","640713":"PENYINGGAHAN -- 75763","640714":"BENTIAN BESAR -- 75778","640715":"LINGGANG BIGUNG -- 75576","640716":"NYUATAN -- 75776","640717":"SILUQ NGURAI -- 75774","640718":"MOOK MANAAR BULATN -- 75774","640719":"TERING -- 75766","640720":"SEKOLAQ DARAT -- 75765","640801":"MUARA ANCALONG -- 75656","640802":"MUARA WAHAU -- 75655","640803":"MUARA BENGKAL -- 75654","640804":"SANGATTA UTARA -- 75683","640805":"SANGKULIRANG -- 75684","640806":"BUSANG -- 75556","640807":"TELEN -- 75555","640808":"KOMBENG -- -","640809":"BENGALON -- 75618","640810":"KALIORANG -- 75618","640811":"SANDARAN -- 75685","640812":"SANGATTA SELATAN -- 75683","640813":"TELUK PANDAN -- 75683","640814":"RANTAU PULUNG -- 75683","640815":"KAUBUN -- 75619","640816":"KARANGAN -- 75684","640817":"BATU AMPAR -- 75654","640818":"LONG MESANGAT -- 75656","640901":"PENAJAM -- 76141","640902":"WARU -- 76284","640903":"BABULU -- 76285","640904":"SEPAKU -- 76147","641101":"LONG BAGUN -- 75767","641102":"LONG HUBUNG -- 75779","641103":"LAHAM -- 75779","641104":"LONG APARI -- 75769","641105":"LONG PAHANGAI -- 75768","647101":"BALIKPAPAN TIMUR -- 76117","647102":"BALIKPAPAN BARAT -- 76131","647103":"BALIKPAPAN UTARA -- 76136","647104":"BALIKPAPAN TENGAH -- 76121","647105":"BALIKPAPAN SELATAN -- 76114","647106":"BALIKPAPAN KOTA -- 76114","647201":"PALARAN -- 75253","647202":"SAMARINDA SEBERANG -- 75131","647203":"SAMARINDA ULU -- 75124","647204":"SAMARINDA ILIR -- 75117","647205":"SAMARINDA UTARA -- 75118","647206":"SUNGAI KUNJANG -- 75125","647207":"SAMBUTAN -- 75115","647208":"SUNGAI PINANG -- 75119","647209":"SAMARINDA KOTA -- 75121","647210":"LOA JANAN ILIR -- 75131","647401":"BONTANG UTARA -- 75311","647402":"BONTANG SELATAN -- 75325","647403":"BONTANG BARAT -- 75313","650101":"TANJUNG PALAS -- 77211","650102":"TANJUNG PALAS BARAT -- 77217","650103":"TANJUNG PALAS UTARA -- 77215","650104":"TANJUNG PALAS TIMUR -- 77215","650105":"TANJUNG SELOR -- 77212","650106":"TANJUNG PALAS TENGAH -- 77216","650107":"PESO -- 77261","650108":"PESO HILIR -- 77261","650109":"SEKATAK -- 77263","650110":"BUNYU -- 77281","650201":"MENTARANG -- 77555","650202":"MALINAU KOTA -- 77554","650203":"PUJUNGAN -- 77562","650204":"KAYAN HILIR -- 77571","650205":"KAYAN HULU -- 77572","650206":"MALINAU SELATAN -- 77554","650207":"MALINAU UTARA -- 77554","650208":"MALINAU BARAT -- 77554","650209":"SUNGAI BOH -- 77573","650210":"KAYAN SELATAN -- 77573","650211":"BAHAU HULU -- 77562","650212":"MENTARANG HULU -- 77155","650213":"MALINAU SELATAN HILIR -- 77554","650214":"MALINAU SELATAN HULU -- 77554","650215":"SUNGAI TUBU -- 77555","650301":"SEBATIK -- 77483","650302":"NUNUKAN -- 77482","650303":"SEMBAKUNG -- 77453","650304":"LUMBIS -- 77457","650305":"KRAYAN -- 77456","650306":"SEBUKU -- 77482","650307":"KRAYAN SELATAN -- 77456","650308":"SEBATIK BARAT -- 77483","650309":"NUNUKAN SELATAN -- 77482","650310":"SEBATIK TIMUR -- 77483","650311":"SEBATIK UTARA -- 77483","650312":"SEBATIK TENGAH -- 77483","650313":"SEI MENGGARIS -- 77482","650314":"TULIN ONSOI -- 77482","650315":"LUMBIS OGONG -- 77457","650316":"SEMBAKUNG ATULAI -- -","650401":"SESAYAP -- 77152","650402":"SESAYAP HILIR -- 77152","650403":"TANA LIA -- 77453","650405":"MURUK RIAN -- -","657101":"TARAKAN BARAT -- 77111","657102":"TARAKAN TENGAH -- 77113","657103":"TARAKAN TIMUR -- 77115","657104":"TARAKAN UTARA -- 77116","710105":"SANG TOMBOLANG -- 95762","710109":"DUMOGA BARAT -- 95773","710110":"DUMOGA TIMUR -- 95772","710111":"DUMOGA UTARA -- 95772","710112":"LOLAK -- 95761","710113":"BOLAANG -- 95752","710114":"LOLAYAN -- 95771","710119":"PASSI BARAT -- 95751","710120":"POIGAR -- 95753","710122":"PASSI TIMUR -- 95751","710131":"BOLAANG TIMUR -- 95752","710132":"BILALANG -- 95751","710133":"DUMOGA -- 95772","710134":"DUMOGA TENGGARA -- 95772","710135":"DUMOGA TENGAH -- 95773","710201":"TONDANO BARAT -- 95616","710202":"TONDANO TIMUR -- 95612","710203":"ERIS -- 95683","710204":"KOMBI -- 95684","710205":"LEMBEAN TIMUR -- 95683","710206":"KAKAS -- 95682","710207":"TOMPASO -- 95693","710208":"REMBOKEN -- 95681","710209":"LANGOWAN TIMUR -- 95694","710210":"LANGOWAN BARAT -- 95694","710211":"SONDER -- 95691","710212":"KAWANGKOAN -- 95692","710213":"PINELENG -- 95661","710214":"TOMBULU -- 95661","710215":"TOMBARIRI -- 95651","710216":"TONDANO UTARA -- 95614","710217":"LANGOWAN SELATAN -- 95694","710218":"TONDANO SELATAN -- 95618","710219":"LANGOWAN UTARA -- 95694","710220":"KAKAS BARAT -- 95682","710221":"KAWANGKOAN UTARA -- 95692","710222":"KAWANGKOAN BARAT -- 95692","710223":"MANDOLANG -- 95661","710224":"TOMBARIRI TIMUR -- 95651","710225":"TOMPASO BARAT -- 95693","710308":"TABUKAN UTARA -- 95856","710309":"NUSA TABUKAN -- 95856","710310":"MANGANITU SELATAN -- 95854","710311":"TATOARENG -- 95854","710312":"TAMAKO -- 95855","710313":"MANGANITU -- 95853","710314":"TABUKAN TENGAH -- 95857","710315":"TABUKAN SELATAN -- 95858","710316":"KENDAHE -- 95852","710317":"TAHUNA -- 95818","710319":"TABUKAN SELATAN TENGAH -- 95858","710320":"TABUKAN SELATAN TENGGARA -- 95858","710323":"TAHUNA BARAT -- 95818","710324":"TAHUNA TIMUR -- 95814","710325":"KEPULAUAN MARORE -- 95856","710401":"LIRUNG -- 95871","710402":"BEO -- 95881","710403":"RAINIS -- 95882","710404":"ESSANG -- 95883","710405":"NANUSA -- 95884","710406":"KABARUAN -- 95872","710407":"MELONGUANE -- 95885","710408":"GEMEH -- 95883","710409":"DAMAU -- 95872","710410":"TAMPAN' AMMA -- -","710411":"SALIBABU -- 95871","710412":"KALONGAN -- 95871","710413":"MIANGAS -- 95884","710414":"BEO UTARA -- 95881","710415":"PULUTAN -- 95882","710416":"MELONGUANE TIMUR -- 95885","710417":"MORONGE -- 95871","710418":"BEO SELATAN -- 95881","710419":"ESSANG SELATAN -- 95883","710501":"MODOINDING -- 95958","710502":"TOMPASO BARU -- 95357","710503":"RANOYAPO -- 95999","710507":"MOTOLING -- 95956","710508":"SINONSAYANG -- 95959","710509":"TENGA -- 95775","710510":"AMURANG -- 95954","710512":"TUMPAAN -- 95352","710513":"TARERAN -- 95953","710515":"KUMELEMBUAI -- 95956","710516":"MAESAAN -- 95357","710517":"AMURANG BARAT -- 95955","710518":"AMURANG TIMUR -- 95954","710519":"TATAPAAN -- 95352","710521":"MOTOLING BARAT -- 95956","710522":"MOTOLING TIMUR -- 95956","710523":"SULUUN TARERAN -- 95953","710601":"KEMA -- 95379","710602":"KAUDITAN -- 95372","710603":"AIRMADIDI -- 95371","710604":"WORI -- 95376","710605":"DIMEMBE -- 95373","710606":"LIKUPANG BARAT -- 95377","710607":"LIKUPANG TIMUR -- 95375","710608":"KALAWAT -- 95378","710609":"TALAWAAN -- 95373","710610":"LIKUPANG SELATAN -- 95375","710701":"RATAHAN -- 95995","710702":"PUSOMAEN -- 95997","710703":"BELANG -- 95997","710704":"RATATOTOK -- 95997","710705":"TOMBATU -- 95996","710706":"TOULUAAN -- 95998","710707":"TOULUAAN SELATAN -- 95998","710708":"SILIAN RAYA -- 95998","710709":"TOMBATU TIMUR -- 95996","710710":"TOMBATU UTARA -- 95996","710711":"PASAN -- 95995","710712":"RATAHAN TIMUR -- 95995","710801":"SANGKUB -- 95762","710802":"BINTAUNA -- 95763","710803":"BOLANGITANG TIMUR -- 95764","710804":"BOLANGITANG BARAT -- 95764","710805":"KAIDIPANG -- 95765","710806":"PINOGALUMAN -- 95765","710901":"SIAU TIMUR -- 95861","710902":"SIAU BARAT -- 95862","710903":"TAGULANDANG -- 95863","710904":"SIAU TIMUR SELATAN -- 95861","710905":"SIAU BARAT SELATAN -- 95862","710906":"TAGULANDANG UTARA -- 95863","710907":"BIARO -- 95864","710908":"SIAU BARAT UTARA -- 95862","710909":"SIAU TENGAH -- 95861","710910":"TAGULANDANG SELATAN -- 95863","711001":"TUTUYAN -- 95782","711002":"KOTABUNAN -- 95782","711003":"NUANGAN -- 95775","711004":"MODAYAG -- 95781","711005":"MODAYAG BARAT -- 95781","711101":"BOLAANG UKI -- 95774","711102":"POSIGADAN -- 95774","711103":"PINOLOSIAN -- 95775","711104":"PINOLOSIAN TENGAH -- 95775","711105":"PINOLOSIAN TIMUR -- 95775","717101":"BUNAKEN -- 95231","717102":"TUMINITING -- 95239","717103":"SINGKIL -- 95231","717104":"WENANG -- 95113","717105":"TIKALA -- 95125","717106":"SARIO -- 95116","717107":"WANEA -- 95117","717108":"MAPANGET -- 95251","717109":"MALALAYANG -- 95115","717110":"BUNAKEN KEPULAUAN -- 95231","717111":"PAAL DUA -- 95127","717201":"LEMBEH SELATAN -- 95552","717202":"MADIDIR -- 95513","717203":"RANOWULU -- 95537","717204":"AERTEMBAGA -- 95526","717205":"MATUARI -- 95545","717206":"GIRIAN -- 95544","717207":"MAESA -- 95511","717208":"LEMBEH UTARA -- 95551","717301":"TOMOHON SELATAN -- 95433","717302":"TOMOHON TENGAH -- 95441","717303":"TOMOHON UTARA -- 95416","717304":"TOMOHON BARAT -- 95424","717305":"TOMOHON TIMUR -- 95449","717401":"KOTAMOBAGU UTARA -- 95713","717402":"KOTAMOBAGU TIMUR -- 95719","717403":"KOTAMOBAGU SELATAN -- 95717","717404":"KOTAMOBAGU BARAT -- 95715","720101":"BATUI -- 94762","720102":"BUNTA -- 94753","720103":"KINTOM -- 94761","720104":"LUWUK -- 94711","720105":"LAMALA -- 94771","720106":"BALANTAK -- 94773","720107":"PAGIMANA -- 94752","720108":"BUALEMO -- 94752","720109":"TOILI -- 94765","720110":"MASAMA -- 94772","720111":"LUWUK TIMUR -- 94723","720112":"TOILI BARAT -- 94765","720113":"NUHON -- 94753","720114":"MOILONG -- 94765","720115":"BATUI SELATAN -- 94762","720116":"LOBU -- 94752","720117":"SIMPANG RAYA -- 94753","720118":"BALANTAK SELATAN -- 94773","720119":"BALANTAK UTARA -- 94773","720120":"LUWUK SELATAN -- 94717","720121":"LUWUK UTARA -- 94711","720122":"MANTOH -- 94771","720123":"NAMBO -- 94761","720201":"POSO KOTA -- 94616","720202":"POSO PESISIR -- 94652","720203":"LAGE -- 94661","720204":"PAMONA PUSELEMBA -- 94663","720205":"PAMONA TIMUR -- 94663","720206":"PAMONA SELATAN -- 94664","720207":"LORE UTARA -- 94653","720208":"LORE TENGAH -- 94653","720209":"LORE SELATAN -- 94654","720218":"POSO PESISIR UTARA -- 94652","720219":"POSO PESISIR SELATAN -- 94652","720220":"PAMONA BARAT -- 94664","720221":"POSO KOTA SELATAN -- 94614","720222":"POSO KOTA UTARA -- 94616","720223":"LORE BARAT -- 94654","720224":"LORE TIMUR -- 94653","720225":"LORE PIORE -- 94653","720226":"PAMONA TENGGARA -- 94664","720227":"PAMONA UTARA -- 94663","720304":"RIO PAKAVA -- 94362","720308":"BANAWA -- 94351","720309":"LABUAN -- 94352","720310":"SINDUE -- 94353","720311":"SIRENJA -- 94354","720312":"BALAESANG -- 94355","720314":"SOJOL -- 94356","720318":"BANAWA SELATAN -- 94351","720319":"TANANTOVEA -- 94352","720321":"PANEMBANI -- -","720324":"SINDUE TOMBUSABORA -- 94353","720325":"SINDUE TOBATA -- 94353","720327":"BANAWA TENGAH -- 94351","720330":"SOJOL UTARA -- 94356","720331":"BALAESANG TANJUNG -- 94355","720401":"DAMPAL SELATAN -- 94554","720402":"DAMPAL UTARA -- 94553","720403":"DONDO -- 94552","720404":"BASIDONDO -- 94552","720405":"OGODEIDE -- 94516","720406":"LAMPASIO -- 94518","720407":"BAOLAN -- 94514","720408":"GALANG -- 94561","720409":"TOLI-TOLI UTARA -- -","720410":"DAKO PEMEAN -- -","720501":"MOMUNU -- 94565","720502":"LAKEA -- 94563","720503":"BOKAT -- 94566","720504":"BUNOBOGU -- 94567","720505":"PALELEH -- 94568","720507":"TILOAN -- 94565","720508":"BUKAL -- 94563","720509":"GADUNG -- 94568","720510":"KARAMAT -- 94563","720511":"PALELEH BARAT -- 94568","720605":"BUNGKU TENGAH -- 94973","720606":"BUNGKU SELATAN -- 94974","720607":"MENUI KEPULAUAN -- 94975","720608":"BUNGKU BARAT -- 94976","720609":"BUMI RAYA -- 94976","720610":"BAHODOPI -- 94974","720612":"WITA PONDA -- 94976","720615":"BUNGKU PESISIR -- 94974","720618":"BUNGKU TIMUR -- 94973","720703":"TOTIKUM -- 94884","720704":"TINANGKUNG -- 94885","720705":"LIANG -- 94883","720706":"BULAGI -- 94882","720707":"BUKO -- 94881","720709":"BULAGI SELATAN -- 94882","720711":"TINANGKUNG SELATAN -- 94885","720715":"TOTIKUM SELATAN -- 94884","720716":"PELING TENGAH -- 94883","720717":"BULAGI UTARA -- 94882","720718":"BUKO SELATAN -- 94881","720719":"TINANGKUNG UTARA -- 94885","720801":"PARIGI -- 94471","720802":"AMPIBABO -- 94474","720803":"TINOMBO -- 94475","720804":"MOUTONG -- 94479","720805":"TOMINI -- 94476","720806":"SAUSU -- 94473","720807":"BOLANO LAMBUNU -- 94479","720808":"KASIMBAR -- 94474","720809":"TORUE -- 94473","720810":"TINOMBO SELATAN -- 94475","720811":"PARIGI SELATAN -- 94471","720812":"MEPANGA -- 94476","720813":"TORIBULU -- 94474","720814":"TAOPA -- 94479","720815":"BALINGGI -- 94473","720816":"PARIGI BARAT -- 94471","720817":"SINIU -- 94474","720818":"PALASA -- 94476","720819":"PARIGI UTARA -- 94471","720820":"PARIGI TENGAH -- 94471","720821":"BOLANO -- 94479","720822":"ONGKA MALINO -- 94479","720823":"SIDOAN -- -","720901":"UNA UNA -- -","720902":"TOGEAN -- 94683","720903":"WALEA KEPULAUAN -- 94692","720904":"AMPANA TETE -- 94684","720905":"AMPANA KOTA -- 94683","720906":"ULUBONGKA -- 94682","720907":"TOJO BARAT -- 94681","720908":"TOJO -- 94681","720909":"WALEA BESAR -- 94692","720910":"RATOLINDO -- -","720911":"BATUDAKA -- -","720912":"TALATAKO -- -","721001":"SIGI BIROMARU -- 94364","721002":"PALOLO -- 94364","721003":"NOKILALAKI -- 94364","721004":"LINDU -- 94363","721005":"KULAWI -- 94363","721006":"KULAWI SELATAN -- 94363","721007":"PIPIKORO -- 94112","721008":"GUMBASA -- 94364","721009":"DOLO SELATAN -- 94361","721010":"TANAMBULAVA -- 94364","721011":"DOLO BARAT -- 94361","721012":"DOLO -- 94361","721013":"KINOVARO -- -","721014":"MARAWOLA -- 94362","721015":"MARAWOLA BARAT -- 94362","721101":"BANGGAI -- 94891","721102":"BANGGAI UTARA -- 94891","721103":"BOKAN KEPULAUAN -- 94892","721104":"BANGKURUNG -- 94892","721105":"LABOBO -- 94892","721106":"BANGGAI SELATAN -- 94891","721107":"BANGGAI TENGAH -- 94891","721201":"PETASIA -- 94971","721202":"PETASIA TIMUR -- 94971","721203":"LEMBO RAYA -- 94966","721204":"LEMBO -- 94966","721205":"MORI ATAS -- 94965","721206":"MORI UTARA -- 94965","721207":"SOYO JAYA -- 94971","721208":"BUNGKU UTARA -- 94972","721209":"MAMOSALATO -- 94972","727101":"PALU TIMUR -- 94111","727102":"PALU BARAT -- 94226","727103":"PALU SELATAN -- 94231","727104":"PALU UTARA -- 94146","727105":"ULUJADI -- 94228","727106":"TATANGA -- 94221","727107":"TAWAELI -- 94142","727108":"MANTIKULORE -- 94233","730101":"BENTENG -- 92812","730102":"BONTOHARU -- 92811","730103":"BONTOMATENE -- 92854","730104":"BONTOMANAI -- 92851","730105":"BONTOSIKUYU -- 92855","730106":"PASIMASUNGGU -- 92861","730107":"PASIMARANNU -- 92862","730108":"TAKA BONERATE -- 92861","730109":"PASILAMBENA -- 92863","730110":"PASIMASUNGGU TIMUR -- 92861","730111":"BUKI -- 92854","730201":"GANTORANG -- 92561","730202":"UJUNG BULU -- 92511","730203":"BONTO BAHARI -- 92571","730204":"BONTO TIRO -- 92572","730205":"HERLANG -- 92573","730206":"KAJANG -- 92574","730207":"BULUKUMPA -- 92552","730208":"KINDANG -- 92517","730209":"UJUNGLOE -- 92661","730210":"RILAUALE -- 92552","730301":"BISSAPPU -- 92451","730302":"BANTAENG -- 92411","730303":"EREMERASA -- 92415","730304":"TOMPO BULU -- 92461","730305":"PAJUKUKANG -- 92461","730306":"ULUERE -- 92451","730307":"GANTARANG KEKE -- 92461","730308":"SINOA -- 92451","730401":"BANGKALA -- 92352","730402":"TAMALATEA -- 92351","730403":"BINAMU -- 92316","730404":"BATANG -- 92361","730405":"KELARA -- 92371","730406":"BANGKALA BARAT -- 92352","730407":"BONTORAMBA -- 92351","730408":"TURATEA -- 92313","730409":"ARUNGKEKE -- 92361","730410":"RUMBIA -- 92371","730411":"TAROWANG -- 92361","730501":"MAPPAKASUNGGU -- 92232","730502":"MANGARABOMBANG -- 92261","730503":"POLOMBANGKENG SELATAN -- 92252","730504":"POLOMBANGKENG UTARA -- 92221","730505":"GALESONG SELATAN -- 92254","730506":"GALESONG UTARA -- 92255","730507":"PATTALLASSANG -- 92171","730508":"SANROBONE -- 92231","730509":"GALESONG -- 92255","730601":"BONTONOMPO -- 92153","730602":"BAJENG -- 92152","730603":"TOMPOBULLU -- -","730604":"TINGGIMONCONG -- 92174","730605":"PARANGLOE -- 92173","730606":"BONTOMARANNU -- 92171","730607":"PALANGGA -- -","730608":"SOMBA UPU -- -","730609":"BUNGAYA -- 92176","730610":"TOMBOLOPAO -- 92171","730611":"BIRINGBULU -- 90244","730612":"BAROMBONG -- 90225","730613":"PATTALASANG -- -","730614":"MANUJU -- 92173","730615":"BONTOLEMPANGANG -- 92176","730616":"BONTONOMPO SELATAN -- 92153","730617":"PARIGI -- 92174","730618":"BAJENG BARAT -- 92152","730701":"SINJAI BARAT -- 92653","730702":"SINJAI SELATAN -- 92661","730703":"SINJAI TIMUR -- 92671","730704":"SINJAI TENGAH -- 92652","730705":"SINJAI UTARA -- 92616","730706":"BULUPODDO -- 92654","730707":"SINJAI BORONG -- 92662","730708":"TELLU LIMPOE -- 91662","730709":"PULAU SEMBILAN -- 92616","730801":"BONTOCANI -- 92768","730802":"KAHU -- 92767","730803":"KAJUARA -- 92776","730804":"SALOMEKKO -- 92775","730805":"TONRA -- 92774","730806":"LIBURENG -- 92766","730807":"MARE -- 92773","730808":"SIBULUE -- 92781","730809":"BAREBBO -- 92771","730810":"CINA -- 92772","730811":"PONRE -- 92765","730812":"LAPPARIAJA -- 92763","730813":"LAMURU -- 92764","730814":"ULAWENG -- 92762","730815":"PALAKKA -- 92761","730816":"AWANGPONE -- 92776","730817":"TELLU SIATTINGE -- 92752","730818":"AJANGALE -- 92755","730819":"DUA BOCCOE -- 92753","730820":"CENRANA -- 92754","730821":"TANETE RIATTANG -- 92716","730822":"TANETE RIATTANG BARAT -- 92735","730823":"TANETE RIATTANG TIMUR -- 92716","730824":"AMALI -- 92755","730825":"TELLULIMPOE -- 91662","730826":"BENGO -- 92763","730827":"PATIMPENG -- 92768","730901":"MANDAI -- 90552","730902":"CAMBA -- 90562","730903":"BANTIMURUNG -- 90561","730904":"MAROS BARU -- 90516","730905":"BONTOA -- 90554","730906":"MALLLAWA -- -","730907":"TANRALILI -- 90553","730908":"MARUSU -- 90552","730909":"SIMBANG -- 90561","730910":"CENRANA -- 92754","730911":"TOMPOBULU -- 92461","730912":"LAU -- 90871","730913":"MONCONG LOE -- 90562","730914":"TURIKALE -- 90516","731001":"LIUKANG TANGAYA -- 90673","731002":"LIUKANG KALMAS -- 90672","731003":"LIUKANG TUPABBIRING -- 90671","731004":"PANGKAJENE -- 90612","731005":"BALOCCI -- 90661","731006":"BUNGORO -- 90651","731007":"LABAKKANG -- 90653","731008":"MARANG -- 90654","731009":"SEGERI -- 90655","731010":"MINASA TENE -- 90614","731011":"MANDALLE -- 90655","731012":"TONDONG TALLASA -- 90561","731101":"TANETE RIAJA -- 90762","731102":"TANETE RILAU -- 90761","731103":"BARRU -- 90712","731104":"SOPPENG RIAJA -- 90752","731105":"MALLUSETASI -- 90753","731106":"PUJANANTING -- 90762","731107":"BALUSU -- 91855","731201":"MARIORIWAWO -- 90862","731202":"LILIRAJA -- 90861","731203":"LILIRILAU -- 90871","731204":"LALABATA -- 90814","731205":"MARIORIAWA -- 90852","731206":"DONRI DONRI -- -","731207":"GANRA -- 90861","731208":"CITTA -- 90861","731301":"SABANGPARU -- -","731302":"PAMMANA -- 90971","731303":"TAKKALALLA -- 90981","731304":"SAJOANGING -- 90982","731305":"MAJAULENG -- 90991","731306":"TEMPE -- 91992","731307":"BELAWA -- 90953","731308":"TANASITOLO -- 90951","731309":"MANIANGPAJO -- 90952","731310":"PITUMPANUA -- 90992","731311":"BOLA -- 90984","731312":"PENRANG -- 90983","731313":"GILIRENG -- 90954","731314":"KEERA -- 90993","731401":"PANCA LAUTAN -- 91672","731402":"TELLU LIMPOE -- 91662","731403":"WATANG PULU -- 91661","731404":"BARANTI -- 91652","731405":"PANCA RIJANG -- 91651","731406":"KULO -- 91653","731407":"MARITENGNGAE -- 91611","731408":"WT. SIDENRENG -- -","731409":"DUA PITUE -- 91681","731410":"PITU RIAWA -- 91683","731411":"PITU RAISE -- 91691","731501":"MATIRRO SOMPE -- -","731502":"SUPPA -- 91272","731503":"MATTIRO BULU -- 91271","731504":"WATANG SAWITO -- -","731505":"PATAMPANUA -- 91252","731506":"DUAMPANUA -- 91253","731507":"LEMBANG -- 91254","731508":"CEMPA -- 91262","731509":"TIROANG -- 91256","731510":"LANSIRANG -- -","731511":"PALETEANG -- 91215","731512":"BATU LAPPA -- 91253","731601":"MAIWA -- 91761","731602":"ENREKANG -- 91711","731603":"BARAKA -- 91753","731604":"ANGGERAJA -- 91752","731605":"ALLA -- 90981","731606":"BUNGIN -- 91761","731607":"CENDANA -- 91711","731608":"CURIO -- 91754","731609":"MALUA -- 91752","731610":"BUNTU BATU -- 91753","731611":"MASALLE -- 91754","731612":"BAROKO -- 91754","731701":"BASSE SANGTEMPE -- 91992","731702":"LAROMPONG -- 91998","731703":"SULI -- 91996","731704":"BAJO -- 91995","731705":"BUA PONRANG -- 91993","731706":"WALENRANG -- 91951","731707":"BELOPA -- 91994","731708":"BUA -- 91993","731709":"LAMASI -- 91952","731710":"LAROMPONG SELATAN -- 91998","731711":"PONRANG -- 91999","731712":"LATIMOJONG -- 91921","731713":"KAMANRE -- 91994","731714":"BELOPA UTARA -- 91994","731715":"WALENRANG BARAT -- 91951","731716":"WALENRANG UTARA -- 91952","731717":"WALENRANG TIMUR -- 91951","731718":"LAMASI TIMUR -- 91951","731719":"SULI BARAT -- 91996","731720":"BAJO BARAT -- 91995","731721":"PONRANG SELATAN -- 91999","731722":"BASSE SANGTEMPE UTARA -- 91992","731801":"SALUPUTI -- -","731802":"BITTUANG -- 91856","731803":"BONGGAKARADENG -- 91872","731805":"MAKALE -- 91811","731809":"SIMBUANG -- 91873","731811":"RANTETAYO -- 91862","731812":"MENGKENDEK -- 91871","731813":"SANGALLA -- 91881","731819":"GANDANGBATU SILLANAN -- 91871","731820":"REMBON -- 91861","731827":"MAKALE UTARA -- 91812","731828":"MAPPAK -- 92232","731829":"MAKALE SELATAN -- 91815","731831":"MASANDA -- 91854","731833":"SANGALLA SELATAN -- 91881","731834":"SANGALLA UTARA -- 91881","731835":"MALIMBONG BALEPE -- 91861","731837":"RANO -- 91872","731838":"KURRA -- 91862","732201":"MALANGKE -- 92957","732202":"BONE BONE -- -","732203":"MASAMBA -- 92916","732204":"SABBANG -- 92955","732205":"LIMBONG -- 91861","732206":"SUKAMAJU -- 92963","732207":"SEKO -- 92956","732208":"MALANGKE BARAT -- 92957","732209":"RAMPI -- 92964","732210":"MAPPEDECENG -- 92917","732211":"BAEBUNTA -- 92965","732212":"TANA LILI -- 91966","732401":"MANGKUTANA -- 92973","732402":"NUHA -- 92983","732403":"TOWUTI -- 92982","732404":"MALILI -- 92981","732405":"ANGKONA -- 92985","732406":"WOTU -- 92971","732407":"BURAU -- 92975","732408":"TOMONI -- 92972","732409":"TOMONI TIMUR -- 92972","732410":"KALAENA -- 92973","732411":"WASUPONDA -- 92983","732601":"RANTEPAO -- 91835","732602":"SESEAN -- 91853","732603":"NANGGALA -- 91855","732604":"RINDINGALLO -- 91854","732605":"BUNTAO -- 91853","732606":"SA'DAN -- -","732607":"SANGGALANGI -- 91852","732608":"SOPAI -- 92119","732609":"TIKALA -- 91833","732610":"BALUSU -- 91855","732611":"TALLUNGLIPU -- 91832","732612":"DENDE' PIONGAN NAPO -- -","732613":"BUNTU PEPASAN -- 91854","732614":"BARUPPU -- 91854","732615":"KESU -- 91852","732616":"TONDON -- 90561","732617":"BANGKELEKILA -- 91853","732618":"RANTEBUA -- 91853","732619":"SESEAN SULOARA -- 91853","732620":"KAPALA PITU -- 91854","732621":"AWAN RANTE KARUA -- 91854","737101":"MARISO -- 90126","737102":"MAMAJANG -- 90131","737103":"MAKASAR -- -","737104":"UJUNG PANDANG -- 90111","737105":"WAJO -- 90173","737106":"BONTOALA -- 90153","737107":"TALLO -- 90212","737108":"UJUNG TANAH -- 90167","737109":"PANAKUKKANG -- -","737110":"TAMALATE -- 90224","737111":"BIRINGKANAYA -- 90243","737112":"MANGGALA -- 90234","737113":"RAPPOCINI -- 90222","737114":"TAMALANREA -- 90244","737201":"BACUKIKI -- 91121","737202":"UJUNG -- 92661","737203":"SOREANG -- 91131","737204":"BACUKIKI BARAT -- 91121","737301":"WARA -- 91922","737302":"WARA UTARA -- 91911","737303":"WARA SELATAN -- 91959","737304":"TELLUWANUA -- 91958","737305":"WARA TIMUR -- 91921","737306":"WARA BARAT -- 91921","737307":"SENDANA -- 91925","737308":"MUNGKAJANG -- 91925","737309":"BARA -- 92653","740101":"WUNDULAKO -- 93561","740104":"KOLAKA -- 93517","740107":"POMALAA -- 93562","740108":"WATUBANGGA -- -","740110":"WOLO -- 93754","740112":"BAULA -- 93561","740114":"LATAMBAGA -- 93512","740118":"TANGGETADA -- 93563","740120":"SAMATURU -- 93915","740124":"TOARI -- 93563","740125":"POLINGGONA -- 93563","740127":"IWOIMENDAA -- -","740201":"LAMBUYA -- 93464","740202":"UNAAHA -- 93413","740203":"WAWOTOBI -- 93461","740204":"PONDIDAHA -- 93463","740205":"SAMPARA -- 93354","740210":"ABUKI -- 93452","740211":"SOROPIA -- 93351","740215":"TONGAUNA -- 93461","740216":"LATOMA -- 93461","740217":"PURIALA -- 93464","740218":"UEPAI -- 93464","740219":"WONGGEDUKU -- 93463","740220":"BESULUTU -- 93354","740221":"BONDOALA -- 93354","740223":"ROUTA -- 93653","740224":"ANGGABERI -- 93419","740225":"MELUHU -- 93461","740228":"AMONGGEDO -- 93463","740231":"ASINUA -- 93452","740232":"KONAWE -- 93461","740233":"KAPOIALA -- 93354","740236":"LALONGGASUMEETO -- 93351","740237":"ONEMBUTE -- 93464","740306":"NAPABALANO -- 93654","740307":"MALIGANO -- 93674","740313":"WAKORUMBA SELATAN -- 93674","740314":"LASALEPA -- 93654","740315":"BATALAIWARU -- 93614","740316":"KATOBU -- 93611","740317":"DURUKA -- 93659","740318":"LOHIA -- 93658","740319":"WATOPUTE -- 93656","740320":"KONTUNAGA -- 93658","740323":"KABANGKA -- 93664","740324":"KABAWO -- 93661","740325":"PARIGI -- 93663","740326":"BONE -- 93663","740327":"TONGKUNO -- 93662","740328":"PASIR PUTIH -- 93674","740330":"KONTU KOWUNA -- 93661","740331":"MAROBO -- 93663","740332":"TONGKUNO SELATAN -- 93662","740333":"PASI KOLAGA -- 93674","740334":"BATUKARA -- 93674","740337":"TOWEA -- 93654","740411":"PASARWAJO -- 93754","740422":"KAPONTORI -- 93755","740423":"LASALIMU -- 93756","740424":"LASALIMU SELATAN -- 93756","740427":"SIOTAPINA -- -","740428":"WOLOWA -- 93754","740429":"WABULA -- 93754","740501":"TINANGGEA -- 93885","740502":"ANGATA -- 93875","740503":"ANDOOLO -- 93819","740504":"PALANGGA -- 93883","740505":"LANDONO -- 93873","740506":"LAINEA -- 93881","740507":"KONDA -- 93874","740508":"RANOMEETO -- 93871","740509":"KOLONO -- 93395","740510":"MORAMO -- 93891","740511":"LAONTI -- 93892","740512":"LALEMBUU -- 93885","740513":"BENUA -- 93875","740514":"PALANGGA SELATAN -- 93883","740515":"MOWILA -- 93873","740516":"MORAMO UTARA -- 93891","740517":"BUKE -- 93815","740518":"WOLASI -- 93874","740519":"LAEYA -- 93881","740520":"BAITO -- 93883","740521":"BASALA -- 93875","740522":"RANOMEETO BARAT -- 93871","740601":"POLEANG -- 93773","740602":"POLEANG TIMUR -- 93773","740603":"RAROWATU -- 93774","740604":"RUMBIA -- 93771","740605":"KABAENA -- 93781","740606":"KABAENA TIMUR -- 93783","740607":"POLEANG BARAT -- 93772","740608":"MATA OLEO -- 93771","740609":"RAROWATU UTARA -- 93774","740610":"POLEANG UTARA -- 93773","740611":"POLEANG SELATAN -- 93773","740612":"POLEANG TENGGARA -- 93773","740613":"KABAENA SELATAN -- 93781","740614":"KABAENA BARAT -- 93781","740615":"KABAENA UTARA -- 93781","740616":"KABAENA TENGAH -- 93783","740617":"KEP. MASALOKA RAYA -- -","740618":"RUMBIA TENGAH -- 93771","740619":"POLEANG TENGAH -- 93772","740620":"TONTONUNU -- 93772","740621":"LANTARI JAYA -- 93774","740622":"MATA USU -- 93774","740701":"WANGI-WANGI -- 93795","740702":"KALEDUPA -- 93792","740703":"TOMIA -- 93793","740704":"BINONGKO -- 93794","740705":"WANGI WANGI SELATAN -- -","740706":"KALEDUPA SELATAN -- 93792","740707":"TOMIA TIMUR -- 93793","740708":"TOGO BINONGKO -- 93794","740801":"LASUSUA -- 93916","740802":"PAKUE -- 93954","740803":"BATU PUTIH -- 93955","740804":"RANTE ANGIN -- 93956","740805":"KODEOHA -- 93957","740806":"NGAPA -- 93958","740807":"WAWO -- 93461","740808":"LAMBAI -- 93956","740809":"WATUNOHU -- 93958","740810":"PAKUE TENGAH -- 93954","740811":"PAKUE UTARA -- 93954","740812":"POREHU -- 93955","740813":"TOLALA -- 93955","740814":"TIWU -- 93957","740815":"KATOI -- 93913","740901":"ASERA -- 93353","740902":"WIWIRANO -- 93353","740903":"LANGGIKIMA -- 93352","740904":"MOLAWE -- 93352","740905":"LASOLO -- 93352","740906":"LEMBO -- 93352","740907":"SAWA -- 93352","740908":"OHEO -- 93353","740909":"ANDOWIA -- 93353","740910":"MOTUI -- 93352","741001":"KULISUSU -- 93672","741002":"KAMBOWA -- 93673","741003":"BONEGUNU -- 93673","741004":"KULISUSU BARAT -- 93672","741005":"KULISUSU UTARA -- 93672","741006":"WAKORUMBA UTARA -- 93671","741101":"TIRAWUTA -- 93572","741102":"LOEA -- 93572","741103":"LADONGI -- 93573","741104":"POLI POLIA -- 93573","741105":"LAMBANDIA -- 93573","741106":"LALOLAE -- 93572","741107":"MOWEWE -- 93571","741108":"ULUIWOI -- 93575","741109":"TINONDO -- 93571","741110":"AERE -- -","741111":"UEESI -- -","741201":"WAWONII BARAT -- 93393","741202":"WAWONII UTARA -- 93393","741203":"WAWONII TIMUR LAUT -- 93393","741204":"WAWONII TIMUR -- 93393","741205":"WAWONII TENGGARA -- 93393","741206":"WAWONII SELATAN -- 93393","741207":"WAWONII TENGAH -- 93393","741301":"SAWERIGADI -- 93657","741302":"BARANGKA -- 93652","741303":"LAWA -- 93753","741304":"WADAGA -- 93652","741305":"TIWORO SELATAN -- 93664","741306":"MAGINTI -- 93664","741307":"TIWORO TENGAH -- 93653","741308":"TIWORO UTARA -- 93653","741309":"TIWORO KEPULAUAN -- 93653","741310":"KUSAMBI -- 93655","741311":"NAPANO KUSAMBI -- 93655","741401":"LAKUDO -- 93763","741402":"MAWASANGKA TIMUR -- 93762","741403":"MAWASANGKA TENGAH -- 93762","741404":"MAWASANGKA -- 93762","741405":"TALAGA RAYA -- 93781","741406":"GU -- 93761","741407":"SANGIA WAMBULU -- -","741501":"BATAUGA -- 93752","741502":"SAMPOLAWA -- 93753","741503":"LAPANDEWA -- 93753","741504":"BATU ATAS -- 93753","741505":"SIOMPU BARAT -- 93752","741506":"SIOMPU -- 93752","741507":"KADATUA -- 93752","747101":"MANDONGA -- 93112","747102":"KENDARI -- 93123","747103":"BARUGA -- 93116","747104":"POASIA -- 93231","747105":"KENDARI BARAT -- 93123","747106":"ABELI -- 93234","747107":"WUA-WUA -- 93118","747108":"KADIA -- 93118","747109":"PUUWATU -- 93115","747110":"KAMBU -- 93231","747201":"BETOAMBARI -- 93724","747202":"WOLIO -- 93714","747203":"SORA WALIO -- 93757","747204":"BUNGI -- 93758","747205":"KOKALUKUNA -- 93716","747206":"MURHUM -- 93727","747207":"LEA-LEA -- 93758","747208":"BATUPOARO -- 93721","750101":"LIMBOTO -- 96212","750102":"TELAGA -- 96181","750103":"BATUDAA -- 96271","750104":"TIBAWA -- 96251","750105":"BATUDAA PANTAI -- 96272","750109":"BOLIYOHUTO -- 96264","750110":"TELAGA BIRU -- 96181","750111":"BONGOMEME -- 96271","750113":"TOLANGOHULA -- 96214","750114":"MOOTILANGO -- 96127","750116":"PULUBALA -- 96127","750117":"LIMBOTO BARAT -- 96215","750118":"TILANGO -- 96123","750119":"TABONGO -- 96271","750120":"BILUHU -- 96272","750121":"ASPARAGA -- 96214","750122":"TALAGA JAYA -- -","750123":"BILATO -- 96264","750124":"DUNGALIYO -- 96271","750201":"PAGUYAMAN -- 96261","750202":"WONOSARI -- 96262","750203":"DULUPI -- 96263","750204":"TILAMUTA -- 96263","750205":"MANANGGU -- 96265","750206":"BOTUMOITA -- 96264","750207":"PAGUYAMAN PANTAI -- 96261","750301":"TAPA -- 96582","750302":"KABILA -- 96583","750303":"SUWAWA -- 96584","750304":"BONEPANTAI -- 96585","750305":"BULANGO UTARA -- 96582","750306":"TILONGKABILA -- 96583","750307":"BOTUPINGGE -- 96583","750308":"KABILA BONE -- 96583","750309":"BONE -- 96585","750310":"BONE RAYA -- 96585","750311":"SUWAWA TIMUR -- 96584","750312":"SUWAWA SELATAN -- 96584","750313":"SUWAWA TENGAH -- 96584","750314":"BULANGO ULU -- 96582","750315":"BULANGO SELATAN -- 96582","750316":"BULANGO TIMUR -- 96582","750317":"BULAWA -- 96585","750318":"PINOGU -- 96584","750401":"POPAYATO -- 96467","750402":"LEMITO -- 96468","750403":"RANDANGAN -- 96469","750404":"MARISA -- 96266","750405":"PAGUAT -- 96265","750406":"PATILANGGIO -- 96266","750407":"TALUDITI -- 96469","750408":"DENGILO -- 96265","750409":"BUNTULIA -- 96266","750410":"DUHIADAA -- 96266","750411":"WANGGARASI -- 96468","750412":"POPAYATO TIMUR -- 96467","750413":"POPAYATO BARAT -- 96467","750501":"ATINGGOLA -- 96253","750502":"KWANDANG -- 96252","750503":"ANGGREK -- 96525","750504":"SUMALATA -- 96254","750505":"TOLINGGULA -- 96524","750506":"GENTUMA RAYA -- 96253","750507":"TOMOLITO -- 96252","750508":"PONELO KEPULAUAN -- 96252","750509":"MONANO -- 96525","750510":"BIAU -- 96524","750511":"SUMALATA TIMUR -- 96254","757101":"KOTA BARAT -- 96136","757102":"KOTA SELATAN -- 96111","757103":"KOTA UTARA -- 96121","757104":"DUNGINGI -- 96138","757105":"KOTA TIMUR -- 96114","757106":"KOTA TENGAH -- 96128","757107":"SIPATANA -- 96124","757108":"DUMBO RAYA -- 96118","757109":"HULONTHALANGI -- 96116","760101":"BAMBALAMOTU -- 91574","760102":"PASANGKAYU -- 91571","760103":"BARAS -- 91572","760104":"SARUDU -- 91573","760105":"DAPURANG -- 91512","760106":"DURIPOKU -- 91573","760107":"BULU TABA -- 91572","760108":"TIKKE RAYA -- 91571","760109":"PEDONGGA -- 91571","760110":"BAMBAIRA -- 91574","760111":"SARJO -- 91574","760112":"LARIANG -- 91572","760201":"MAMUJU -- 91514","760202":"TAPALANG -- 91352","760203":"KALUKKU -- 91561","760204":"KALUMPANG -- 91562","760207":"PAPALANG -- 91563","760208":"SAMPAGA -- 91563","760211":"TOMMO -- 91562","760212":"SIMBORO DAN KEPULAUAN -- 91512","760213":"TAPALANG BARAT -- 91352","760215":"BONEHAU -- 91562","760216":"KEP. BALA BALAKANG -- 91512","760301":"MAMBI -- 91371","760302":"ARALLE -- 91373","760303":"MAMASA -- 91362","760304":"PANA -- 91363","760305":"TABULAHAN -- 91372","760306":"SUMARORONG -- 91361","760307":"MESSAWA -- 91361","760308":"SESENAPADANG -- 91365","760309":"TANDUK KALUA -- 91366","760310":"TABANG -- 91364","760311":"BAMBANG -- 91371","760312":"BALLA -- 91366","760313":"NOSU -- 91363","760314":"TAWALIAN -- 91365","760315":"RANTEBULAHAN TIMUR -- 91371","760316":"BUNTUMALANGKA -- 91373","760317":"MEHALAAN -- 91371","760401":"TINAMBUNG -- 91354","760402":"CAMPALAGIAN -- 91353","760403":"WONOMULYO -- 91352","760404":"POLEWALI -- 91314","760405":"TUTAR -- 91355","760406":"BINUANG -- 91312","760407":"TAPANGO -- 91352","760408":"MAPILLI -- 91353","760409":"MATANGNGA -- 91352","760410":"LUYO -- 91353","760411":"LIMBORO -- 91413","760412":"BALANIPA -- 91354","760413":"ANREAPI -- 91315","760414":"MATAKALI -- 91352","760415":"ALLU -- 96365","760416":"BULO -- 91353","760501":"BANGGAE -- 91411","760502":"PAMBOANG -- 91451","760503":"SENDANA -- 91452","760504":"MALUNDA -- 91453","760505":"ULUMANDA -- -","760506":"TAMMERODO SENDANA -- -","760507":"TUBO SENDANA -- 91452","760508":"BANGGAE TIMUR -- 91411","760601":"TOBADAK -- 91563","760602":"PANGALE -- 91563","760603":"BUDONG-BUDONG -- 91563","760604":"TOPOYO -- 91563","760605":"KAROSSA -- 91512","810101":"AMAHAI -- 97516","810102":"TEON NILA SERUA -- 97558","810106":"SERAM UTARA -- 97557","810109":"BANDA -- 97593","810111":"TEHORU -- 97553","810112":"SAPARUA -- 97592","810113":"PULAU HARUKU -- 97591","810114":"SALAHUTU -- 97582","810115":"LEIHITU -- 97581","810116":"NUSA LAUT -- 97511","810117":"KOTA MASOHI -- -","810120":"SERAM UTARA BARAT -- 97557","810121":"TELUK ELPAPUTIH -- 97516","810122":"LEIHITU BARAT -- 97581","810123":"TELUTIH -- 97553","810124":"SERAM UTARA TIMUR SETI -- 97557","810125":"SERAM UTARA TIMUR KOBI -- 97557","810126":"SAPARUA TIMUR -- 97592","810201":"KEI KECIL -- 97615","810203":"KEI BESAR -- 97661","810204":"KEI BESAR SELATAN -- 97661","810205":"KEI BESAR UTARA TIMUR -- 97661","810213":"KEI KECIL TIMUR -- 97615","810214":"KEI KECIL BARAT -- 97615","810215":"MANYEUW -- 97611","810216":"HOAT SORBAY -- 97611","810217":"KEI BESAR UTARA BARAT -- 97661","810218":"KEI BESAR SELATAN BARAT -- 97661","810219":"KEI KECIL TIMUR SELATAN -- 97615","810301":"TANIMBAR SELATAN -- 97464","810302":"SELARU -- 97453","810303":"WER TAMRIAN -- 97464","810304":"WER MAKTIAN -- 97464","810305":"TANIMBAR UTARA -- 97463","810306":"YARU -- 97463","810307":"WUAR LABOBAR -- 97463","810308":"KORMOMOLIN -- 97463","810309":"NIRUNMAS -- 97463","810318":"MOLU MARU -- 97463","810401":"NAMLEA -- 97571","810402":"AIR BUAYA -- 97572","810403":"WAEAPO -- 97574","810406":"WAPLAU -- 97571","810410":"BATABUAL -- 97574","810411":"LOLONG GUBA -- 97574","810412":"WAELATA -- 97574","810413":"FENA LEISELA -- 97572","810414":"TELUK KAIELY -- 97574","810415":"LILIALY -- 97571","810501":"BULA -- 97554","810502":"SERAM TIMUR -- 97594","810503":"WERINAMA -- 97554","810504":"PULAU GOROM -- -","810505":"WAKATE -- 97595","810506":"TUTUK TOLU -- 97594","810507":"SIWALALAT -- 97554","810508":"KILMURY -- 97594","810509":"PULAU PANJANG -- 97599","810510":"TEOR -- 97597","810511":"GOROM TIMUR -- 97596","810512":"BULA BARAT -- 97554","810513":"KIAN DARAT -- 97594","810514":"SIRITAUN WIDA TIMUR -- 97598","810515":"TELUK WARU -- 97554","810601":"KAIRATU -- 97566","810602":"SERAM BARAT -- 97562","810603":"TANIWEL -- 97561","810604":"HUAMUAL BELAKANG -- 97567","810605":"AMALATU -- 97566","810606":"INAMOSOL -- 97566","810607":"KAIRATU BARAT -- 97566","810608":"HUAMUAL -- 97567","810609":"KEPULAUAN MANIPA -- 97567","810610":"TANIWEL TIMUR -- 97561","810611":"ELPAPUTIH -- 97566","810701":"PULAU-PULAU ARU -- 97662","810702":"ARU SELATAN -- 97666","810703":"ARU TENGAH -- 97665","810704":"ARU UTARA -- 97663","810705":"ARU UTARA TIMUR BATULEY -- 97663","810706":"SIR-SIR -- 97664","810707":"ARU TENGAH TIMUR -- 97665","810708":"ARU TENGAH SELATAN -- 97665","810709":"ARU SELATAN TIMUR -- 97666","810710":"ARU SELATAN UTARA -- 97668","810801":"MOA LAKOR -- 97454","810802":"DAMER -- 97128","810803":"MNDONA HIERA -- -","810804":"PULAU-PULAU BABAR -- 97652","810805":"PULAU-PULAU BABAR TIMUR -- 97652","810806":"WETAR -- 97454","810807":"PULAU-PULAU TERSELATAN -- -","810808":"PULAU LETI -- -","810809":"PULAU MASELA -- 97652","810810":"DAWELOR DAWERA -- 97652","810811":"PULAU WETANG -- 97452","810812":"PULAU LAKOR -- 97454","810813":"WETAR UTARA -- 97454","810814":"WETAR BARAT -- 97454","810815":"WETAR TIMUR -- 97454","810816":"KEPULAUAN ROMANG -- 97454","810817":"KISAR UTARA -- 97454","810901":"NAMROLE -- 97573","810902":"WAESAMA -- 97574","810903":"AMBALAU -- 97512","810904":"KEPALA MADAN -- 97572","810905":"LEKSULA -- 97573","810906":"FENA FAFAN -- 97573","817101":"NUSANIWE -- 97117","817102":"SIRIMAU -- 97127","817103":"BAGUALA -- 97231","817104":"TELUK AMBON -- 97234","817105":"LEITIMUR SELATAN -- 97129","817201":"PULAU DULLAH UTARA -- 97611","817202":"PULAU DULLAH SELATAN -- 97611","817203":"TAYANDO TAM -- 97611","817204":"PULAU-PULAU KUR -- 97652","817205":"KUR SELATAN -- 97652","820101":"JAILOLO -- 97752","820102":"LOLODA -- 97755","820103":"IBU -- 97754","820104":"SAHU -- 97753","820105":"JAILOLO SELATAN -- 97752","820107":"IBU UTARA -- 97754","820108":"IBU SELATAN -- 97754","820109":"SAHU TIMUR -- 97753","820201":"WEDA -- 97853","820202":"PATANI -- 97854","820203":"PULAU GEBE -- 97854","820204":"WEDA UTARA -- 97853","820205":"WEDA SELATAN -- 97853","820206":"PATANI UTARA -- 97854","820207":"WEDA TENGAH -- 97853","820208":"PATANI BARAT -- 97854","820304":"GALELA -- 97761","820305":"TOBELO -- 97762","820306":"TOBELO SELATAN -- 97762","820307":"KAO -- 97752","820308":"MALIFUT -- 97764","820309":"LOLODA UTARA -- 97755","820310":"TOBELO UTARA -- 97762","820311":"TOBELO TENGAH -- 97762","820312":"TOBELO TIMUR -- 97762","820313":"TOBELO BARAT -- 97762","820314":"GALELA BARAT -- 97761","820315":"GALELA UTARA -- 97761","820316":"GALELA SELATAN -- 97761","820319":"LOLODA KEPULAUAN -- 97755","820320":"KAO UTARA -- 97764","820321":"KAO BARAT -- 97764","820322":"KAO TELUK -- 97752","820401":"PULAU MAKIAN -- 97756","820402":"KAYOA -- 97781","820403":"GANE TIMUR -- 97783","820404":"GANE BARAT -- 97782","820405":"OBI SELATAN -- 97792","820406":"OBI -- 97792","820407":"BACAN TIMUR -- 97791","820408":"BACAN -- 97791","820409":"BACAN BARAT -- 97791","820410":"MAKIAN BARAT -- 97756","820411":"KAYOA BARAT -- 97781","820412":"KAYOA SELATAN -- 97781","820413":"KAYOA UTARA -- 97781","820414":"BACAN BARAT UTARA -- 97791","820415":"KASIRUTA BARAT -- 97791","820416":"KASIRUTA TIMUR -- 97791","820417":"BACAN SELATAN -- 97791","820418":"KEPULAUAN BOTANGLOMANG -- 97791","820419":"MANDIOLI SELATAN -- 97791","820420":"MANDIOLI UTARA -- 97791","820421":"BACAN TIMUR SELATAN -- 97791","820422":"BACAN TIMUR TENGAH -- 97791","820423":"GANE BARAT SELATAN -- 97782","820424":"GANE BARAT UTARA -- 97782","820425":"KEPULAUAN JORONGA -- 97782","820426":"GANE TIMUR SELATAN -- 97783","820427":"GANE TIMUR TENGAH -- 97783","820428":"OBI BARAT -- 97792","820429":"OBI TIMUR -- 97792","820430":"OBI UTARA -- 97792","820501":"MANGOLI TIMUR -- 97793","820502":"SANANA -- 97795","820503":"SULABESI BARAT -- 97795","820506":"MANGOLI BARAT -- 97792","820507":"SULABESI TENGAH -- 97795","820508":"SULABESI TIMUR -- 97795","820509":"SULABESI SELATAN -- 97795","820510":"MANGOLI UTARA TIMUR -- 97793","820511":"MANGOLI TENGAH -- 97793","820512":"MANGOLI SELATAN -- 97793","820513":"MANGOLI UTARA -- 97793","820518":"SANANA UTARA -- 97795","820601":"WASILE -- 97863","820602":"MABA -- 97862","820603":"MABA SELATAN -- 97862","820604":"WASILE SELATAN -- 97863","820605":"WASILE TENGAH -- 97863","820606":"WASILE UTARA -- 97863","820607":"WASILE TIMUR -- 97863","820608":"MABA TENGAH -- 97862","820609":"MABA UTARA -- 97862","820610":"KOTA MABA -- 97862","820701":"MOROTAI SELATAN -- 97771","820702":"MOROTAI SELATAN BARAT -- 97771","820703":"MOROTAI JAYA -- 97772","820704":"MOROTAI UTARA -- 97772","820705":"MOROTAI TIMUR -- 97772","820801":"TALIABU BARAT -- 97794","820802":"TALIABU BARAT LAUT -- 97794","820803":"LEDE -- 97793","820804":"TALIABU UTARA -- 97794","820805":"TALIABU TIMUR -- 97793","820806":"TALIABU TIMUR SELATAN -- 97793","820807":"TALIABU SELATAN -- 97794","820808":"TABONA -- -","827101":"PULAU TERNATE -- 97751","827102":"KOTA TERNATE SELATAN -- -","827103":"KOTA TERNATE UTARA -- -","827104":"PULAU MOTI -- 97751","827105":"PULAU BATANG DUA -- 97751","827106":"KOTA TERNATE TENGAH -- -","827107":"PULAU HIRI -- 97751","827201":"TIDORE -- 97813","827202":"OBA UTARA -- 97852","827203":"OBA -- 97852","827204":"TIDORE SELATAN -- 97813","827205":"TIDORE UTARA -- 97813","827206":"OBA TENGAH -- 97852","827207":"OBA SELATAN -- 97852","827208":"TIDORE TIMUR -- 97813","910101":"MERAUKE -- 99616","910102":"MUTING -- 99652","910103":"OKABA -- 99654","910104":"KIMAAM -- 99655","910105":"SEMANGGA -- 99651","910106":"TANAH MIRING -- 99651","910107":"JAGEBOB -- 99656","910108":"SOTA -- 99656","910109":"ULILIN -- 99652","910110":"ELIKOBAL -- -","910111":"KURIK -- 99656","910112":"NAUKENJERAI -- 99616","910113":"ANIMHA -- 99656","910114":"MALIND -- 99656","910115":"TUBANG -- 99654","910116":"NGGUTI -- 99654","910117":"KAPTEL -- 99654","910118":"TABONJI -- 99655","910119":"WAAN -- 99655","910120":"ILWAYAB -- -","910201":"WAMENA -- 99511","910203":"KURULU -- 99552","910204":"ASOLOGAIMA -- 99554","910212":"HUBIKOSI -- 99566","910215":"BOLAKME -- 99557","910225":"WALELAGAMA -- 99511","910227":"MUSATFAK -- 99554","910228":"WOLO -- 99557","910229":"ASOLOKOBAL -- 99511","910234":"PELEBAGA -- 99566","910235":"YALENGGA -- 99557","910240":"TRIKORA -- 99511","910241":"NAPUA -- 99511","910242":"WALAIK -- 99511","910243":"WOUMA -- 99511","910244":"HUBIKIAK -- 99566","910245":"IBELE -- 99566","910246":"TAELAREK -- 99566","910247":"ITLAY HISAGE -- 99511","910248":"SIEPKOSI -- 99511","910249":"USILIMO -- 99552","910250":"WITA WAYA -- 99552","910251":"LIBAREK -- 99552","910252":"WADANGKU -- 99552","910253":"PISUGI -- 99552","910254":"KORAGI -- 99557","910255":"TAGIME -- 99561","910256":"MOLAGALOME -- 99557","910257":"TAGINERI -- 99561","910258":"SILO KARNO DOGA -- 99554","910259":"PIRAMID -- 99554","910260":"MULIAMA -- 99554","910261":"BUGI -- 99557","910262":"BPIRI -- 99557","910263":"WELESI -- 99511","910264":"ASOTIPO -- 99511","910265":"MAIMA -- 99511","910266":"POPUGOBA -- 99511","910267":"WAME -- 99511","910268":"WESAPUT -- 99511","910301":"SENTANI -- 99359","910302":"SENTANI TIMUR -- 99359","910303":"DEPAPRE -- 99353","910304":"SENTANI BARAT -- 99358","910305":"KEMTUK -- 99357","910306":"KEMTUK GRESI -- 99357","910307":"NIMBORAN -- 99361","910308":"NIMBOKRANG -- 99362","910309":"UNURUM GUAY -- 99356","910310":"DEMTA -- 99354","910311":"KAUREH -- 99364","910312":"EBUNGFA -- 99352","910313":"WAIBU -- 99358","910314":"NAMBLUONG -- 99361","910315":"YAPSI -- 99364","910316":"AIRU -- 99364","910317":"RAVENI RARA -- 99353","910318":"GRESI SELATAN -- 99357","910319":"YOKARI -- 99354","910401":"NABIRE -- 98856","910402":"NAPAN -- 98861","910403":"YAUR -- 98852","910406":"UWAPA -- 98853","910407":"WANGGAR -- 98856","910410":"SIRIWO -- 98854","910411":"MAKIMI -- 98861","910412":"TELUK UMAR -- 98852","910416":"TELUK KIMI -- 98818","910417":"YARO -- 98853","910421":"WAPOGA -- 98261","910422":"NABIRE BARAT -- 98856","910423":"MOORA -- 98861","910424":"DIPA -- 98768","910425":"MENOU -- 98853","910501":"YAPEN SELATAN -- 98214","910502":"YAPEN BARAT -- 98253","910503":"YAPEN TIMUR -- 98252","910504":"ANGKAISERA -- 98255","910505":"POOM -- 98254","910506":"KOSIWO -- 98215","910507":"YAPEN UTARA -- 98252","910508":"RAIMBAWI -- 98252","910509":"TELUK AMPIMOI -- 98252","910510":"KEPULAUAN AMBAI -- 98255","910511":"WONAWA -- 98253","910512":"WINDESI -- 98254","910513":"PULAU KURUDU -- 98252","910514":"PULAU YERUI -- 98253","910601":"BIAK KOTA -- 98118","910602":"BIAK UTARA -- 98153","910603":"BIAK TIMUR -- 98152","910604":"NUMFOR BARAT -- 98172","910605":"NUMFOR TIMUR -- 98171","910608":"BIAK BARAT -- 98154","910609":"WARSA -- 98157","910610":"PADAIDO -- 98158","910611":"YENDIDORI -- 98155","910612":"SAMOFA -- 98156","910613":"YAWOSI -- 98153","910614":"ANDEY -- 98153","910615":"SWANDIWE -- 98154","910616":"BRUYADORI -- 98171","910617":"ORKERI -- 98172","910618":"POIRU -- 98171","910619":"AIMANDO PADAIDO -- 98158","910620":"ORIDEK -- 98152","910621":"BONDIFUAR -- 98157","910701":"MULIA -- 98911","910703":"ILU -- 98916","910706":"FAWI -- 98917","910707":"MEWOLUK -- 98918","910708":"YAMO -- 98913","910710":"NUME -- -","910711":"TORERE -- 98914","910712":"TINGGINAMBUT -- 98912","910717":"PAGALEME -- -","910718":"GURAGE -- -","910719":"IRIMULI -- -","910720":"MUARA -- 99351","910721":"ILAMBURAWI -- -","910722":"YAMBI -- -","910723":"LUMO -- -","910724":"MOLANIKIME -- -","910725":"DOKOME -- -","910726":"KALOME -- -","910727":"WANWI -- -","910728":"YAMONERI -- -","910729":"WAEGI -- -","910730":"NIOGA -- -","910731":"GUBUME -- -","910732":"TAGANOMBAK -- -","910733":"DAGAI -- -","910734":"KIYAGE -- -","910801":"PANIAI TIMUR -- 98711","910802":"PANIAI BARAT -- 98763","910804":"ARADIDE -- 98766","910807":"BOGABAIDA -- -","910809":"BIBIDA -- 98782","910812":"DUMADAMA -- 98782","910813":"SIRIWO -- 98854","910819":"KEBO -- 98715","910820":"YATAMO -- 98725","910821":"EKADIDE -- 98766","910901":"MIMIKA BARU -- 99910","910902":"AGIMUGA -- 99964","910903":"MIMIKA TIMUR -- 99972","910904":"MIMIKA BARAT -- 99974","910905":"JITA -- 99965","910906":"JILA -- 99966","910907":"MIMIKA TIMUR JAUH -- 99971","910908":"MIMIKA TENGAH -- -","910909":"KUALA KENCANA -- 99968","910910":"TEMBAGAPURA -- 99967","910911":"MIMIKA BARAT JAUH -- 99974","910912":"MIMIKA BARAT TENGAH -- 99973","910913":"KWAMKI NARAMA -- -","910914":"HOYA -- -","910916":"WANIA -- -","910917":"AMAR -- -","910918":"ALAMA -- 99564","911001":"SARMI -- 99373","911002":"TOR ATAS -- 99372","911003":"PANTAI BARAT -- 99374","911004":"PANTAI TIMUR -- 99371","911005":"BONGGO -- 99355","911009":"APAWER HULU -- 99374","911012":"SARMI SELATAN -- 99373","911013":"SARMI TIMUR -- 99373","911014":"PANTAI TIMUR BAGIAN BARAT -- -","911015":"BONGGO TIMUR -- 99355","911101":"WARIS -- 99467","911102":"ARSO -- 99468","911103":"SENGGI -- 99465","911104":"WEB -- 99466","911105":"SKANTO -- 99469","911106":"ARSO TIMUR -- 99468","911107":"TOWE -- 99466","911201":"OKSIBIL -- 99573","911202":"KIWIROK -- 99574","911203":"OKBIBAB -- 99572","911204":"IWUR -- 99575","911205":"BATOM -- 99576","911206":"BORME -- 99577","911207":"KIWIROK TIMUR -- 99574","911208":"ABOY -- 99572","911209":"PEPERA -- 99573","911210":"BIME -- 99577","911211":"ALEMSOM -- 99573","911212":"OKBAPE -- 99573","911213":"KALOMDOL -- 99573","911214":"OKSOP -- 99573","911215":"SERAMBAKON -- 99573","911216":"OK AOM -- 99573","911217":"KAWOR -- 99575","911218":"AWINBON -- 99575","911219":"TARUP -- 99575","911220":"OKHIKA -- 99574","911221":"OKSAMOL -- 99574","911222":"OKLIP -- 99574","911223":"OKBEMTAU -- 99574","911224":"OKSEBANG -- 99574","911225":"OKBAB -- 99572","911226":"BATANI -- 99576","911227":"WEIME -- 99576","911228":"MURKIM -- 99576","911229":"MOFINOP -- 99576","911230":"JETFA -- 99572","911231":"TEIRAPLU -- 99572","911232":"EIPUMEK -- 99577","911233":"PAMEK -- 99577","911234":"NONGME -- 99576","911301":"KURIMA -- 99571","911302":"ANGGRUK -- 99582","911303":"NINIA -- 99578","911306":"SILIMO -- 99552","911307":"SAMENAGE -- 99571","911308":"NALCA -- 99582","911309":"DEKAI -- 99571","911310":"OBIO -- 99571","911311":"SURU SURU -- 99571","911312":"WUSAMA -- -","911313":"AMUMA -- 99571","911314":"MUSAIK -- 99571","911315":"PASEMA -- 99571","911316":"HOGIO -- 99571","911317":"MUGI -- 99564","911318":"SOBA -- 99578","911319":"WERIMA -- 99571","911320":"TANGMA -- 99571","911321":"UKHA -- 99571","911322":"PANGGEMA -- 99582","911323":"KOSAREK -- 99582","911324":"NIPSAN -- 99582","911325":"UBAHAK -- 99582","911326":"PRONGGOLI -- 99582","911327":"WALMA -- 99582","911328":"YAHULIAMBUT -- 99578","911329":"HEREAPINI -- 99582","911330":"UBALIHI -- 99582","911331":"TALAMBO -- 99582","911332":"PULDAMA -- 99582","911333":"ENDOMEN -- 99582","911334":"KONA -- 99571","911335":"DIRWEMNA -- 99582","911336":"HOLUON -- 99578","911337":"LOLAT -- 99578","911338":"SOLOIKMA -- 99578","911339":"SELA -- 99373","911340":"KORUPUN -- 99578","911341":"LANGDA -- 99578","911342":"BOMELA -- 99578","911343":"SUNTAMON -- 99578","911344":"SEREDELA -- 99571","911345":"SOBAHAM -- 99578","911346":"KABIANGGAMA -- 99578","911347":"KWELEMDUA -- 99578","911348":"KWIKMA -- 99578","911349":"HILIPUK -- 99578","911350":"DURAM -- 99582","911351":"YOGOSEM -- 99571","911352":"KAYO -- 99571","911353":"SUMO -- 99571","911401":"KARUBAGA -- 99562","911402":"BOKONDINI -- 99561","911403":"KANGGIME -- 99568","911404":"KEMBU -- 99569","911405":"GOYAGE -- 99562","911406":"WUNIM -- -","911407":"WINA -- 99569","911408":"UMAGI -- 99569","911409":"PANAGA -- 99569","911410":"WONIKI -- 99568","911411":"KUBU -- 99562","911412":"KONDA/ KONDAGA -- -","911413":"NELAWI -- 99562","911414":"KUARI -- 99562","911415":"BOKONERI -- 99561","911416":"BEWANI -- 99561","911418":"NABUNAGE -- 99568","911419":"GILUBANDU -- 99568","911420":"NUNGGAWI -- 99568","911421":"GUNDAGI -- 99569","911422":"NUMBA -- 99562","911423":"TIMORI -- 99569","911424":"DUNDU -- 99569","911425":"GEYA -- 99562","911426":"EGIAM -- 99569","911427":"POGANERI -- 99569","911428":"KAMBONERI -- 99561","911429":"AIRGARAM -- 99562","911430":"WARI/TAIYEVE II -- -","911431":"DOW -- 99569","911432":"TAGINERI -- 99561","911433":"YUNERI -- 99562","911434":"WAKUWO -- 99568","911435":"GIKA -- 99569","911436":"TELENGGEME -- 99569","911437":"ANAWI -- 99562","911438":"WENAM -- 99562","911439":"WUGI -- 99562","911440":"DANIME -- 99561","911441":"TAGIME -- 99561","911442":"KAI -- 99562","911443":"AWEKU -- 99568","911444":"BOGONUK -- 99568","911445":"LI ANOGOMMA -- 99562","911446":"BIUK -- 99562","911447":"YUKO -- 99569","911501":"WAROPEN BAWAH -- 98261","911503":"MASIREI -- 98263","911507":"RISEI SAYATI -- 98263","911508":"UREI FAISEI -- -","911509":"INGGERUS -- 98261","911510":"KIRIHI -- 98263","911514":"WONTI -- -","911515":"SOYOI MAMBAI -- -","911601":"MANDOBO -- 99663","911602":"MINDIPTANA -- 99662","911603":"WAROPKO -- 99664","911604":"KOUH -- 99661","911605":"JAIR -- 99661","911606":"BOMAKIA -- 99663","911607":"KOMBUT -- 99662","911608":"INIYANDIT -- 99662","911609":"ARIMOP -- 99663","911610":"FOFI -- 99663","911611":"AMBATKWI -- 99664","911612":"MANGGELUM -- 99665","911613":"FIRIWAGE -- 99665","911614":"YANIRUMA -- 99664","911615":"SUBUR -- 99661","911616":"KOMBAY -- 99664","911617":"NINATI -- 99664","911618":"SESNUK -- 99662","911619":"KI -- 99663","911620":"KAWAGIT -- 99665","911701":"OBAA -- 99871","911702":"MAMBIOMAN BAPAI -- -","911703":"CITAK-MITAK -- -","911704":"EDERA -- 99853","911705":"HAJU -- 99881","911706":"ASSUE -- 99874","911707":"KAIBAR -- 99875","911708":"PASSUE -- 99871","911709":"MINYAMUR -- 99872","911710":"VENAHA -- 99853","911711":"SYAHCAME -- 99853","911712":"YAKOMI -- 99853","911713":"BAMGI -- 99853","911714":"PASSUE BAWAH -- 99875","911715":"TI ZAIN -- 99875","911801":"AGATS -- 99777","911802":"ATSJ -- 99776","911803":"SAWA ERMA -- 99778","911804":"AKAT -- 99779","911805":"FAYIT -- 99782","911806":"PANTAI KASUARI -- 99773","911807":"SUATOR -- 99766","911808":"SURU-SURU -- 99778","911809":"KOLF BRAZA -- 99766","911810":"UNIR SIRAU -- 99778","911811":"JOERAT -- 99778","911812":"PULAU TIGA -- 99778","911813":"JETSY -- 99777","911814":"DER KOUMUR -- 99773","911815":"KOPAY -- 99773","911816":"SAFAN -- 99773","911817":"SIRETS -- 99776","911818":"AYIP -- 99776","911819":"BETCBAMU -- 99776","911901":"SUPIORI SELATAN -- 98161","911902":"SUPIORI UTARA -- 98162","911903":"SUPIORI TIMUR -- 98161","911904":"KEPULAUAN ARURI -- 98161","911905":"SUPIORI BARAT -- 98162","912001":"MAMBERAMO TENGAH -- 99376","912002":"MAMBERAMO HULU -- 99377","912003":"RUFAER -- 99377","912004":"MAMBERAMO TENGAH TIMUR -- 99376","912005":"MAMBERAMO HILIR -- 99375","912006":"WAROPEN ATAS -- 98262","912007":"BENUKI -- 98262","912008":"SAWAI -- 98262","912101":"KOBAGMA -- -","912102":"KELILA -- 99553","912103":"ERAGAYAM -- 99553","912104":"MEGAMBILIS -- 99558","912105":"ILUGWA -- 99557","912201":"ELELIM -- 99584","912202":"APALAPSILI -- 99586","912203":"ABENAHO -- 99587","912204":"BENAWA -- 99583","912205":"WELAREK -- 99585","912301":"TIOM -- 99563","912302":"PIRIME -- 99567","912303":"MAKKI -- 99555","912304":"GAMELIA -- 99556","912305":"DIMBA -- 99567","912306":"MELAGINERI -- -","912307":"BALINGGA -- 99567","912308":"TIOMNERI -- 99563","912309":"KUYAWAGE -- 99563","912310":"POGA -- 99556","912311":"NINAME -- -","912312":"NOGI -- -","912313":"YIGINUA -- -","912314":"TIOM OLLO -- -","912315":"YUGUNGWI -- -","912316":"MOKONI -- -","912317":"WEREKA -- -","912318":"MILIMBO -- -","912319":"WIRINGGAMBUT -- -","912320":"GOLLO -- -","912321":"AWINA -- -","912322":"AYUMNATI -- -","912323":"WANO BARAT -- -","912324":"GOA BALIM -- -","912325":"BRUWA -- -","912326":"BALINGGA BARAT -- -","912327":"GUPURA -- -","912328":"KOLAWA -- -","912329":"GELOK BEAM -- -","912330":"KULY LANNY -- -","912331":"LANNYNA -- -","912332":"KARU -- 99562","912333":"YILUK -- -","912334":"GUNA -- -","912335":"KELULOME -- -","912336":"NIKOGWE -- -","912337":"MUARA -- 99351","912338":"BUGUK GONA -- -","912339":"MELAGI -- -","912401":"KENYAM -- 99565","912402":"MAPENDUMA -- 99564","912403":"YIGI -- 99564","912404":"WOSAK -- 99565","912405":"GESELMA -- 99564","912406":"MUGI -- 99564","912407":"MBUWA -- -","912408":"GEAREK -- 99565","912409":"KOROPTAK -- 99564","912410":"KEGAYEM -- 99564","912411":"PARO -- 99564","912412":"MEBAROK -- 99564","912413":"YENGGELO -- 99564","912414":"KILMID -- 99564","912415":"ALAMA -- 99564","912416":"YAL -- 99557","912417":"MAM -- 99872","912418":"DAL -- 99571","912419":"NIRKURI -- 99564","912420":"INIKGAL -- 99564","912421":"INIYE -- 99564","912422":"MBULMU YALMA -- 99564","912423":"MBUA TENGAH -- 99565","912424":"EMBETPEN -- 99565","912425":"KORA -- 99511","912426":"WUSI -- 99565","912427":"PIJA -- 99565","912428":"MOBA -- 99565","912429":"WUTPAGA -- 99564","912430":"NENGGEAGIN -- 99564","912431":"KREPKURI -- 99565","912432":"PASIR PUTIH -- 99565","912501":"ILAGA -- 98972","912502":"WANGBE -- 98971","912503":"BEOGA -- 98971","912504":"DOUFO -- -","912505":"POGOMA -- 98973","912506":"SINAK -- 98973","912507":"AGANDUGUME -- -","912508":"GOME -- 98972","912601":"KAMU -- 98863","912602":"MAPIA -- 98854","912603":"PIYAIYE -- 98857","912604":"KAMU UTARA -- 98863","912605":"SUKIKAI SELATAN -- 98857","912606":"MAPIA BARAT -- 98854","912607":"KAMU SELATAN -- 98862","912608":"KAMU TIMUR -- 98863","912609":"MAPIA TENGAH -- 98854","912610":"DOGIYAI -- 98862","912701":"SUGAPA -- 98768","912702":"HOMEYO -- 98767","912703":"WANDAI -- 98784","912704":"BIANDOGA -- 98784","912705":"AGISIGA -- 98783","912706":"HITADIPA -- 98768","912707":"UGIMBA -- -","912708":"TOMOSIGA -- -","912801":"TIGI -- 98764","912802":"TIGI TIMUR -- 98781","912803":"BOWOBADO -- 98781","912804":"TIGI BARAT -- 98764","912805":"KAPIRAYA -- 98727","917101":"JAYAPURA UTARA -- 99113","917102":"JAYAPURA SELATAN -- 99223","917103":"ABEPURA -- 99351","917104":"MUARA TAMI -- 99351","917105":"HERAM -- 99351","920101":"MAKBON -- 98471","920104":"BERAUR -- 98453","920105":"SALAWATI -- 98452","920106":"SEGET -- 98452","920107":"AIMAS -- 98457","920108":"KLAMONO -- 98456","920110":"SAYOSA -- 98471","920112":"SEGUN -- 98452","920113":"MAYAMUK -- 98451","920114":"SALAWATI SELATAN -- 98452","920117":"KLABOT -- 98453","920118":"KLAWAK -- 98453","920120":"MAUDUS -- 98472","920139":"MARIAT -- 98457","920140":"KLAILI -- -","920141":"KLASO -- 98472","920142":"MOISEGEN -- 98451","920203":"WARMARE -- 98352","920204":"PRAFI -- 98356","920205":"MASNI -- 98357","920212":"MANOKWARI BARAT -- 98314","920213":"MANOKWARI TIMUR -- 98311","920214":"MANOKWARI UTARA -- 98315","920215":"MANOKWARI SELATAN -- 98315","920217":"TANAH RUBUH -- 98315","920221":"SIDEY -- 98357","920301":"FAK-FAK -- -","920302":"FAK-FAK BARAT -- -","920303":"FAK-FAK TIMUR -- -","920304":"KOKAS -- 98652","920305":"FAK-FAK TENGAH -- -","920306":"KARAS -- 98662","920307":"BOMBERAY -- 98662","920308":"KRAMONGMONGGA -- 98652","920309":"TELUK PATIPI -- 98661","920310":"PARIWARI -- -","920311":"WARTUTIN -- -","920312":"FAKFAK TIMUR TENGAH -- -","920313":"ARGUNI -- 98653","920314":"MBAHAMDANDARA -- -","920315":"KAYAUNI -- -","920316":"FURWAGI -- -","920317":"TOMAGE -- -","920401":"TEMINABUAN -- 98454","920404":"INANWATAN -- 98455","920406":"SAWIAT -- 98456","920409":"KOKODA -- 98455","920410":"MOSWAREN -- 98454","920411":"SEREMUK -- 98454","920412":"WAYER -- 98454","920414":"KAIS -- 98455","920415":"KONDA -- 98454","920420":"MATEMANI -- 98455","920421":"KOKODA UTARA -- 98455","920422":"SAIFI -- 98454","920424":"FOKOUR -- 98456","920501":"MISOOL (MISOOL UTARA) -- 98483","920502":"WAIGEO UTARA -- 98481","920503":"WAIGEO SELATAN -- 98482","920504":"SALAWATI UTARA -- 98484","920505":"KEPULAUAN AYAU -- 98481","920506":"MISOOL TIMUR -- 98483","920507":"WAIGEO BARAT -- 98481","920508":"WAIGEO TIMUR -- 98482","920509":"TELUK MAYALIBIT -- 98482","920510":"KOFIAU -- 98483","920511":"MEOS MANSAR -- 98482","920513":"MISOOL SELATAN -- 98483","920514":"WARWARBOMI -- -","920515":"WAIGEO BARAT KEPULAUAN -- 98481","920516":"MISOOL BARAT -- 98483","920517":"KEPULAUAN SEMBILAN -- 98483","920518":"KOTA WAISAI -- 98482","920519":"TIPLOL MAYALIBIT -- 98482","920520":"BATANTA UTARA -- 98484","920521":"SALAWATI BARAT -- 98484","920522":"SALAWATI TENGAH -- 98484","920523":"SUPNIN -- 98481","920524":"AYAU -- 98481","920525":"BATANTA SELATAN -- 98484","920601":"BINTUNI -- 98364","920602":"MERDEY -- 98373","920603":"BABO -- 98363","920604":"ARANDAY -- 98365","920605":"MOSKONA SELATAN -- 98365","920606":"MOSKONA UTARA -- 98373","920607":"WAMESA -- 98361","920608":"FAFURWAR -- 98363","920609":"TEMBUNI -- 98364","920610":"KURI -- 98362","920611":"MANIMERI -- 98364","920612":"TUHIBA -- 98364","920613":"DATARAN BEIMES -- 98364","920614":"SUMURI -- 98363","920615":"KAITARO -- 98363","920616":"AROBA -- 98363","920617":"MASYETA -- 98373","920618":"BISCOOP -- 98373","920619":"TOMU -- 98365","920620":"KAMUNDAN -- 98365","920621":"WERIAGAR -- 98365","920622":"MOSKONA BARAT -- 98365","920623":"MEYADO -- -","920624":"MOSKONA TIMUR -- 98373","920701":"WASIOR -- 98362","920702":"WINDESI -- 98361","920703":"TELUK DUAIRI -- 98362","920704":"WONDIBOY -- 98362","920705":"WAMESA -- 98361","920706":"RUMBERPON -- 98361","920707":"NAIKERE -- 98362","920708":"RASIEI -- 98362","920709":"KURI WAMESA -- 98362","920710":"ROON -- 98362","920711":"ROSWAR -- 98361","920712":"NIKIWAR -- 98361","920713":"SOUG JAYA -- 98361","920801":"KAIMANA -- 98654","920802":"BURUWAY -- 98656","920803":"TELUK ARGUNI ATAS -- 98653","920804":"TELUK ETNA -- 98655","920805":"KAMBRAU -- -","920806":"TELUK ARGUNI BAWAH -- 98653","920807":"YAMOR -- 98655","920901":"FEF -- 98473","920902":"MIYAH -- 98473","920903":"YEMBUN -- 98474","920904":"KWOOR -- 98473","920905":"SAUSAPOR -- 98473","920906":"ABUN -- 98473","920907":"SYUJAK -- 98473","920913":"BIKAR -- -","920914":"BAMUSBAMA -- -","920916":"MIYAH SELATAN -- -","920917":"IRERES -- -","920918":"TOBOUW -- -","920919":"WILHEM ROUMBOUTS -- -","920920":"TINGGOUW -- -","920921":"KWESEFO -- -","920922":"MAWABUAN -- -","920923":"KEBAR TIMUR -- -","920924":"KEBAR SELATAN -- -","920925":"MANEKAR -- -","920926":"MPUR -- -","920927":"AMBERBAKEN BARAT -- -","920928":"KASI -- -","920929":"SELEMKAI -- -","921001":"AIFAT -- 98463","921002":"AIFAT UTARA -- 98463","921003":"AIFAT TIMUR -- 98463","921004":"AIFAT SELATAN -- 98463","921005":"AITINYO BARAT -- 98462","921006":"AITINYO -- 98462","921007":"AITINYO UTARA -- 98462","921008":"AYAMARU -- 98461","921009":"AYAMARU UTARA -- 98461","921010":"AYAMARU TIMUR -- 98461","921011":"MARE -- 98461","921101":"RANSIKI -- 98355","921102":"ORANSBARI -- 98353","921103":"NENEY -- 98355","921104":"DATARAN ISIM -- 98359","921105":"MOMI WAREN -- 98355","921106":"TAHOTA -- 98355","921201":"ANGGI -- 98354","921202":"ANGGI GIDA -- 98354","921203":"MEMBEY -- 98354","921204":"SURUREY -- 98359","921205":"DIDOHU -- 98359","921206":"TAIGE -- 98354","921207":"CATUBOUW -- 98358","921208":"TESTEGA -- 98357","921209":"MINYAMBAOUW -- -","921210":"HINGK -- 98357","927101":"SORONG -- 98413","927102":"SORONG TIMUR -- 98418","927103":"SORONG BARAT -- 98412","927104":"SORONG KEPULAUAN -- 98413","927105":"SORONG UTARA -- 98416","927106":"SORONG MANOI -- 98414","927107":"SORONG KOTA -- -","927108":"KLAURUNG -- -","927109":"MALAIMSIMSA -- -","927110":"MALADUM MES -- -"}}` + +var jsonData = []byte(Wilayah) + +type Hasil struct { + NIK string `json:"nik"` + Sex string `json:"jenis_kelamin"` + Ttl string `json:"tanggal_lahir"` + Provinsi string `json:"provinsi"` + Kabkot string `json:"kabupaten/kota"` + Kecamatan string `json:"kecamatan"` + KodPos string `json:"kodepos"` + Uniq string `json:"uniqcode"` + Status string `json:"status"` + Pesan string `json:"pesan"` +} + +func FetchNIKData(nik string) Hasil { + var dt map[string]map[string]interface{} + json.Unmarshal(jsonData, &dt) + + hasil := Hasil{} + + if len(nik) == 16 && nil != dt["provinsi"][nik[0:2]] && nil != dt["kabkot"][nik[0:4]] && nil != dt["kecamatan"][nik[0:6]] { + var ( + thnNIK = nik[10:12] + t = nik[6:8] + KecKodPos = strings.Split(fmt.Sprintf("%v", dt["kecamatan"][nik[0:6]]), " -- ") + kecamatan = KecKodPos[0] + kodepos = KecKodPos[1] + sex = "LAKI-LAKI" + tgllahir string + bulanlahir string + thnlahir string + ) + if t > "40" { + sex = "PEREMPUAN" + } + // t, _ = strconv.ParseInt(, 10, 64) + x, _ := strconv.ParseInt(t, 10, 64) + + // tgllahir = strconv.FormatInt(x, 10) + if x > 40 { + tgl := x - 40 + tgllahir = strconv.FormatInt(tgl, 10) + if len(strconv.FormatInt(tgl, 10)) == 1 { + tgllahir = fmt.Sprintf("0" + strconv.FormatInt(tgl, 10)) + // log.Println("0" + strconv.FormatInt(tgl, 10)) + } + } else { + if len(strconv.FormatInt(x, 10)) == 1 { + tgllahir = fmt.Sprintf("0" + strconv.FormatInt(x, 10)) + // log.Println("0" + strconv.FormatInt(x, 10)) + } else { + tgllahir = strconv.FormatInt(x, 10) + } + } + tahunNIK, _ := strconv.ParseInt(thnNIK, 10, 64) + year, _, _ := time.Now().Date() + year = year % 1e2 + if tahunNIK <= int64(year) { + tahunNIK += 2000 + thnlahir = fmt.Sprint(tahunNIK) + } else { + tahunNIK += 1900 + thnlahir = fmt.Sprint(tahunNIK) + } + bulanlahir = nik[8:10] + hasil.NIK = nik + hasil.Sex = sex + // hasil.Ttl = bulanlahir + "-" + bulanlahir + "-" + tgllahir + hasil.Ttl = tgllahir + "-" + bulanlahir + "-" + thnlahir + hasil.Provinsi = fmt.Sprintf("%v", dt["provinsi"][nik[0:2]]) + hasil.Kabkot = fmt.Sprintf("%v", dt["kabkot"][nik[0:4]]) + hasil.Kecamatan = kecamatan + hasil.KodPos = kodepos + hasil.Uniq = nik[12:16] + hasil.Status = "Sukses" + // fmt.Println(thnlahir, tgllahir, bulanlahir, kecamatan, kodepos, sex) + return hasil + } + hasil.Status = "Gagal" + hasil.Pesan = "format salah / tidak ada dalam database" + return hasil +} diff --git a/utils/redis_utility.go b/utils/redis_utility.go new file mode 100644 index 0000000..ecdd132 --- /dev/null +++ b/utils/redis_utility.go @@ -0,0 +1,231 @@ +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "rijig/config" + + "github.com/go-redis/redis/v8" +) + +func SetCache(key string, value interface{}, expiration time.Duration) error { + jsonData, err := json.Marshal(value) + if err != nil { + return fmt.Errorf("failed to marshal data: %v", err) + } + + err = config.RedisClient.Set(config.Ctx, key, jsonData, expiration).Err() + if err != nil { + return fmt.Errorf("failed to set cache: %v", err) + } + + return nil +} + +func GetCache(key string, dest interface{}) error { + val, err := config.RedisClient.Get(config.Ctx, key).Result() + if err != nil { + if err == redis.Nil { + return errors.New("ErrCacheMiss") + } + return fmt.Errorf("failed to get cache: %v", err) + } + + err = json.Unmarshal([]byte(val), dest) + if err != nil { + return fmt.Errorf("failed to unmarshal cache data: %v", err) + } + + return nil +} + +func DeleteCache(key string) error { + err := config.RedisClient.Del(config.Ctx, key).Err() + if err != nil { + return fmt.Errorf("failed to delete cache: %v", err) + } + return nil +} + +func ScanAndDelete(pattern string) error { + var cursor uint64 + for { + keys, nextCursor, err := config.RedisClient.Scan(config.Ctx, cursor, pattern, 10).Result() + if err != nil { + return err + } + if len(keys) > 0 { + if err := config.RedisClient.Del(config.Ctx, keys...).Err(); err != nil { + return err + } + } + if nextCursor == 0 { + break + } + cursor = nextCursor + } + return nil +} + +func CacheExists(key string) (bool, error) { + exists, err := config.RedisClient.Exists(config.Ctx, key).Result() + if err != nil { + return false, fmt.Errorf("failed to check cache existence: %v", err) + } + return exists > 0, nil +} + +func SetCacheWithTTL(key string, value interface{}, expiration time.Duration) error { + return SetCache(key, value, expiration) +} + +func GetTTL(key string) (time.Duration, error) { + ttl, err := config.RedisClient.TTL(config.Ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to get TTL: %v", err) + } + return ttl, nil +} + +func RefreshTTL(key string, expiration time.Duration) error { + err := config.RedisClient.Expire(config.Ctx, key, expiration).Err() + if err != nil { + return fmt.Errorf("failed to refresh TTL: %v", err) + } + return nil +} + +func IncrementCounter(key string, expiration time.Duration) (int64, error) { + val, err := config.RedisClient.Incr(config.Ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to increment counter: %v", err) + } + + if val == 1 { + config.RedisClient.Expire(config.Ctx, key, expiration) + } + + return val, nil +} + +func DecrementCounter(key string) (int64, error) { + val, err := config.RedisClient.Decr(config.Ctx, key).Result() + if err != nil { + return 0, fmt.Errorf("failed to decrement counter: %v", err) + } + return val, nil +} + +func GetCounter(key string) (int64, error) { + val, err := config.RedisClient.Get(config.Ctx, key).Int64() + if err != nil { + if err == redis.Nil { + return 0, nil + } + return 0, fmt.Errorf("failed to get counter: %v", err) + } + return val, nil +} + +func SetList(key string, values []interface{}, expiration time.Duration) error { + + config.RedisClient.Del(config.Ctx, key) + + if len(values) > 0 { + err := config.RedisClient.LPush(config.Ctx, key, values...).Err() + if err != nil { + return fmt.Errorf("failed to set list: %v", err) + } + + if expiration > 0 { + config.RedisClient.Expire(config.Ctx, key, expiration) + } + } + + return nil +} + +func GetList(key string) ([]string, error) { + vals, err := config.RedisClient.LRange(config.Ctx, key, 0, -1).Result() + if err != nil { + return nil, fmt.Errorf("failed to get list: %v", err) + } + return vals, nil +} + +func AddToList(key string, value interface{}, expiration time.Duration) error { + err := config.RedisClient.LPush(config.Ctx, key, value).Err() + if err != nil { + return fmt.Errorf("failed to add to list: %v", err) + } + + if expiration > 0 { + config.RedisClient.Expire(config.Ctx, key, expiration) + } + + return nil +} + +func SetHash(key string, fields map[string]interface{}, expiration time.Duration) error { + err := config.RedisClient.HMSet(config.Ctx, key, fields).Err() + if err != nil { + return fmt.Errorf("failed to set hash: %v", err) + } + + if expiration > 0 { + config.RedisClient.Expire(config.Ctx, key, expiration) + } + + return nil +} + +func GetHash(key string) (map[string]string, error) { + vals, err := config.RedisClient.HGetAll(config.Ctx, key).Result() + if err != nil { + return nil, fmt.Errorf("failed to get hash: %v", err) + } + return vals, nil +} + +func GetHashField(key, field string) (string, error) { + val, err := config.RedisClient.HGet(config.Ctx, key, field).Result() + if err != nil { + if err == redis.Nil { + return "", fmt.Errorf("hash field not found") + } + return "", fmt.Errorf("failed to get hash field: %v", err) + } + return val, nil +} + +func SetHashField(key, field string, value interface{}, expiration time.Duration) error { + err := config.RedisClient.HSet(config.Ctx, key, field, value).Err() + if err != nil { + return fmt.Errorf("failed to set hash field: %v", err) + } + + if expiration > 0 { + config.RedisClient.Expire(config.Ctx, key, expiration) + } + + return nil +} + +func FlushDB() error { + err := config.RedisClient.FlushDB(config.Ctx).Err() + if err != nil { + return fmt.Errorf("failed to flush database: %v", err) + } + return nil +} + +func GetAllKeys(pattern string) ([]string, error) { + keys, err := config.RedisClient.Keys(config.Ctx, pattern).Result() + if err != nil { + return nil, fmt.Errorf("failed to get keys: %v", err) + } + return keys, nil +} diff --git a/utils/regexp_formatter.go b/utils/regexp_formatter.go deleted file mode 100644 index 3b020ed..0000000 --- a/utils/regexp_formatter.go +++ /dev/null @@ -1,63 +0,0 @@ -package utils - -import ( - "fmt" - "regexp" - "strconv" - "strings" -) - -func IsValidPhoneNumber(phone string) bool { - re := regexp.MustCompile(`^62\d{9,13}$`) - return re.MatchString(phone) -} - -func IsValidEmail(email string) bool { - re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) - return re.MatchString(email) -} - -func IsValidPassword(password string) bool { - - if len(password) < 6 { - return false - } - - hasUpper := false - hasDigit := false - hasSpecial := false - - for _, char := range password { - if char >= 'A' && char <= 'Z' { - hasUpper = true - } else if char >= '0' && char <= '9' { - hasDigit = true - } else if isSpecialCharacter(char) { - hasSpecial = true - } - } - - return hasUpper && hasDigit && hasSpecial -} - -func isSpecialCharacter(char rune) bool { - specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" - return strings.ContainsRune(specialChars, char) -} - -func ValidateFloatPrice(price string) (float64, error) { - - // price = strings.Trim(price, `"`) - // price = strings.TrimSpace(price) - - parsedPrice, err := strconv.ParseFloat(price, 64) - if err != nil { - return 0, fmt.Errorf("harga tidak valid. Format harga harus angka desimal.") - } - - if parsedPrice <= 0 { - return 0, fmt.Errorf("harga harus lebih besar dari 0.") - } - - return parsedPrice, nil -} diff --git a/utils/response.go b/utils/response.go index b004ba8..a36fb64 100644 --- a/utils/response.go +++ b/utils/response.go @@ -1,107 +1,107 @@ package utils -import ( - "github.com/gofiber/fiber/v2" -) +// import ( +// "github.com/gofiber/fiber/v2" +// ) -type MetaData struct { - Status int `json:"status"` - Page int `json:"page,omitempty"` - Limit int `json:"limit,omitempty"` - Total int `json:"total,omitempty"` - Message string `json:"message"` -} +// type MetaData struct { +// Status int `json:"status"` +// Page int `json:"page,omitempty"` +// Limit int `json:"limit,omitempty"` +// Total int `json:"total,omitempty"` +// Message string `json:"message"` +// } -type APIResponse struct { - Meta MetaData `json:"meta"` - Data interface{} `json:"data,omitempty"` -} +// type APIResponse struct { +// Meta MetaData `json:"meta"` +// Data interface{} `json:"data,omitempty"` +// } -func PaginatedResponse(c *fiber.Ctx, data interface{}, page, limit, total int, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusOK, - Page: page, - Limit: limit, - Total: total, - Message: message, - }, - Data: data, - } - return c.Status(fiber.StatusOK).JSON(response) -} +// func PaginatedResponse(c *fiber.Ctx, data interface{}, page, limit, total int, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusOK, +// Page: page, +// Limit: limit, +// Total: total, +// Message: message, +// }, +// Data: data, +// } +// return c.Status(fiber.StatusOK).JSON(response) +// } -func NonPaginatedResponse(c *fiber.Ctx, data interface{}, total int, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusOK, - Total: total, - Message: message, - }, - Data: data, - } - return c.Status(fiber.StatusOK).JSON(response) -} +// func NonPaginatedResponse(c *fiber.Ctx, data interface{}, total int, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusOK, +// Total: total, +// Message: message, +// }, +// Data: data, +// } +// return c.Status(fiber.StatusOK).JSON(response) +// } -func ErrorResponse(c *fiber.Ctx, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusNotFound, - Message: message, - }, - } - return c.Status(fiber.StatusNotFound).JSON(response) -} +// func ErrorResponse(c *fiber.Ctx, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusNotFound, +// Message: message, +// }, +// } +// return c.Status(fiber.StatusNotFound).JSON(response) +// } -func ValidationErrorResponse(c *fiber.Ctx, errors map[string][]string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusBadRequest, - Message: "invalid user request", - }, - Data: errors, - } - return c.Status(fiber.StatusBadRequest).JSON(response) -} +// func ValidationErrorResponse(c *fiber.Ctx, errors map[string][]string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusBadRequest, +// Message: "invalid user request", +// }, +// Data: errors, +// } +// return c.Status(fiber.StatusBadRequest).JSON(response) +// } -func InternalServerErrorResponse(c *fiber.Ctx, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusInternalServerError, - Message: message, - }, - } - return c.Status(fiber.StatusInternalServerError).JSON(response) -} +// func InternalServerErrorResponse(c *fiber.Ctx, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusInternalServerError, +// Message: message, +// }, +// } +// return c.Status(fiber.StatusInternalServerError).JSON(response) +// } -func GenericResponse(c *fiber.Ctx, status int, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: status, - Message: message, - }, - } - return c.Status(status).JSON(response) -} +// func GenericResponse(c *fiber.Ctx, status int, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: status, +// Message: message, +// }, +// } +// return c.Status(status).JSON(response) +// } -func SuccessResponse(c *fiber.Ctx, data interface{}, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusOK, - Message: message, - }, - Data: data, - } - return c.Status(fiber.StatusOK).JSON(response) -} +// func SuccessResponse(c *fiber.Ctx, data interface{}, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusOK, +// Message: message, +// }, +// Data: data, +// } +// return c.Status(fiber.StatusOK).JSON(response) +// } -func CreateResponse(c *fiber.Ctx, data interface{}, message string) error { - response := APIResponse{ - Meta: MetaData{ - Status: fiber.StatusCreated, - Message: message, - }, - Data: data, - } - return c.Status(fiber.StatusOK).JSON(response) -} +// func CreateResponse(c *fiber.Ctx, data interface{}, message string) error { +// response := APIResponse{ +// Meta: MetaData{ +// Status: fiber.StatusCreated, +// Message: message, +// }, +// Data: data, +// } +// return c.Status(fiber.StatusOK).JSON(response) +// } diff --git a/utils/role_permission.go b/utils/role_permission.go index 6de0e72..a805ea9 100644 --- a/utils/role_permission.go +++ b/utils/role_permission.go @@ -1,8 +1,17 @@ package utils -const ( +// RoleID based +/* const ( RoleAdministrator = "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7" RolePengelola = "0bf86966-7042-410a-a88c-d01f70832348" RolePengepul = "d7245535-0e9e-4d35-ab39-baece5c10b3c" RoleMasyarakat = "60e5684e4-b214-4bd0-972f-3be80c4649a0" +) */ + +// RoleName based +const ( + RoleAdministrator = "administrator" + RolePengelola = "pengelola" + RolePengepul = "pengepul" + RoleMasyarakat = "masyarakat" ) diff --git a/utils/todo_validation.go b/utils/todo_validation.go new file mode 100644 index 0000000..51aab56 --- /dev/null +++ b/utils/todo_validation.go @@ -0,0 +1,95 @@ +package utils + +import ( + "errors" + "fmt" + "log" + "regexp" + "strings" + + "crypto/rand" + "math/big" + + "golang.org/x/crypto/bcrypt" +) + +func IsValidPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^628\d{9,14}$`) + return re.MatchString(phone) +} + +func IsValidDate(date string) bool { + re := regexp.MustCompile(`^\d{2}-\d{2}-\d{4}$`) + return re.MatchString(date) +} + + +func IsValidEmail(email string) bool { + re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) + return re.MatchString(email) +} + +func IsValidPassword(password string) bool { + + if len(password) < 8 { + return false + } + + hasUpper := false + hasDigit := false + hasSpecial := false + + for _, char := range password { + if char >= 'A' && char <= 'Z' { + hasUpper = true + } else if char >= '0' && char <= '9' { + hasDigit = true + } else if isSpecialCharacter(char) { + hasSpecial = true + } + } + + return hasUpper && hasDigit && hasSpecial +} + +func isSpecialCharacter(char rune) bool { + specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" + return strings.ContainsRune(specialChars, char) +} + +func HashingPlainText(plainText string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(plainText), bcrypt.DefaultCost) + if err != nil { + log.Println("Error hashing password:", err) + } + return string(bytes), nil +} + +func CompareHashAndPlainText(hashedText, plaintext string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedText), []byte(plaintext)) + return err == nil +} + +func IsNumeric(s string) bool { + re := regexp.MustCompile(`^[0-9]+$`) + return re.MatchString(s) +} + +func ValidatePin(pin string) error { + if len(pin) != 6 { + return errors.New("PIN must be 6 digits") + } + if !IsNumeric(pin) { + return errors.New("PIN must contain only numbers") + } + return nil +} + +func GenerateOTP() (string, error) { + max := big.NewInt(9999) + n, err := rand.Int(rand.Reader, max) + if err != nil { + return "", err + } + return fmt.Sprintf("%04d", n.Int64()), nil +} diff --git a/utils/token_management.go b/utils/token_management.go new file mode 100644 index 0000000..95ca2c8 --- /dev/null +++ b/utils/token_management.go @@ -0,0 +1,654 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "os" + "strings" + "time" + + "rijig/config" + + "github.com/golang-jwt/jwt/v5" +) + +type TokenType string + +const ( + TokenTypePartial TokenType = "partial" + TokenTypeFull TokenType = "full" + TokenTypeRefresh TokenType = "refresh" +) + +const ( + RegStatusIncomplete = "uncomplete" + RegStatusPending = "awaiting_approval" + RegStatusConfirmed = "approved" + RegStatusComplete = "complete" + RegStatusRejected = "rejected" +) + +const ( + ProgressOTPVerified = 1 + ProgressDataSubmitted = 2 + ProgressComplete = 3 +) + +type JWTClaims struct { + UserID string `json:"user_id"` + Role string `json:"role"` + DeviceID string `json:"device_id"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int `json:"registration_progress"` + TokenType TokenType `json:"token_type"` + SessionID string `json:"session_id,omitempty"` + jwt.RegisteredClaims +} + +type RefreshTokenData struct { + RefreshToken string `json:"refresh_token"` + ExpiresAt int64 `json:"expires_at"` + DeviceID string `json:"device_id"` + Role string `json:"role"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int `json:"registration_progress"` + SessionID string `json:"session_id"` + CreatedAt int64 `json:"created_at"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token,omitempty"` + ExpiresIn int64 `json:"expires_in"` + TokenType TokenType `json:"token_type"` + RegistrationStatus string `json:"registration_status"` + RegistrationProgress int `json:"registration_progress"` + NextStep string `json:"next_step,omitempty"` + SessionID string `json:"session_id"` + RequiresAdminApproval bool `json:"requires_admin_approval,omitempty"` +} + +type RegistrationStepInfo struct { + Step int `json:"step"` + Status string `json:"status"` + Description string `json:"description"` + RequiresAdminApproval bool `json:"requires_admin_approval"` + IsAccessible bool `json:"is_accessible"` + IsCompleted bool `json:"is_completed"` +} + +func GetTTLFromEnv(key string, fallback time.Duration) time.Duration { + raw := os.Getenv(key) + if raw == "" { + return fallback + } + + ttl, err := time.ParseDuration(raw) + if err != nil { + return fallback + } + return ttl +} + +var ( + ACCESS_TOKEN_EXPIRY = GetTTLFromEnv("ACCESS_TOKEN_EXPIRY", 23*time.Hour) + REFRESH_TOKEN_EXPIRY = GetTTLFromEnv("REFRESH_TOKEN_EXPIRY", 28*24*time.Hour) + PARTIAL_TOKEN_EXPIRY = GetTTLFromEnv("PARTIAL_TOKEN_EXPIRY", 2*time.Hour) +) + +func GenerateSessionID() (string, error) { + bytes := make([]byte, 16) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate session ID: %v", err) + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func GenerateJTI() string { + bytes := make([]byte, 16) + if _, err := rand.Read(bytes); err != nil { + + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return base64.URLEncoding.EncodeToString(bytes) +} + +func ConstantTimeCompare(a, b string) bool { + if len(a) != len(b) { + return false + } + result := 0 + for i := 0; i < len(a); i++ { + result |= int(a[i]) ^ int(b[i]) + } + return result == 0 +} + +func ExtractTokenFromHeader(authHeader string) (string, error) { + if authHeader == "" { + return "", fmt.Errorf("authorization header is empty") + } + + const bearerPrefix = "Bearer " + if len(authHeader) < len(bearerPrefix) || !strings.HasPrefix(authHeader, bearerPrefix) { + return "", fmt.Errorf("invalid authorization header format") + } + + token := strings.TrimSpace(authHeader[len(bearerPrefix):]) + if token == "" { + return "", fmt.Errorf("token is empty") + } + + return token, nil +} + +func GenerateAccessToken(userID, role, deviceID, registrationStatus string, registrationProgress int, tokenType TokenType, sessionID string) (string, error) { + secretKey := config.GetSecretKey() + if secretKey == "" { + return "", fmt.Errorf("secret key not found") + } + + if userID == "" || role == "" || deviceID == "" { + return "", fmt.Errorf("required fields cannot be empty") + } + + now := time.Now() + expiry := ACCESS_TOKEN_EXPIRY + if tokenType == TokenTypePartial { + expiry = PARTIAL_TOKEN_EXPIRY + } + + claims := JWTClaims{ + UserID: userID, + Role: role, + DeviceID: deviceID, + RegistrationStatus: registrationStatus, + RegistrationProgress: registrationProgress, + TokenType: tokenType, + SessionID: sessionID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(now.Add(expiry)), + IssuedAt: jwt.NewNumericDate(now), + NotBefore: jwt.NewNumericDate(now), + Issuer: "rijig-api", + Subject: userID, + ID: GenerateJTI(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", fmt.Errorf("failed to sign token: %v", err) + } + + return tokenString, nil +} + +func GenerateRefreshToken() (string, error) { + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate refresh token: %v", err) + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func ValidateAccessToken(tokenString string) (*JWTClaims, error) { + secretKey := config.GetSecretKey() + if secretKey == "" { + return nil, fmt.Errorf("secret key not found") + } + + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(secretKey), nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to parse token: %v", err) + } + + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + if IsTokenBlacklisted(claims.ID) { + return nil, fmt.Errorf("token has been revoked") + } + return claims, nil + } + + return nil, fmt.Errorf("invalid token") +} + +func ValidateTokenWithChecks(tokenString string, requiredTokenType TokenType, requireCompleteReg bool) (*JWTClaims, error) { + claims, err := ValidateAccessToken(tokenString) + if err != nil { + return nil, err + } + + if requiredTokenType != "" && claims.TokenType != requiredTokenType { + return nil, fmt.Errorf("invalid token type: expected %s, got %s", requiredTokenType, claims.TokenType) + } + + if requireCompleteReg && !IsRegistrationComplete(claims.RegistrationStatus) { + return nil, fmt.Errorf("registration not complete") + } + + return claims, nil +} + +func ValidateTokenForStep(tokenString string, role string, requiredStep int) (*JWTClaims, error) { + claims, err := ValidateAccessToken(tokenString) + if err != nil { + return nil, err + } + + if claims.Role != role { + return nil, fmt.Errorf("role mismatch") + } + + if claims.RegistrationProgress < requiredStep { + return nil, fmt.Errorf("step not accessible yet: current step %d, required step %d", + claims.RegistrationProgress, requiredStep) + } + + return claims, nil +} + +func StoreRefreshToken(userID, deviceID, refreshToken, role, registrationStatus string, registrationProgress int, sessionID string) error { + key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) + + DeleteCache(key) + + data := RefreshTokenData{ + RefreshToken: refreshToken, + ExpiresAt: time.Now().Add(REFRESH_TOKEN_EXPIRY).Unix(), + DeviceID: deviceID, + Role: role, + RegistrationStatus: registrationStatus, + RegistrationProgress: registrationProgress, + SessionID: sessionID, + CreatedAt: time.Now().Unix(), + } + + err := SetCache(key, data, REFRESH_TOKEN_EXPIRY) + if err != nil { + return fmt.Errorf("failed to store refresh token: %v", err) + } + + return nil +} + +func ValidateRefreshToken(userID, deviceID, refreshToken string) (*RefreshTokenData, error) { + key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) + + var data RefreshTokenData + err := GetCache(key, &data) + if err != nil { + return nil, fmt.Errorf("refresh token not found or invalid") + } + + if !ConstantTimeCompare(data.RefreshToken, refreshToken) { + return nil, fmt.Errorf("refresh token mismatch") + } + + if time.Now().Unix() > data.ExpiresAt { + DeleteCache(key) + return nil, fmt.Errorf("refresh token expired") + } + + return &data, nil +} + +func RefreshAccessToken(userID, deviceID, refreshToken string) (*TokenResponse, error) { + data, err := ValidateRefreshToken(userID, deviceID, refreshToken) + if err != nil { + return nil, err + } + + tokenType := DetermineTokenType(data.RegistrationStatus) + + accessToken, err := GenerateAccessToken( + userID, + data.Role, + deviceID, + data.RegistrationStatus, + data.RegistrationProgress, + tokenType, + data.SessionID, + ) + if err != nil { + return nil, fmt.Errorf("failed to generate new access token: %v", err) + } + + expiry := ACCESS_TOKEN_EXPIRY + if tokenType == TokenTypePartial { + expiry = PARTIAL_TOKEN_EXPIRY + } + + nextStep := GetNextRegistrationStep(data.Role, data.RegistrationProgress, data.RegistrationStatus) + requiresAdminApproval := RequiresAdminApproval(data.Role, data.RegistrationProgress, data.RegistrationStatus) + + return &TokenResponse{ + AccessToken: accessToken, + ExpiresIn: int64(expiry.Seconds()), + TokenType: tokenType, + RegistrationStatus: data.RegistrationStatus, + RegistrationProgress: data.RegistrationProgress, + NextStep: nextStep, + SessionID: data.SessionID, + RequiresAdminApproval: requiresAdminApproval, + }, nil +} + +func GenerateTokenPair(userID, role, deviceID, registrationStatus string, registrationProgress int) (*TokenResponse, error) { + if userID == "" || role == "" || deviceID == "" { + return nil, fmt.Errorf("required parameters cannot be empty") + } + + sessionID, err := GenerateSessionID() + if err != nil { + return nil, fmt.Errorf("failed to generate session ID: %v", err) + } + + tokenType := DetermineTokenType(registrationStatus) + + accessToken, err := GenerateAccessToken( + userID, + role, + deviceID, + registrationStatus, + registrationProgress, + tokenType, + sessionID, + ) + if err != nil { + return nil, err + } + + refreshToken, err := GenerateRefreshToken() + if err != nil { + return nil, err + } + + err = StoreRefreshToken(userID, deviceID, refreshToken, role, registrationStatus, registrationProgress, sessionID) + if err != nil { + return nil, err + } + + expiry := ACCESS_TOKEN_EXPIRY + if tokenType == TokenTypePartial { + expiry = PARTIAL_TOKEN_EXPIRY + } + + nextStep := GetNextRegistrationStep(role, registrationProgress, registrationStatus) + requiresAdminApproval := RequiresAdminApproval(role, registrationProgress, registrationStatus) + + return &TokenResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + ExpiresIn: int64(expiry.Seconds()), + TokenType: tokenType, + RegistrationStatus: registrationStatus, + RegistrationProgress: registrationProgress, + NextStep: nextStep, + SessionID: sessionID, + RequiresAdminApproval: requiresAdminApproval, + }, nil +} + +func GenerateTokenForRole(userID, role, deviceID string, progress int, status string) (*TokenResponse, error) { + switch role { + case RoleAdministrator: + + return GenerateTokenPair(userID, role, deviceID, RegStatusComplete, ProgressComplete) + default: + + return GenerateTokenPair(userID, role, deviceID, status, progress) + } +} + +func DetermineTokenType(registrationStatus string) TokenType { + if registrationStatus == RegStatusComplete { + return TokenTypeFull + } + return TokenTypePartial +} + +func IsRegistrationComplete(registrationStatus string) bool { + return registrationStatus == RegStatusComplete +} + +func RequiresAdminApproval(role string, progress int, status string) bool { + switch role { + case RolePengelola, RolePengepul: + return progress == ProgressDataSubmitted && status == RegStatusPending + default: + return false + } +} + +func GetNextRegistrationStep(role string, progress int, status string) string { + switch role { + case RoleAdministrator: + return "completed" + + case RoleMasyarakat: + switch progress { + case ProgressOTPVerified: + return "complete_personal_data" + case ProgressDataSubmitted: + return "create_pin" + case ProgressComplete: + return "completed" + } + + case RolePengepul: + switch progress { + case ProgressOTPVerified: + return "upload_ktp" + case ProgressDataSubmitted: + if status == RegStatusPending { + return "awaiting_admin_approval" + } else if status == RegStatusConfirmed { + return "create_pin" + } + case ProgressComplete: + return "completed" + } + + case RolePengelola: + switch progress { + case ProgressOTPVerified: + return "complete_company_data" + case ProgressDataSubmitted: + if status == RegStatusPending { + return "awaiting_admin_approval" + } else if status == RegStatusConfirmed { + return "create_pin" + } + case ProgressComplete: + return "completed" + } + } + return "unknown" +} + +func GetRegistrationStepInfo(role string, currentProgress int, currentStatus string) *RegistrationStepInfo { + switch role { + case RoleAdministrator: + return &RegistrationStepInfo{ + Step: ProgressComplete, + Status: RegStatusComplete, + Description: "Administrator registration complete", + IsAccessible: true, + IsCompleted: true, + } + + case RoleMasyarakat: + return getMasyarakatStepInfo(currentProgress, currentStatus) + + case RolePengepul: + return getPengepulStepInfo(currentProgress, currentStatus) + + case RolePengelola: + return getPengelolaStepInfo(currentProgress, currentStatus) + } + + return &RegistrationStepInfo{ + Step: 0, + Status: "unknown", + Description: "Unknown role", + IsAccessible: false, + IsCompleted: false, + } +} + +func getMasyarakatStepInfo(progress int, status string) *RegistrationStepInfo { + switch progress { + case ProgressOTPVerified: + return &RegistrationStepInfo{ + Step: ProgressOTPVerified, + Status: status, + Description: "Complete personal data", + IsAccessible: true, + IsCompleted: false, + } + case ProgressDataSubmitted: + return &RegistrationStepInfo{ + Step: ProgressDataSubmitted, + Status: status, + Description: "Create PIN", + IsAccessible: true, + IsCompleted: false, + } + case ProgressComplete: + return &RegistrationStepInfo{ + Step: ProgressComplete, + Status: RegStatusComplete, + Description: "Registration complete", + IsAccessible: true, + IsCompleted: true, + } + } + return nil +} + +func getPengepulStepInfo(progress int, status string) *RegistrationStepInfo { + switch progress { + case ProgressOTPVerified: + return &RegistrationStepInfo{ + Step: ProgressOTPVerified, + Status: status, + Description: "Upload KTP", + IsAccessible: true, + IsCompleted: false, + } + case ProgressDataSubmitted: + if status == RegStatusPending { + return &RegistrationStepInfo{ + Step: ProgressDataSubmitted, + Status: status, + Description: "Awaiting admin approval", + RequiresAdminApproval: true, + IsAccessible: false, + IsCompleted: false, + } + } else if status == RegStatusConfirmed { + return &RegistrationStepInfo{ + Step: ProgressDataSubmitted, + Status: status, + Description: "Create PIN", + IsAccessible: true, + IsCompleted: false, + } + } + case ProgressComplete: + return &RegistrationStepInfo{ + Step: ProgressComplete, + Status: RegStatusComplete, + Description: "Registration complete", + IsAccessible: true, + IsCompleted: true, + } + } + return nil +} + +func getPengelolaStepInfo(progress int, status string) *RegistrationStepInfo { + switch progress { + case ProgressOTPVerified: + return &RegistrationStepInfo{ + Step: ProgressOTPVerified, + Status: status, + Description: "Complete company data", + IsAccessible: true, + IsCompleted: false, + } + case ProgressDataSubmitted: + if status == RegStatusPending { + return &RegistrationStepInfo{ + Step: ProgressDataSubmitted, + Status: status, + Description: "Awaiting admin approval", + RequiresAdminApproval: true, + IsAccessible: false, + IsCompleted: false, + } + } else if status == RegStatusConfirmed { + return &RegistrationStepInfo{ + Step: ProgressDataSubmitted, + Status: status, + Description: "Create PIN", + IsAccessible: true, + IsCompleted: false, + } + } + case ProgressComplete: + return &RegistrationStepInfo{ + Step: ProgressComplete, + Status: RegStatusComplete, + Description: "Registration complete", + IsAccessible: true, + IsCompleted: true, + } + } + return nil +} + +func RevokeRefreshToken(userID, deviceID string) error { + key := fmt.Sprintf("refresh_token:%s:%s", userID, deviceID) + err := DeleteCache(key) + if err != nil { + return fmt.Errorf("failed to revoke refresh token: %v", err) + } + return nil +} + +func RevokeAllRefreshTokens(userID string) error { + pattern := fmt.Sprintf("refresh_token:%s:*", userID) + err := ScanAndDelete(pattern) + if err != nil { + return fmt.Errorf("failed to revoke all refresh tokens: %v", err) + } + return nil +} + +func BlacklistToken(jti string, expiresAt time.Time) error { + key := fmt.Sprintf("blacklist:%s", jti) + ttl := time.Until(expiresAt) + if ttl <= 0 { + return nil + } + return SetCache(key, true, ttl) +} + +func IsTokenBlacklisted(jti string) bool { + key := fmt.Sprintf("blacklist:%s", jti) + var exists bool + err := GetCache(key, &exists) + return err == nil && exists +} From d8b43348b3467d25d98799617847ccebedb198c8 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 9 Jun 2025 06:03:43 +0700 Subject: [PATCH 42/48] feat: setup docker --- .dockerignore | 71 ++ .gitignore | 63 +- Dockerfile.dev | 28 + Makefile | 153 +++++ cmd/main.go | 35 +- config/setup_config.go | 22 +- config/whatsapp.go | 338 ++++++++-- docker-compose.dev.yml | 139 ++++ dto/requestpickup_dto.go | 2 +- internal/about/about_handler.go | 23 +- internal/about/about_route.go | 33 +- internal/address/address_handler.go | 114 +++- internal/address/address_route.go | 25 +- internal/article/article_route.go | 9 +- internal/cart/cart_dto.go | 50 +- internal/cart/cart_repository.go | 163 ++++- internal/cart/cart_service.go | 357 +++++++++- internal/collector/collector_dto.go | 253 ++++++- internal/collector/collector_handler.go | 361 +++++++++- internal/collector/collector_repository.go | 338 +++++++++- internal/collector/collector_service.go | 350 +++++++++- internal/handler/about_handler.go | 176 ----- internal/handler/address_handler.go | 93 --- internal/handler/article_handler.go | 138 ---- internal/handler/auth/auth_admin_handler.go | 81 --- .../handler/auth/auth_masyarakat_handler.go | 82 --- .../handler/auth/auth_pengepul_handler.go | 82 --- .../handler/auth/auth_pnegelola_handler.go | 82 --- internal/handler/auth_handler.go | 80 --- internal/handler/banner_handler.go | 109 --- internal/handler/cart_handler.go | 93 --- internal/handler/collector_handler.go | 194 ------ internal/handler/company_profile_handler.go | 100 --- internal/handler/coveragearea_handler.go | 93 --- internal/handler/identitycard_handler.go | 134 ---- internal/handler/initialcoint_handler.go | 99 --- internal/handler/pickup_history_handler.go | 37 - internal/handler/pickup_matching_handler.go | 49 -- internal/handler/product_handler.go | 227 ------- internal/handler/rating_handler.go | 66 -- internal/handler/request_pickup_handler.go | 150 ----- internal/handler/role_handler.go | 47 -- internal/handler/store_handler.go | 159 ----- internal/handler/trash_handler.go | 148 ---- internal/handler/user_handler.go | 101 --- internal/handler/userpin_handler.go | 102 --- internal/handler/whatsapp_handler.go | 24 - internal/handler/wilayah_indonesia_handler.go | 200 ------ internal/repositories/about_repo.go | 112 ---- internal/repositories/address_repo.go | 61 -- internal/repositories/article_repo.go | 75 --- internal/repositories/auth/auth_admin_repo.go | 93 --- .../repositories/auth/auth_masyarakat_repo.go | 48 -- .../repositories/auth/auth_pengelola_repo.go | 48 -- .../repositories/auth/auth_pengepul_repo.go | 48 -- internal/repositories/auth_repo.go | 48 -- internal/repositories/banner_repo.go | 70 -- internal/repositories/collector_repo.go | 135 ---- internal/repositories/company_profile_repo.go | 78 --- internal/repositories/coveragearea_repo.go | 82 --- internal/repositories/identitycard_repo.go | 82 --- internal/repositories/initialcoint_repo.go | 68 -- internal/repositories/pickup_history_repo.go | 34 - internal/repositories/product_repo.go | 139 ---- internal/repositories/rating_repo.go | 48 -- internal/repositories/request_pickup_repo.go | 143 ---- internal/repositories/role_repo.go | 49 -- internal/repositories/store_repo.go | 88 --- internal/repositories/trash_repo.go | 175 ----- internal/repositories/trashcart_repo.go | 166 ----- internal/repositories/user_repo.go | 77 --- internal/repositories/userpin_repo.go | 60 -- .../repositories/wilayah_indonesia_repo.go | 244 ------- internal/services/about_service.go | 436 ------------ internal/services/address_service.go | 412 ------------ internal/services/article_service.go | 415 ------------ internal/services/auth/auth_admin_service.go | 192 ------ .../services/auth/auth_masyarakat_service.go | 171 ----- .../services/auth/auth_pengelola_service.go | 171 ----- .../services/auth/auth_pengepul_service.go | 172 ----- internal/services/auth/otp.go | 14 - internal/services/auth_service.go | 213 ------ internal/services/banner_service.go | 366 ---------- internal/services/cart_redis.go | 73 -- internal/services/cart_service.go | 266 -------- internal/services/collector_service.go | 224 ------- internal/services/company_profile_service.go | 163 ----- internal/services/coveragearea_service.go | 155 ----- internal/services/identitycard_service.go | 289 -------- internal/services/initialcoint_service.go | 299 --------- internal/services/pickup_history_service.go | 36 - internal/services/pickup_maching_service.go | 146 ---- internal/services/product_service.go | 404 ----------- internal/services/rating_service.go | 43 -- internal/services/request_pickup_service.go | 137 ---- internal/services/role_service.go | 103 --- internal/services/store_service.go | 294 -------- internal/services/trash_service.go | 631 ------------------ internal/services/trashcart_service.go | 154 ----- internal/services/user_service.go | 231 ------- internal/services/userpin_service.go | 133 ---- .../services/wilayah_indonesia_service.go | 494 -------------- internal/worker/cart_worker.go | 3 +- presentation/about_route.go | 35 - presentation/address_route.go | 26 - presentation/article_route.go | 26 - presentation/auth/auth_admin_route.go | 36 - presentation/auth/auth_masyarakat_route.go | 27 - presentation/auth/auth_pengelola_route.go | 27 - presentation/auth/auth_pengepul_route.go | 27 - presentation/auth_route.go | 23 - presentation/cart_router.go | 28 - presentation/collector_route.go | 42 -- presentation/company_profile_route.go | 27 - presentation/coveragearea_route.go | 25 - presentation/identitycard_route.go | 29 - presentation/pickup_matching_route.go | 25 - presentation/rating_route.go | 24 - presentation/request_pickup_route.go | 35 - presentation/role_route.go | 19 - presentation/trash_route.go | 33 - presentation/user_route.go | 29 - presentation/userpin_route.go | 24 - presentation/wilayahindonesia_route.go | 36 - router/setup_routes.go.go | 33 +- 125 files changed, 2826 insertions(+), 12794 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.dev create mode 100644 Makefile create mode 100644 docker-compose.dev.yml delete mode 100644 internal/handler/about_handler.go delete mode 100644 internal/handler/address_handler.go delete mode 100644 internal/handler/article_handler.go delete mode 100644 internal/handler/auth/auth_admin_handler.go delete mode 100644 internal/handler/auth/auth_masyarakat_handler.go delete mode 100644 internal/handler/auth/auth_pengepul_handler.go delete mode 100644 internal/handler/auth/auth_pnegelola_handler.go delete mode 100644 internal/handler/auth_handler.go delete mode 100644 internal/handler/banner_handler.go delete mode 100644 internal/handler/cart_handler.go delete mode 100644 internal/handler/collector_handler.go delete mode 100644 internal/handler/company_profile_handler.go delete mode 100644 internal/handler/coveragearea_handler.go delete mode 100644 internal/handler/identitycard_handler.go delete mode 100644 internal/handler/initialcoint_handler.go delete mode 100644 internal/handler/pickup_history_handler.go delete mode 100644 internal/handler/pickup_matching_handler.go delete mode 100644 internal/handler/product_handler.go delete mode 100644 internal/handler/rating_handler.go delete mode 100644 internal/handler/request_pickup_handler.go delete mode 100644 internal/handler/role_handler.go delete mode 100644 internal/handler/store_handler.go delete mode 100644 internal/handler/trash_handler.go delete mode 100644 internal/handler/user_handler.go delete mode 100644 internal/handler/userpin_handler.go delete mode 100644 internal/handler/whatsapp_handler.go delete mode 100644 internal/handler/wilayah_indonesia_handler.go delete mode 100644 internal/repositories/about_repo.go delete mode 100644 internal/repositories/address_repo.go delete mode 100644 internal/repositories/article_repo.go delete mode 100644 internal/repositories/auth/auth_admin_repo.go delete mode 100644 internal/repositories/auth/auth_masyarakat_repo.go delete mode 100644 internal/repositories/auth/auth_pengelola_repo.go delete mode 100644 internal/repositories/auth/auth_pengepul_repo.go delete mode 100644 internal/repositories/auth_repo.go delete mode 100644 internal/repositories/banner_repo.go delete mode 100644 internal/repositories/collector_repo.go delete mode 100644 internal/repositories/company_profile_repo.go delete mode 100644 internal/repositories/coveragearea_repo.go delete mode 100644 internal/repositories/identitycard_repo.go delete mode 100644 internal/repositories/initialcoint_repo.go delete mode 100644 internal/repositories/pickup_history_repo.go delete mode 100644 internal/repositories/product_repo.go delete mode 100644 internal/repositories/rating_repo.go delete mode 100644 internal/repositories/request_pickup_repo.go delete mode 100644 internal/repositories/role_repo.go delete mode 100644 internal/repositories/store_repo.go delete mode 100644 internal/repositories/trash_repo.go delete mode 100644 internal/repositories/trashcart_repo.go delete mode 100644 internal/repositories/user_repo.go delete mode 100644 internal/repositories/userpin_repo.go delete mode 100644 internal/repositories/wilayah_indonesia_repo.go delete mode 100644 internal/services/about_service.go delete mode 100644 internal/services/address_service.go delete mode 100644 internal/services/article_service.go delete mode 100644 internal/services/auth/auth_admin_service.go delete mode 100644 internal/services/auth/auth_masyarakat_service.go delete mode 100644 internal/services/auth/auth_pengelola_service.go delete mode 100644 internal/services/auth/auth_pengepul_service.go delete mode 100644 internal/services/auth/otp.go delete mode 100644 internal/services/auth_service.go delete mode 100644 internal/services/banner_service.go delete mode 100644 internal/services/cart_redis.go delete mode 100644 internal/services/cart_service.go delete mode 100644 internal/services/collector_service.go delete mode 100644 internal/services/company_profile_service.go delete mode 100644 internal/services/coveragearea_service.go delete mode 100644 internal/services/identitycard_service.go delete mode 100644 internal/services/initialcoint_service.go delete mode 100644 internal/services/pickup_history_service.go delete mode 100644 internal/services/pickup_maching_service.go delete mode 100644 internal/services/product_service.go delete mode 100644 internal/services/rating_service.go delete mode 100644 internal/services/request_pickup_service.go delete mode 100644 internal/services/role_service.go delete mode 100644 internal/services/store_service.go delete mode 100644 internal/services/trash_service.go delete mode 100644 internal/services/trashcart_service.go delete mode 100644 internal/services/user_service.go delete mode 100644 internal/services/userpin_service.go delete mode 100644 internal/services/wilayah_indonesia_service.go delete mode 100644 presentation/about_route.go delete mode 100644 presentation/address_route.go delete mode 100644 presentation/article_route.go delete mode 100644 presentation/auth/auth_admin_route.go delete mode 100644 presentation/auth/auth_masyarakat_route.go delete mode 100644 presentation/auth/auth_pengelola_route.go delete mode 100644 presentation/auth/auth_pengepul_route.go delete mode 100644 presentation/auth_route.go delete mode 100644 presentation/cart_router.go delete mode 100644 presentation/collector_route.go delete mode 100644 presentation/company_profile_route.go delete mode 100644 presentation/coveragearea_route.go delete mode 100644 presentation/identitycard_route.go delete mode 100644 presentation/pickup_matching_route.go delete mode 100644 presentation/rating_route.go delete mode 100644 presentation/request_pickup_route.go delete mode 100644 presentation/role_route.go delete mode 100644 presentation/trash_route.go delete mode 100644 presentation/user_route.go delete mode 100644 presentation/userpin_route.go delete mode 100644 presentation/wilayahindonesia_route.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8f6fb7e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,71 @@ +# Git +.git +.gitignore +README.md +.gitattributes + +# Documentation +*.md +docs/ + +# Environment files (kecuali yang diperlukan) +.env +.env.local +.env.example +# Kita tetap include .env.dev dan .env.docker untuk development + +# Logs +*.log +logs/ + +# Dependencies +vendor/ + +# Test files +*_test.go +testdata/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +tmp/ + +# Build artifacts (untuk production) +main +*.exe + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# Air specific +.air.toml +tmp/ +*_templ.go + +# Coverage +*.out +coverage.html + +# Database +*.db +*.sqlite +*.sqlite3 + +# Public uploads (jika ada) +public/uploads/ + +# Makefile +Makefile \ No newline at end of file diff --git a/.gitignore b/.gitignore index e3d41dc..73071d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# If you prefer the allow list template instead of the deny list, see community template: -# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore -# # Binaries for programs and plugins *.exe *.exe~ @@ -14,17 +11,61 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out -# Dependency directories (remove the comment below to include it) -# vendor/ +# Dependency directories +vendor/ # Go workspace file go.work go.work.sum -# env file -.env -.env.prod -.env.dev +# Environment files - ignore all variations +.env* +!.env.example -# Ignore public uploads -/public/apirijig/v2/uploads/ \ No newline at end of file +# Logs +*.log +logs/ + +# Temporary files +tmp/ +*.tmp +*.temp + +# IDE/Editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Air live reload tool +.air.toml + +# Public uploads - user generated content +/public/apirijig/v2/uploads/ + +# Build outputs +/bin/ +/build/ +/dist/ + +# Coverage reports +coverage.txt +coverage.html +*.cover + +# Debug files +debug +*.pprof + +# Local development files +*.local \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..c08cd9d --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,28 @@ +# Dockerfile untuk development environment dengan Air hot reload +FROM golang:1.23-alpine + +# Install dependencies dan Air +RUN apk add --no-cache git ca-certificates curl && \ + go install github.com/cosmtrek/air@latest + +# Set working directory +WORKDIR /app + +# Copy go mod files dan download dependencies +COPY go.mod go.sum ./ +RUN go mod download + +# Copy source code +COPY . . + +# Create tmp directory untuk Air +RUN mkdir -p tmp + +# Set timezone (optional) +RUN cp /usr/share/zoneinfo/Asia/Jakarta /etc/localtime + +# Expose port +EXPOSE 7000 + +# Run Air untuk hot reload +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6179595 --- /dev/null +++ b/Makefile @@ -0,0 +1,153 @@ +# Makefile untuk mengelola Docker commands + +.PHONY: help build up down restart logs clean dev prod dev-build dev-up dev-down dev-logs + +# Color codes untuk output yang lebih menarik +GREEN := \033[0;32m +YELLOW := \033[1;33m +RED := \033[0;31m +NC := \033[0m # No Color + +# Default target +help: + @echo "$(GREEN)Available commands:$(NC)" + @echo "$(YELLOW)Production:$(NC)" + @echo " build - Build all Docker images" + @echo " up - Start all services" + @echo " down - Stop all services" + @echo " restart - Restart all services" + @echo " logs - Show logs for all services" + @echo " clean - Remove all containers and volumes" + @echo " prod - Start production environment" + @echo "" + @echo "$(YELLOW)Development (dengan Air hot reload):$(NC)" + @echo " dev-build - Build development images" + @echo " dev-up - Start development environment dengan hot reload" + @echo " dev-down - Stop development environment" + @echo " dev-logs - Show development logs" + @echo " dev-clean - Clean development environment" + @echo " dev-restart- Restart development environment" + @echo "" + @echo "$(YELLOW)Utilities:$(NC)" + @echo " app-logs - Show only app logs" + @echo " db-logs - Show only database logs" + @echo " status - Check service status" + @echo " shell - Execute bash in app container" + @echo " psql - Execute psql in postgres container" + @echo " redis-cli - Execute redis-cli in redis container" + +# Production Commands +build: + @echo "$(GREEN)Building production images...$(NC)" + docker compose build --no-cache + +up: + @echo "$(GREEN)Starting production services...$(NC)" + docker compose up -d + +down: + @echo "$(RED)Stopping production services...$(NC)" + docker compose down + +restart: + @echo "$(YELLOW)Restarting production services...$(NC)" + docker compose restart + +logs: + @echo "$(GREEN)Showing production logs...$(NC)" + docker compose logs -f + +clean: + @echo "$(RED)Cleaning production environment...$(NC)" + docker compose down -v --remove-orphans + docker system prune -f + docker volume prune -f + +prod: + @echo "$(GREEN)Starting production environment...$(NC)" + docker compose up -d + +# Development Commands (dengan Air hot reload) +dev-build: + @echo "$(GREEN)Building development images dengan Air...$(NC)" + docker compose -f docker-compose.dev.yml build --no-cache + +dev-up: + @echo "$(GREEN)Starting development environment dengan Air hot reload...$(NC)" + docker compose -f docker-compose.dev.yml up -d + @echo "$(GREEN)Development services started!$(NC)" + @echo "$(YELLOW)API Server: http://localhost:7000$(NC)" + @echo "$(YELLOW)PostgreSQL: localhost:5433$(NC)" + @echo "$(YELLOW)Redis: localhost:6378$(NC)" + @echo "$(YELLOW)pgAdmin: http://localhost:8080 (admin@rijig.com / admin123)$(NC)" + @echo "$(YELLOW)Redis Commander: http://localhost:8081$(NC)" + @echo "" + @echo "$(GREEN)✨ Hot reload is active! Edit your Go files and see changes automatically ✨$(NC)" + +dev-down: + @echo "$(RED)Stopping development services...$(NC)" + docker compose -f docker-compose.dev.yml down + +dev-logs: + @echo "$(GREEN)Showing development logs...$(NC)" + docker compose -f docker-compose.dev.yml logs -f + +dev-clean: + @echo "$(RED)Cleaning development environment...$(NC)" + docker compose -f docker-compose.dev.yml down -v --remove-orphans + docker system prune -f + +dev-restart: + @echo "$(YELLOW)Restarting development services...$(NC)" + docker compose -f docker-compose.dev.yml restart + +# Development utilities +dev-app-logs: + @echo "$(GREEN)Showing development app logs...$(NC)" + docker compose -f docker-compose.dev.yml logs -f app + +dev-db-logs: + @echo "$(GREEN)Showing development database logs...$(NC)" + docker compose -f docker-compose.dev.yml logs -f postgres + +dev-shell: + @echo "$(GREEN)Accessing development app container...$(NC)" + docker compose -f docker-compose.dev.yml exec app sh + +dev-status: + @echo "$(GREEN)Development service status:$(NC)" + docker compose -f docker-compose.dev.yml ps + +# Shared utilities +app-logs: + docker compose logs -f app + +db-logs: + docker compose logs -f postgres + +status: + docker compose ps + +shell: + docker compose exec app sh + +psql: + docker compose exec postgres psql -U postgres -d apirijig_v2 + +redis-cli: + docker compose exec redis redis-cli + +# Rebuild and restart app only +app-rebuild: + docker compose build app + docker compose up -d app + +# View real-time resource usage +stats: + docker stats + +# Quick development setup (recommended) +dev: + @echo "$(GREEN)Setting up complete development environment...$(NC)" + make dev-build + make dev-up \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 46df279..85c404a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,13 +1,11 @@ package main import ( - "log" "rijig/config" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/internal/worker" + // "rijig/internal/repositories" + // "rijig/internal/services" + "rijig/router" - "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" @@ -15,21 +13,21 @@ import ( func main() { config.SetupConfig() - cartRepo := repositories.NewCartRepository() - trashRepo := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(cartRepo, trashRepo) - worker := worker.NewCartWorker(cartService, cartRepo, trashRepo) + // cartRepo := repositories.NewCartRepository() + // trashRepo := repositories.NewTrashRepository(config.DB) + // cartService := services.NewCartService(cartRepo, trashRepo) + // worker := worker.NewCartWorker(cartService, cartRepo, trashRepo) - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() + // go func() { + // ticker := time.NewTicker(30 * time.Second) + // defer ticker.Stop() - for range ticker.C { - if err := worker.AutoCommitExpiringCarts(); err != nil { - log.Printf("Auto-commit error: %v", err) - } - } - }() + // for range ticker.C { + // if err := worker.AutoCommitExpiringCarts(); err != nil { + // log.Printf("Auto-commit error: %v", err) + // } + // } + // }() app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { @@ -45,7 +43,6 @@ func main() { app.Use(cors.New()) - router.SetupRoutes(app) config.StartServer(app) } diff --git a/config/setup_config.go b/config/setup_config.go index b302514..0702601 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -2,17 +2,29 @@ package config import ( "log" + "os" "github.com/joho/godotenv" ) func SetupConfig() { - err := godotenv.Load(".env.dev") - if err != nil { - log.Fatalf("Error loading .env file: %v", err) - } + if _, exists := os.LookupEnv("DOCKER_ENV"); exists { + + log.Println("Running in Docker container, using environment variables") + } else { + + err := godotenv.Load(".env.dev") + if err != nil { + log.Printf("Warning: Error loading .env file: %v", err) + log.Println("Trying to use system environment variables...") + } else { + log.Println("Loaded environment from .env.dev file") + } + } ConnectDatabase() ConnectRedis() - InitWhatsApp() + go func() { + InitWhatsApp() // Ini tidak akan blocking startup server + }() } diff --git a/config/whatsapp.go b/config/whatsapp.go index 1ed9dca..aef3559 100644 --- a/config/whatsapp.go +++ b/config/whatsapp.go @@ -6,7 +6,9 @@ import ( "log" "os" "os/signal" + "sync" "syscall" + "time" _ "github.com/lib/pq" "github.com/mdp/qrterminal/v3" @@ -14,15 +16,64 @@ import ( "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" + "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" ) -var WhatsAppClient *whatsmeow.Client -var container *sqlstore.Container +type WhatsAppManager struct { + Client *whatsmeow.Client + container *sqlstore.Container + isConnected bool + mu sync.RWMutex + ctx context.Context + cancel context.CancelFunc + shutdownCh chan struct{} +} + +var ( + waManager *WhatsAppManager + once sync.Once +) + +func GetWhatsAppManager() *WhatsAppManager { + once.Do(func() { + ctx, cancel := context.WithCancel(context.Background()) + waManager = &WhatsAppManager{ + ctx: ctx, + cancel: cancel, + shutdownCh: make(chan struct{}), + } + }) + return waManager +} func InitWhatsApp() { - dbLog := waLog.Stdout("Database", "DEBUG", true) + manager := GetWhatsAppManager() + + log.Println("Initializing WhatsApp client...") + + if err := manager.setupDatabase(); err != nil { + log.Fatalf("Failed to setup WhatsApp database: %v", err) + } + + if err := manager.setupClient(); err != nil { + log.Fatalf("Failed to setup WhatsApp client: %v", err) + } + + if err := manager.handleAuthentication(); err != nil { + log.Fatalf("Failed to authenticate WhatsApp: %v", err) + } + + manager.setupEventHandlers() + + go manager.handleShutdown() + + log.Println("WhatsApp client initialized successfully and ready to send messages!") +} + +func (w *WhatsAppManager) setupDatabase() error { + dbLog := waLog.Stdout("WhatsApp-DB", "ERROR", true) dsn := fmt.Sprintf( "postgres://%s:%s@%s:%s/%s?sslmode=disable", @@ -34,114 +85,291 @@ func InitWhatsApp() { ) var err error - container, err = sqlstore.New("postgres", dsn, dbLog) + w.container, err = sqlstore.New("postgres", dsn, dbLog) if err != nil { - log.Fatalf("Failed to connect to WhatsApp database: %v", err) + return fmt.Errorf("failed to connect to database: %v", err) } - deviceStore, err := container.GetFirstDevice() + log.Println("WhatsApp database connection established") + return nil +} + +func (w *WhatsAppManager) setupClient() error { + deviceStore, err := w.container.GetFirstDevice() if err != nil { - log.Fatalf("Failed to get WhatsApp device: %v", err) + return fmt.Errorf("failed to get device store: %v", err) } - clientLog := waLog.Stdout("Client", "DEBUG", true) - WhatsAppClient = whatsmeow.NewClient(deviceStore, clientLog) + clientLog := waLog.Stdout("WhatsApp-Client", "ERROR", true) + w.Client = whatsmeow.NewClient(deviceStore, clientLog) - if WhatsAppClient.Store.ID == nil { - fmt.Println("WhatsApp Client is not logged in, generating QR Code...") + return nil +} - qrChan, _ := WhatsAppClient.GetQRChannel(context.Background()) - err = WhatsAppClient.Connect() - if err != nil { - log.Fatalf("Failed to connect WhatsApp client: %v", err) - } +func (w *WhatsAppManager) handleAuthentication() error { + if w.Client.Store.ID == nil { + log.Println("WhatsApp client not logged in, generating QR code...") + return w.authenticateWithQR() + } - for evt := range qrChan { - if evt.Event == "code" { - fmt.Println("QR Code untuk login:") + log.Println("WhatsApp client already logged in, connecting...") + return w.connect() +} + +func (w *WhatsAppManager) authenticateWithQR() error { + qrChan, err := w.Client.GetQRChannel(w.ctx) + if err != nil { + return fmt.Errorf("failed to get QR channel: %v", err) + } + + if err := w.Client.Connect(); err != nil { + return fmt.Errorf("failed to connect client: %v", err) + } + + qrTimeout := time.NewTimer(3 * time.Minute) + defer qrTimeout.Stop() + + for { + select { + case evt := <-qrChan: + switch evt.Event { + case "code": + fmt.Println("\n=== QR CODE UNTUK LOGIN WHATSAPP ===") generateQRCode(evt.Code) - } else { - fmt.Println("Login event:", evt.Event) + fmt.Println("Scan QR code di atas dengan WhatsApp Anda") + fmt.Println("QR code akan expired dalam 3 menit") + case "success": + log.Println("✅ WhatsApp login successful!") + w.setConnected(true) + return nil + case "timeout": + return fmt.Errorf("QR code expired, please restart") + default: + log.Printf("Login status: %s", evt.Event) } - } - } else { - fmt.Println("WhatsApp Client sudah login, langsung terhubung...") - err = WhatsAppClient.Connect() - if err != nil { - log.Fatalf("Failed to connect WhatsApp client: %v", err) + case <-qrTimeout.C: + return fmt.Errorf("QR code authentication timeout after 3 minutes") + case <-w.ctx.Done(): + return fmt.Errorf("authentication cancelled") } } +} - log.Println("WhatsApp client connected successfully!") - go handleShutdown() +func (w *WhatsAppManager) connect() error { + if err := w.Client.Connect(); err != nil { + return fmt.Errorf("failed to connect: %v", err) + } + + time.Sleep(2 * time.Second) + w.setConnected(true) + return nil +} + +func (w *WhatsAppManager) setupEventHandlers() { + w.Client.AddEventHandler(func(evt interface{}) { + switch v := evt.(type) { + case *events.Connected: + log.Println("✅ WhatsApp client connected") + w.setConnected(true) + case *events.Disconnected: + log.Println("❌ WhatsApp client disconnected") + w.setConnected(false) + case *events.LoggedOut: + log.Println("🚪 WhatsApp client logged out") + w.setConnected(false) + case *events.Message: + log.Printf("📨 Message received from %s", v.Info.Sender) + } + }) +} + +func (w *WhatsAppManager) setConnected(status bool) { + w.mu.Lock() + defer w.mu.Unlock() + w.isConnected = status +} + +func (w *WhatsAppManager) IsConnected() bool { + w.mu.RLock() + defer w.mu.RUnlock() + return w.isConnected } func generateQRCode(qrString string) { qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) } -func handleShutdown() { +func (w *WhatsAppManager) handleShutdown() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - <-sigChan + select { + case <-sigChan: + log.Println("Received shutdown signal...") + case <-w.ctx.Done(): + log.Println("Context cancelled...") + } + + w.shutdown() +} + +func (w *WhatsAppManager) shutdown() { log.Println("Shutting down WhatsApp client...") - WhatsAppClient.Disconnect() - os.Exit(0) + + w.cancel() + + if w.Client != nil { + w.Client.Disconnect() + } + + if w.container != nil { + w.container.Close() + } + + close(w.shutdownCh) + log.Println("WhatsApp client shutdown completed") } func SendWhatsAppMessage(phone, message string) error { - if WhatsAppClient == nil { + manager := GetWhatsAppManager() + + if manager.Client == nil { return fmt.Errorf("WhatsApp client is not initialized") } - targetJID, _ := types.ParseJID(phone + "@s.whatsapp.net") - msg := waE2E.Message{ + if !manager.IsConnected() { + return fmt.Errorf("WhatsApp client is not connected") + } + + if phone == "" || message == "" { + return fmt.Errorf("phone number and message cannot be empty") + } + + if phone[0] == '0' { + phone = "62" + phone[1:] // Convert 08xx menjadi 628xx + } + if phone[:2] != "62" { + phone = "62" + phone // Tambahkan 62 jika belum ada + } + + // Parse JID + targetJID, err := types.ParseJID(phone + "@s.whatsapp.net") + if err != nil { + return fmt.Errorf("invalid phone number format: %v", err) + } + + // Buat pesan + msg := &waE2E.Message{ Conversation: proto.String(message), } - _, err := WhatsAppClient.SendMessage(context.Background(), targetJID, &msg) + // Kirim dengan timeout + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := manager.Client.SendMessage(ctx, targetJID, msg) if err != nil { - return fmt.Errorf("failed to send WhatsApp message: %v", err) + return fmt.Errorf("failed to send message: %v", err) } - log.Printf("WhatsApp message sent successfully to: %s", phone) + log.Printf("✅ Message sent to %s (ID: %s)", phone, resp.ID) return nil } +// SendWhatsAppMessageBatch - Kirim pesan ke multiple nomor +func SendWhatsAppMessageBatch(phoneNumbers []string, message string) []error { + var errors []error + + for _, phone := range phoneNumbers { + if err := SendWhatsAppMessage(phone, message); err != nil { + errors = append(errors, fmt.Errorf("failed to send to %s: %v", phone, err)) + continue + } + + // Delay untuk menghindari rate limit + time.Sleep(1 * time.Second) + } + + return errors +} + +// GetWhatsAppStatus - Cek status koneksi +func GetWhatsAppStatus() map[string]interface{} { + manager := GetWhatsAppManager() + + status := map[string]interface{}{ + "initialized": manager.Client != nil, + "connected": manager.IsConnected(), + "logged_in": false, + "jid": "", + } + + if manager.Client != nil && manager.Client.Store.ID != nil { + status["logged_in"] = true + status["jid"] = manager.Client.Store.ID.String() + } + + return status +} + +// LogoutWhatsApp - Logout dan cleanup func LogoutWhatsApp() error { - if WhatsAppClient == nil { + manager := GetWhatsAppManager() + + if manager.Client == nil { return fmt.Errorf("WhatsApp client is not initialized") } - WhatsAppClient.Disconnect() + log.Println("Logging out WhatsApp...") - err := removeWhatsAppDeviceFromContainer() + // Logout + err := manager.Client.Logout() if err != nil { - return fmt.Errorf("failed to remove device from container: %v", err) + log.Printf("Warning: Failed to logout properly: %v", err) } - err = container.Close() - if err != nil { - return fmt.Errorf("failed to close database connection: %v", err) + // Disconnect + manager.Client.Disconnect() + manager.setConnected(false) + + // Hapus device dari store + if err := manager.removeDeviceFromStore(); err != nil { + log.Printf("Warning: Failed to remove device: %v", err) } - log.Println("WhatsApp client disconnected and session cleared successfully.") + // Close database + if manager.container != nil { + manager.container.Close() + } + + log.Println("✅ WhatsApp logout completed") return nil } -func removeWhatsAppDeviceFromContainer() error { - deviceStore, err := container.GetFirstDevice() +func (w *WhatsAppManager) removeDeviceFromStore() error { + deviceStore, err := w.container.GetFirstDevice() if err != nil { - return fmt.Errorf("failed to get WhatsApp device: %v", err) + return err } - if deviceStore != nil { - err := deviceStore.Delete() - if err != nil { - return fmt.Errorf("failed to remove device from store: %v", err) - } + if deviceStore != nil && deviceStore.ID != nil { + return deviceStore.Delete() } return nil } + +// IsValidPhoneNumber - Validasi format nomor telepon Indonesia +func IsValidPhoneNumber(phone string) bool { + // Minimal validasi untuk nomor Indonesia + if len(phone) < 10 || len(phone) > 15 { + return false + } + + // Cek awalan nomor Indonesia + if phone[:2] == "62" || phone[0] == '0' { + return true + } + + return false +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..e152c00 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,139 @@ +# docker-compose.dev.yml - Development environment dengan Air hot reload +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: rijig_postgres_dev + restart: unless-stopped + environment: + POSTGRES_DB: apirijig_v2 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pahmiadmin + PGDATA: /var/lib/postgresql/data/pgdata + ports: + - "5433:5432" + volumes: + - postgres_data_dev:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d + networks: + - rijig_network_dev + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d apirijig_v2"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # Redis Cache + redis: + image: redis:7-alpine + container_name: rijig_redis_dev + restart: unless-stopped + ports: + - "6378:6379" + volumes: + - redis_data_dev:/data + networks: + - rijig_network_dev + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 15s + + # Go Application dengan Air hot reload + app: + build: + context: . + dockerfile: Dockerfile.dev + container_name: rijig_app_dev + restart: unless-stopped + ports: + - "7000:7000" + environment: + # Docker Environment Flag + DOCKER_ENV: "true" + + # Base URL + BASE_URL: /apirijig/v2 + + # Server Settings + SERVER_HOST: 0.0.0.0 + SERVER_PORT: 7000 + + # Database Settings - menggunakan service name sebagai host + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: apirijig_v2 + DB_USER: postgres + DB_PASSWORD: pahmiadmin + + # Redis Settings - menggunakan service name sebagai host + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_PASSWORD: "" + REDIS_DB: 0 + + # Auth Keys + API_KEY: apirijikL0RH64wfkEpPqjAroLVPuFgT0EpsSLBPsmyUvIqZrUAi6X3HNPM7Vter + SECRET_KEY: TJ6h3vPMPlAuv7cbD27RU1/UyRctEih5k4H3+o7tZM1PSwTcoFETL6lqB54= + + # TTL Settings + ACCESS_TOKEN_EXPIRY: 23*time.Hour + REFRESH_TOKEN_EXPIRY: 28*24*time.Hour + PARTIAL_TOKEN_EXPIRY: 2*time.Hour + volumes: + # Mount source code untuk hot reload + - .:/app + # Exclude node_modules dan vendor (jika ada) + - /app/tmp + - /app/vendor + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - rijig_network_dev + + # pgAdmin (optional - untuk GUI database management) + pgadmin: + image: dpage/pgadmin4:latest + container_name: rijig_pgadmin_dev + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: admin@rijig.com + PGADMIN_DEFAULT_PASSWORD: admin123 + PGADMIN_CONFIG_SERVER_MODE: "False" + ports: + - "8080:80" + volumes: + - pgadmin_data_dev:/var/lib/pgadmin + depends_on: + - postgres + networks: + - rijig_network_dev + + # Redis Commander (optional - untuk GUI redis management) + redis-commander: + image: rediscommander/redis-commander:latest + container_name: rijig_redis_commander_dev + restart: unless-stopped + environment: + REDIS_HOSTS: local:redis:6379 + ports: + - "8081:8081" + depends_on: + - redis + networks: + - rijig_network_dev + +networks: + rijig_network_dev: + driver: bridge + +volumes: + postgres_data_dev: + redis_data_dev: + pgadmin_data_dev: diff --git a/dto/requestpickup_dto.go b/dto/requestpickup_dto.go index 01e7ad7..9ac69bd 100644 --- a/dto/requestpickup_dto.go +++ b/dto/requestpickup_dto.go @@ -38,7 +38,7 @@ type ResponseRequestPickup struct { type ResponseRequestPickupItem struct { ID string `json:"id,omitempty"` TrashCategoryID string `json:"trash_category_id,omitempty"` - TrashCategory []ResponseTrashCategoryDTO `json:"trash_category,omitempty"` + // TrashCategory []ResponseTrashCategoryDTO `json:"trash_category,omitempty"` EstimatedAmount float64 `json:"estimated_amount,omitempty"` } diff --git a/internal/about/about_handler.go b/internal/about/about_handler.go index 4ff94ae..ed2ed65 100644 --- a/internal/about/about_handler.go +++ b/internal/about/about_handler.go @@ -4,17 +4,16 @@ import ( "fmt" "log" "rijig/dto" - "rijig/internal/services" "rijig/utils" "github.com/gofiber/fiber/v2" ) type AboutHandler struct { - AboutService services.AboutService + AboutService AboutService } -func NewAboutHandler(aboutService services.AboutService) *AboutHandler { +func NewAboutHandler(aboutService AboutService) *AboutHandler { return &AboutHandler{ AboutService: aboutService, } @@ -36,7 +35,7 @@ func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { return utils.BadRequest(c, "Cover image is required") } - response, err := h.AboutService.CreateAbout(request, aboutCoverImage) + response, err := h.AboutService.CreateAbout(c.Context(), request, aboutCoverImage) if err != nil { log.Printf("Error creating About: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to create About: %v", err)) @@ -60,7 +59,7 @@ func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { return utils.BadRequest(c, "cover_image is required") } - response, err := h.AboutService.UpdateAbout(id, request, aboutCoverImage) + response, err := h.AboutService.UpdateAbout(c.Context(), id, request, aboutCoverImage) if err != nil { log.Printf("Error updating About: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to update About: %v", err)) @@ -70,7 +69,7 @@ func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { } func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error { - response, err := h.AboutService.GetAllAbout() + response, err := h.AboutService.GetAllAbout(c.Context()) if err != nil { log.Printf("Error fetching all About: %v", err) return utils.InternalServerError(c, "Failed to fetch About list") @@ -82,7 +81,7 @@ func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error { func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error { id := c.Params("id") - response, err := h.AboutService.GetAboutByID(id) + response, err := h.AboutService.GetAboutByID(c.Context(), id) if err != nil { log.Printf("Error fetching About by ID: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) @@ -94,7 +93,7 @@ func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error { func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error { id := c.Params("id") - response, err := h.AboutService.GetAboutDetailById(id) + response, err := h.AboutService.GetAboutDetailById(c.Context(), id) if err != nil { log.Printf("Error fetching About detail by ID: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) @@ -106,7 +105,7 @@ func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error { func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error { id := c.Params("id") - if err := h.AboutService.DeleteAbout(id); err != nil { + if err := h.AboutService.DeleteAbout(c.Context(), id); err != nil { log.Printf("Error deleting About: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to delete About: %v", err)) } @@ -132,7 +131,7 @@ func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { return utils.BadRequest(c, "image_detail is required") } - response, err := h.AboutService.CreateAboutDetail(request, aboutDetailImage) + response, err := h.AboutService.CreateAboutDetail(c.Context(), request, aboutDetailImage) if err != nil { log.Printf("Error creating AboutDetail: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to create AboutDetail: %v", err)) @@ -156,7 +155,7 @@ func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { return utils.BadRequest(c, "image_detail is required") } - response, err := h.AboutService.UpdateAboutDetail(id, request, aboutDetailImage) + response, err := h.AboutService.UpdateAboutDetail(c.Context(), id, request, aboutDetailImage) if err != nil { log.Printf("Error updating AboutDetail: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to update AboutDetail: %v", err)) @@ -168,7 +167,7 @@ func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error { id := c.Params("id") - if err := h.AboutService.DeleteAboutDetail(id); err != nil { + if err := h.AboutService.DeleteAboutDetail(c.Context(), id); err != nil { log.Printf("Error deleting AboutDetail: %v", err) return utils.InternalServerError(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err)) } diff --git a/internal/about/about_route.go b/internal/about/about_route.go index 5f6c9a3..03dfa0d 100644 --- a/internal/about/about_route.go +++ b/internal/about/about_route.go @@ -1 +1,32 @@ -package about \ No newline at end of file +package about + +import ( + "rijig/config" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +func AboutRouter(api fiber.Router) { + aboutRepo := NewAboutRepository(config.DB) + aboutService := NewAboutService(aboutRepo) + aboutHandler := NewAboutHandler(aboutService) + + aboutRoutes := api.Group("/about") + aboutRoutes.Use(middleware.AuthMiddleware()) + + aboutRoutes.Get("/", aboutHandler.GetAllAbout) + aboutRoutes.Get("/:id", aboutHandler.GetAboutByID) + aboutRoutes.Post("/", aboutHandler.CreateAbout) + aboutRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAbout) + aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) + + aboutDetailRoutes := api.Group("/about-detail") + aboutDetailRoutes.Use(middleware.AuthMiddleware()) + aboutDetailRoute := api.Group("/about-detail") + aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) + aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) + aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) + aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) +} diff --git a/internal/address/address_handler.go b/internal/address/address_handler.go index 0d5cc40..1c4eb44 100644 --- a/internal/address/address_handler.go +++ b/internal/address/address_handler.go @@ -1 +1,113 @@ -package address \ No newline at end of file +package address + +import ( + "rijig/dto" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type AddressHandler struct { + AddressService AddressService +} + +func NewAddressHandler(addressService AddressService) *AddressHandler { + return &AddressHandler{AddressService: addressService} +} + +func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error { + var request dto.CreateAddressDTO + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + if err := c.BodyParser(&request); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateAddress() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + addressResponse, err := h.AddressService.CreateAddress(c.Context(), claims.UserID, request) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.CreateSuccessWithData(c, "user address created successfully", addressResponse) +} + +func (h *AddressHandler) GetAddressByUserID(c *fiber.Ctx) error { + + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + addresses, err := h.AddressService.GetAddressByUserID(c.Context(), claims.UserID) + if err != nil { + return utils.NotFound(c, err.Error()) + } + + return utils.SuccessWithData(c, "User addresses fetched successfully", addresses) +} + +func (h *AddressHandler) GetAddressByID(c *fiber.Ctx) error { + + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + addressID := c.Params("address_id") + + address, err := h.AddressService.GetAddressByID(c.Context(), claims.UserID, addressID) + if err != nil { + return utils.NotFound(c, err.Error()) + } + + return utils.SuccessWithData(c, "Address fetched successfully", address) +} + +func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error { + + addressID := c.Params("address_id") + + var request dto.CreateAddressDTO + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + if err := c.BodyParser(&request); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateAddress() + if !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + updatedAddress, err := h.AddressService.UpdateAddress(c.Context(), claims.UserID, addressID, request) + if err != nil { + return utils.NotFound(c, err.Error()) + } + + return utils.SuccessWithData(c, "User address updated successfully", updatedAddress) +} + +func (h *AddressHandler) DeleteAddress(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + addressID := c.Params("address_id") + + err = h.AddressService.DeleteAddress(c.Context(), claims.UserID, addressID) + if err != nil { + return utils.Forbidden(c, err.Error()) + } + + return utils.Success(c, "Address deleted successfully") +} diff --git a/internal/address/address_route.go b/internal/address/address_route.go index 0d5cc40..1eb7dd4 100644 --- a/internal/address/address_route.go +++ b/internal/address/address_route.go @@ -1 +1,24 @@ -package address \ No newline at end of file +package address + +import ( + "rijig/config" + "rijig/internal/wilayahindo" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func AddressRouter(api fiber.Router) { + addressRepo := NewAddressRepository(config.DB) + wilayahRepo := wilayahindo.NewWilayahIndonesiaRepository(config.DB) + addressService := NewAddressService(addressRepo, wilayahRepo) + addressHandler := NewAddressHandler(addressService) + + adddressAPI := api.Group("/user/address") + + adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress) + adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID) + adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID) + adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress) + adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware(), addressHandler.DeleteAddress) +} diff --git a/internal/article/article_route.go b/internal/article/article_route.go index e2928e0..78179aa 100644 --- a/internal/article/article_route.go +++ b/internal/article/article_route.go @@ -2,9 +2,6 @@ package article import ( "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" "rijig/middleware" "rijig/utils" @@ -12,9 +9,9 @@ import ( ) func ArticleRouter(api fiber.Router) { - articleRepo := repositories.NewArticleRepository(config.DB) - articleService := services.NewArticleService(articleRepo) - articleHandler := handler.NewArticleHandler(articleService) + articleRepo := NewArticleRepository(config.DB) + articleService := NewArticleService(articleRepo) + articleHandler := NewArticleHandler(articleService) articleAPI := api.Group("/article") diff --git a/internal/cart/cart_dto.go b/internal/cart/cart_dto.go index 795236c..3c6dfc5 100644 --- a/internal/cart/cart_dto.go +++ b/internal/cart/cart_dto.go @@ -1 +1,49 @@ -package cart \ No newline at end of file +package cart + +import ( + "fmt" + "strings" +) + +type RequestCartItemDTO struct { + TrashID string `json:"trash_id"` + Amount float64 `json:"amount"` +} + +type RequestCartDTO struct { + CartItems []RequestCartItemDTO `json:"cart_items"` +} + +type CartResponse struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TotalAmount float64 `json:"total_amount"` + EstimatedTotalPrice float64 `json:"estimated_total_price"` + CartItems []CartItemResponse `json:"cart_items"` +} + +type CartItemResponse struct { + ID string `json:"id"` + TrashID string `json:"trash_id"` + TrashName string `json:"trash_name"` + TrashIcon string `json:"trash_icon"` + TrashPrice float64 `json:"trash_price"` + Amount float64 `json:"amount"` + SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"` +} + +func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { + errors := make(map[string][]string) + + for i, item := range r.CartItems { + if strings.TrimSpace(item.TrashID) == "" { + errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong") + } + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/cart/cart_repository.go b/internal/cart/cart_repository.go index 795236c..f68d1b2 100644 --- a/internal/cart/cart_repository.go +++ b/internal/cart/cart_repository.go @@ -1 +1,162 @@ -package cart \ No newline at end of file +package cart + +import ( + "context" + "errors" + "fmt" + + "rijig/config" + "rijig/model" + + "gorm.io/gorm" +) + +type CartRepository interface { + FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) + AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error + DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error + GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) + UpdateCartTotals(ctx context.Context, cartID string) error + DeleteCart(ctx context.Context, userID string) error + + CreateCartWithItems(ctx context.Context, cart *model.Cart) error + HasExistingCart(ctx context.Context, userID string) (bool, error) +} + +type cartRepository struct{} + +func NewCartRepository() CartRepository { + return &cartRepository{} +} + +func (r *cartRepository) FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) { + var cart model.Cart + db := config.DB.WithContext(ctx) + + err := db. + Preload("CartItems.TrashCategory"). + Where("user_id = ?", userID). + First(&cart).Error + + if err == nil { + return &cart, nil + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + newCart := model.Cart{ + UserID: userID, + TotalAmount: 0, + EstimatedTotalPrice: 0, + } + if err := db.Create(&newCart).Error; err != nil { + return nil, err + } + return &newCart, nil + } + + return nil, err +} + +func (r *cartRepository) AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error { + db := config.DB.WithContext(ctx) + + var item model.CartItem + err := db. + Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). + First(&item).Error + + if errors.Is(err, gorm.ErrRecordNotFound) { + newItem := model.CartItem{ + CartID: cartID, + TrashCategoryID: trashCategoryID, + Amount: amount, + SubTotalEstimatedPrice: amount * estimatedPrice, + } + return db.Create(&newItem).Error + } + + if err != nil { + return err + } + + item.Amount = amount + item.SubTotalEstimatedPrice = amount * estimatedPrice + return db.Save(&item).Error +} + +func (r *cartRepository) DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error { + db := config.DB.WithContext(ctx) + return db.Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). + Delete(&model.CartItem{}).Error +} + +func (r *cartRepository) GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) { + var cart model.Cart + db := config.DB.WithContext(ctx) + + err := db. + Preload("CartItems.TrashCategory"). + Where("user_id = ?", userID). + First(&cart).Error + + if err != nil { + return nil, err + } + return &cart, nil +} + +func (r *cartRepository) UpdateCartTotals(ctx context.Context, cartID string) error { + db := config.DB.WithContext(ctx) + + var items []model.CartItem + if err := db.Where("cart_id = ?", cartID).Find(&items).Error; err != nil { + return err + } + + var totalAmount float64 + var totalPrice float64 + + for _, item := range items { + totalAmount += item.Amount + totalPrice += item.SubTotalEstimatedPrice + } + + return db.Model(&model.Cart{}). + Where("id = ?", cartID). + Updates(map[string]interface{}{ + "total_amount": totalAmount, + "estimated_total_price": totalPrice, + }).Error +} + +func (r *cartRepository) DeleteCart(ctx context.Context, userID string) error { + db := config.DB.WithContext(ctx) + var cart model.Cart + if err := db.Where("user_id = ?", userID).First(&cart).Error; err != nil { + return err + } + return db.Delete(&cart).Error +} + +func (r *cartRepository) CreateCartWithItems(ctx context.Context, cart *model.Cart) error { + db := config.DB.WithContext(ctx) + + return db.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(cart).Error; err != nil { + return fmt.Errorf("failed to create cart: %w", err) + } + return nil + }) +} + +func (r *cartRepository) HasExistingCart(ctx context.Context, userID string) (bool, error) { + db := config.DB.WithContext(ctx) + + var count int64 + err := db.Model(&model.Cart{}).Where("user_id = ?", userID).Count(&count).Error + if err != nil { + return false, err + } + + return count > 0, nil +} diff --git a/internal/cart/cart_service.go b/internal/cart/cart_service.go index 795236c..6684252 100644 --- a/internal/cart/cart_service.go +++ b/internal/cart/cart_service.go @@ -1 +1,356 @@ -package cart \ No newline at end of file +package cart + +import ( + "context" + "fmt" + "log" + "time" + + "rijig/internal/trash" + "rijig/model" + "rijig/utils" + + "github.com/google/uuid" +) + +type CartService struct { + cartRepo CartRepository + trashRepo trash.TrashRepositoryInterface +} + +func NewCartService(cartRepo CartRepository, trashRepo trash.TrashRepositoryInterface) *CartService { + return &CartService{ + cartRepo: cartRepo, + trashRepo: trashRepo, + } +} + +func (s *CartService) AddToCart(ctx context.Context, userID, trashCategoryID string, amount float64) error { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil && err.Error() != "ErrCacheMiss" { + return fmt.Errorf("failed to get cart from cache: %w", err) + } + + if cartItems == nil { + cartItems = make(map[string]model.CartItem) + } + + trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, trashCategoryID) + if err != nil { + return fmt.Errorf("failed to get trash category: %w", err) + } + + cartItems[trashCategoryID] = model.CartItem{ + TrashCategoryID: trashCategoryID, + Amount: amount, + SubTotalEstimatedPrice: amount * float64(trashCategory.EstimatedPrice), + } + + return utils.SetCache(cartKey, cartItems, 24*time.Hour) +} + +func (s *CartService) RemoveFromCart(ctx context.Context, userID, trashCategoryID string) error { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil { + if err.Error() == "ErrCacheMiss" { + return nil + } + return fmt.Errorf("failed to get cart from cache: %w", err) + } + + delete(cartItems, trashCategoryID) + + if len(cartItems) == 0 { + return utils.DeleteCache(cartKey) + } + + return utils.SetCache(cartKey, cartItems, 24*time.Hour) +} + +func (s *CartService) ClearCart(userID string) error { + cartKey := fmt.Sprintf("cart:%s", userID) + return utils.DeleteCache(cartKey) +} + +func (s *CartService) GetCartFromRedis(ctx context.Context, userID string) (*CartResponse, error) { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil { + if err.Error() == "ErrCacheMiss" { + return &CartResponse{ + ID: "N/A", + UserID: userID, + TotalAmount: 0, + EstimatedTotalPrice: 0, + CartItems: []CartItemResponse{}, + }, nil + } + return nil, fmt.Errorf("failed to get cart from cache: %w", err) + } + + var totalAmount float64 + var estimatedTotal float64 + var cartItemDTOs []CartItemResponse + + for _, item := range cartItems { + trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID) + if err != nil { + log.Printf("Failed to get trash category %s: %v", item.TrashCategoryID, err) + continue + } + + totalAmount += item.Amount + estimatedTotal += item.SubTotalEstimatedPrice + + cartItemDTOs = append(cartItemDTOs, CartItemResponse{ + ID: uuid.NewString(), + TrashID: trashCategory.ID, + TrashName: trashCategory.Name, + TrashIcon: trashCategory.IconTrash, + TrashPrice: float64(trashCategory.EstimatedPrice), + Amount: item.Amount, + SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, + }) + } + + resp := &CartResponse{ + ID: "N/A", + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: estimatedTotal, + CartItems: cartItemDTOs, + } + + return resp, nil +} + +func (s *CartService) CommitCartToDatabase(ctx context.Context, userID string) error { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil { + if err.Error() == "ErrCacheMiss" { + log.Printf("No cart items found in Redis for user: %s", userID) + return fmt.Errorf("no cart items found") + } + return fmt.Errorf("failed to get cart from cache: %w", err) + } + + if len(cartItems) == 0 { + log.Printf("No items to commit for user: %s", userID) + return fmt.Errorf("no items to commit") + } + + hasCart, err := s.cartRepo.HasExistingCart(ctx, userID) + if err != nil { + return fmt.Errorf("failed to check existing cart: %w", err) + } + + var cart *model.Cart + if hasCart { + + cart, err = s.cartRepo.GetCartByUser(ctx, userID) + if err != nil { + return fmt.Errorf("failed to get existing cart: %w", err) + } + } else { + + cart, err = s.cartRepo.FindOrCreateCart(ctx, userID) + if err != nil { + return fmt.Errorf("failed to create cart: %w", err) + } + } + + for _, item := range cartItems { + trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID) + if err != nil { + log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) + continue + } + + err = s.cartRepo.AddOrUpdateCartItem( + ctx, + cart.ID, + item.TrashCategoryID, + item.Amount, + float64(trashCategory.EstimatedPrice), + ) + if err != nil { + log.Printf("Failed to add/update cart item: %v", err) + continue + } + } + + if err := s.cartRepo.UpdateCartTotals(ctx, cart.ID); err != nil { + return fmt.Errorf("failed to update cart totals: %w", err) + } + + if err := utils.DeleteCache(cartKey); err != nil { + log.Printf("Failed to clear Redis cart: %v", err) + } + + log.Printf("Cart committed successfully for user: %s", userID) + return nil +} + +func (s *CartService) GetCart(ctx context.Context, userID string) (*CartResponse, error) { + + cartRedis, err := s.GetCartFromRedis(ctx, userID) + if err == nil && len(cartRedis.CartItems) > 0 { + return cartRedis, nil + } + + cartDB, err := s.cartRepo.GetCartByUser(ctx, userID) + if err != nil { + + return &CartResponse{ + ID: "N/A", + UserID: userID, + TotalAmount: 0, + EstimatedTotalPrice: 0, + CartItems: []CartItemResponse{}, + }, nil + } + + var items []CartItemResponse + for _, item := range cartDB.CartItems { + items = append(items, CartItemResponse{ + ID: item.ID, + TrashID: item.TrashCategoryID, + TrashName: item.TrashCategory.Name, + TrashIcon: item.TrashCategory.IconTrash, + TrashPrice: float64(item.TrashCategory.EstimatedPrice), + Amount: item.Amount, + SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, + }) + } + + resp := &CartResponse{ + ID: cartDB.ID, + UserID: cartDB.UserID, + TotalAmount: cartDB.TotalAmount, + EstimatedTotalPrice: cartDB.EstimatedTotalPrice, + CartItems: items, + } + + return resp, nil +} + +func (s *CartService) SyncCartFromDatabaseToRedis(ctx context.Context, userID string) error { + + cartDB, err := s.cartRepo.GetCartByUser(ctx, userID) + if err != nil { + return fmt.Errorf("failed to get cart from database: %w", err) + } + + cartItems := make(map[string]model.CartItem) + for _, item := range cartDB.CartItems { + cartItems[item.TrashCategoryID] = model.CartItem{ + TrashCategoryID: item.TrashCategoryID, + Amount: item.Amount, + SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, + } + } + + cartKey := fmt.Sprintf("cart:%s", userID) + return utils.SetCache(cartKey, cartItems, 24*time.Hour) +} + +func (s *CartService) GetCartItemCount(userID string) (int, error) { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil { + if err.Error() == "ErrCacheMiss" { + return 0, nil + } + return 0, fmt.Errorf("failed to get cart from cache: %w", err) + } + + return len(cartItems), nil +} + +func (s *CartService) DeleteCart(ctx context.Context, userID string) error { + + cartKey := fmt.Sprintf("cart:%s", userID) + if err := utils.DeleteCache(cartKey); err != nil { + log.Printf("Failed to delete cart from Redis: %v", err) + } + + return s.cartRepo.DeleteCart(ctx, userID) +} + +func (s *CartService) UpdateCartWithDTO(ctx context.Context, userID string, cartDTO *RequestCartDTO) error { + + if errors, valid := cartDTO.ValidateRequestCartDTO(); !valid { + return fmt.Errorf("validation failed: %v", errors) + } + + cartKey := fmt.Sprintf("cart:%s", userID) + cartItems := make(map[string]model.CartItem) + + for _, itemDTO := range cartDTO.CartItems { + + trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID) + if err != nil { + log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err) + continue + } + + subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice) + + cartItems[itemDTO.TrashID] = model.CartItem{ + TrashCategoryID: itemDTO.TrashID, + Amount: itemDTO.Amount, + SubTotalEstimatedPrice: subtotal, + } + } + + return utils.SetCache(cartKey, cartItems, 24*time.Hour) +} + +func (s *CartService) AddItemsToCart(ctx context.Context, userID string, items []RequestCartItemDTO) error { + cartKey := fmt.Sprintf("cart:%s", userID) + + var cartItems map[string]model.CartItem + err := utils.GetCache(cartKey, &cartItems) + if err != nil && err.Error() != "ErrCacheMiss" { + return fmt.Errorf("failed to get cart from cache: %w", err) + } + + if cartItems == nil { + cartItems = make(map[string]model.CartItem) + } + + for _, itemDTO := range items { + if itemDTO.TrashID == "" { + continue + } + + trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID) + if err != nil { + log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err) + continue + } + + subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice) + + cartItems[itemDTO.TrashID] = model.CartItem{ + TrashCategoryID: itemDTO.TrashID, + Amount: itemDTO.Amount, + SubTotalEstimatedPrice: subtotal, + } + } + + return utils.SetCache(cartKey, cartItems, 24*time.Hour) +} diff --git a/internal/collector/collector_dto.go b/internal/collector/collector_dto.go index c87b2bd..f7a4db4 100644 --- a/internal/collector/collector_dto.go +++ b/internal/collector/collector_dto.go @@ -1 +1,252 @@ -package collector \ No newline at end of file +package collector + +import ( + "fmt" + "rijig/internal/address" + "rijig/internal/trash" + "strings" + "time" +) + +type CreateCollectorRequest struct { + UserID string `json:"user_id" binding:"required"` + JobStatus string `json:"job_status,omitempty"` + AddressID string `json:"address_id" binding:"required"` + AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items,omitempty"` +} + +type UpdateCollectorRequest struct { + JobStatus string `json:"job_status,omitempty"` + AddressID string `json:"address_id,omitempty"` + AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items,omitempty"` +} + +type CreateAvailableTrashRequest struct { + TrashCategoryID string `json:"trash_category_id" binding:"required"` + Price float32 `json:"price" binding:"required"` +} + +type UpdateAvailableTrashRequest struct { + ID string `json:"id,omitempty"` + TrashCategoryID string `json:"trash_category_id,omitempty"` + Price float32 `json:"price,omitempty"` +} + +type CollectorResponse struct { + ID string `json:"id"` + UserID string `json:"user_id"` + JobStatus string `json:"job_status"` + Rating float32 `json:"rating"` + AddressID string `json:"address_id"` + Address *address.AddressResponseDTO `json:"address,omitempty"` + AvailableTrash []AvailableTrashResponse `json:"available_trash"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type AvailableTrashResponse struct { + ID string `json:"id"` + CollectorID string `json:"collector_id"` + TrashCategoryID string `json:"trash_category_id"` + TrashCategory *trash.ResponseTrashCategoryDTO `json:"trash_category,omitempty"` + Price float32 `json:"price"` +} + +func (r *CreateCollectorRequest) ValidateCreateCollectorRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.UserID) == "" { + errors["user_id"] = append(errors["user_id"], "User ID tidak boleh kosong") + } + + if strings.TrimSpace(r.AddressID) == "" { + errors["address_id"] = append(errors["address_id"], "Address ID tidak boleh kosong") + } + + if r.JobStatus != "" { + r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus)) + if r.JobStatus != "active" && r.JobStatus != "inactive" { + errors["job_status"] = append(errors["job_status"], "Job status hanya boleh 'active' atau 'inactive'") + } + } else { + r.JobStatus = "inactive" + } + + if len(r.AvailableTrashItems) > 0 { + trashCategoryMap := make(map[string]bool) + for i, item := range r.AvailableTrashItems { + fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i) + + if strings.TrimSpace(item.TrashCategoryID) == "" { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong") + } else { + + if trashCategoryMap[item.TrashCategoryID] { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar") + } else { + trashCategoryMap[item.TrashCategoryID] = true + } + } + + if item.Price <= 0 { + errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0") + } + } + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func (r *UpdateCollectorRequest) ValidateUpdateCollectorRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.JobStatus != "" { + r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus)) + if r.JobStatus != "active" && r.JobStatus != "inactive" { + errors["job_status"] = append(errors["job_status"], "Job status hanya boleh 'active' atau 'inactive'") + } + } + + if r.AddressID != "" && strings.TrimSpace(r.AddressID) == "" { + errors["address_id"] = append(errors["address_id"], "Address ID tidak boleh kosong jika disediakan") + } + + if len(r.AvailableTrashItems) > 0 { + trashCategoryMap := make(map[string]bool) + for i, item := range r.AvailableTrashItems { + fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i) + + if strings.TrimSpace(item.TrashCategoryID) == "" { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong") + } else { + + if trashCategoryMap[item.TrashCategoryID] { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar") + } else { + trashCategoryMap[item.TrashCategoryID] = true + } + } + + if item.Price <= 0 { + errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0") + } + } + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func (r *CreateAvailableTrashRequest) ValidateCreateAvailableTrashRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.TrashCategoryID) == "" { + errors["trash_category_id"] = append(errors["trash_category_id"], "Trash category ID tidak boleh kosong") + } + + if r.Price <= 0 { + errors["price"] = append(errors["price"], "Harga harus lebih dari 0") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func (r *UpdateAvailableTrashRequest) ValidateUpdateAvailableTrashRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if r.TrashCategoryID != "" && strings.TrimSpace(r.TrashCategoryID) == "" { + errors["trash_category_id"] = append(errors["trash_category_id"], "Trash category ID tidak boleh kosong jika disediakan") + } + + if r.Price != 0 && r.Price <= 0 { + errors["price"] = append(errors["price"], "Harga harus lebih dari 0 jika disediakan") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} + +func (r *CreateCollectorRequest) IsValidJobStatus(status string) bool { + status = strings.ToLower(strings.TrimSpace(status)) + return status == "active" || status == "inactive" +} + +func (r *UpdateCollectorRequest) IsValidJobStatus(status string) bool { + status = strings.ToLower(strings.TrimSpace(status)) + return status == "active" || status == "inactive" +} + +func (r *CollectorResponse) FormatTimestamp(t time.Time) string { + return t.Format(time.RFC3339) +} + +func (r *CreateCollectorRequest) SetDefaults() { + if r.JobStatus == "" { + r.JobStatus = "inactive" + } else { + r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus)) + } +} + +func (r *UpdateCollectorRequest) NormalizeJobStatus() { + if r.JobStatus != "" { + r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus)) + } +} + +type BulkUpdateAvailableTrashRequest struct { + CollectorID string `json:"collector_id" binding:"required"` + AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items" binding:"required"` +} + +func (r *BulkUpdateAvailableTrashRequest) ValidateBulkUpdateAvailableTrashRequest() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CollectorID) == "" { + errors["collector_id"] = append(errors["collector_id"], "Collector ID tidak boleh kosong") + } + + if len(r.AvailableTrashItems) == 0 { + errors["available_trash_items"] = append(errors["available_trash_items"], "Minimal harus ada 1 item sampah") + } else { + trashCategoryMap := make(map[string]bool) + for i, item := range r.AvailableTrashItems { + fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i) + + if strings.TrimSpace(item.TrashCategoryID) == "" { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong") + } else { + + if trashCategoryMap[item.TrashCategoryID] { + errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar") + } else { + trashCategoryMap[item.TrashCategoryID] = true + } + } + + if item.Price <= 0 { + errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0") + } + } + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/collector/collector_handler.go b/internal/collector/collector_handler.go index c87b2bd..d9a23b1 100644 --- a/internal/collector/collector_handler.go +++ b/internal/collector/collector_handler.go @@ -1 +1,360 @@ -package collector \ No newline at end of file +package collector + +import ( + "rijig/middleware" + "rijig/utils" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +type CollectorHandler struct { + collectorService CollectorService +} + +func NewCollectorHandler(collectorService CollectorService) *CollectorHandler { + return &CollectorHandler{ + collectorService: collectorService, + } +} + +func (h *CollectorHandler) CreateCollector(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + var req CreateCollectorRequest + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + errors, isValid := req.ValidateCreateCollectorRequest() + if !isValid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + req.SetDefaults() + + collector, err := h.collectorService.CreateCollector(c.Context(), &req, claims.UserID) + if err != nil { + if strings.Contains(err.Error(), "already exists") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to create collector") + } + + return utils.CreateSuccessWithData(c, "Collector created successfully", collector) +} + +func (h *CollectorHandler) GetCollectorByID(c *fiber.Ctx) error { + id := c.Params("id") + if strings.TrimSpace(id) == "" { + return utils.BadRequest(c, "Collector ID is required") + } + + collector, err := h.collectorService.GetCollectorByID(c.Context(), id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to get collector") + } + + return utils.SuccessWithData(c, "Collector retrieved successfully", collector) +} + +func (h *CollectorHandler) GetCollectorByUserID(c *fiber.Ctx) error { + // userID := c.Params("userID") + // if strings.TrimSpace(userID) == "" { + // return utils.BadRequest(c, "User ID is required") + // } + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + collector, err := h.collectorService.GetCollectorByUserID(c.Context(), claims.UserID) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found for this user") + } + return utils.InternalServerError(c, "Failed to get collector") + } + + return utils.SuccessWithData(c, "Collector retrieved successfully", collector) +} + +func (h *CollectorHandler) UpdateCollector(c *fiber.Ctx) error { + /* id := c.Params("id") + if strings.TrimSpace(id) == "" { + return utils.BadRequest(c, "Collector ID is required") + } */ + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + var req UpdateCollectorRequest + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + errors, isValid := req.ValidateUpdateCollectorRequest() + if !isValid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + req.NormalizeJobStatus() + + collector, err := h.collectorService.UpdateCollector(c.Context(), claims.UserID, &req) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to update collector") + } + + return utils.SuccessWithData(c, "Collector updated successfully", collector) +} + +func (h *CollectorHandler) DeleteCollector(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + err = h.collectorService.DeleteCollector(c.Context(), claims.UserID) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to delete collector") + } + + return utils.Success(c, "Collector deleted successfully") +} + +func (h *CollectorHandler) ListCollectors(c *fiber.Ctx) error { + + limit, offset, page := h.parsePaginationParams(c) + + collectors, total, err := h.collectorService.ListCollectors(c.Context(), limit, offset) + if err != nil { + return utils.InternalServerError(c, "Failed to get collectors") + } + + responseData := map[string]interface{}{ + "collectors": collectors, + "total": total, + } + + return utils.SuccessWithPagination(c, "Collectors retrieved successfully", responseData, page, limit) +} + +func (h *CollectorHandler) GetActiveCollectors(c *fiber.Ctx) error { + + limit, offset, page := h.parsePaginationParams(c) + + collectors, total, err := h.collectorService.GetActiveCollectors(c.Context(), limit, offset) + if err != nil { + return utils.InternalServerError(c, "Failed to get active collectors") + } + + responseData := map[string]interface{}{ + "collectors": collectors, + "total": total, + } + + return utils.SuccessWithPagination(c, "Active collectors retrieved successfully", responseData, page, limit) +} + +func (h *CollectorHandler) GetCollectorsByAddress(c *fiber.Ctx) error { + addressID := c.Params("addressID") + if strings.TrimSpace(addressID) == "" { + return utils.BadRequest(c, "Address ID is required") + } + + limit, offset, page := h.parsePaginationParams(c) + + collectors, total, err := h.collectorService.GetCollectorsByAddress(c.Context(), addressID, limit, offset) + if err != nil { + return utils.InternalServerError(c, "Failed to get collectors by address") + } + + responseData := map[string]interface{}{ + "collectors": collectors, + "total": total, + "address_id": addressID, + } + + return utils.SuccessWithPagination(c, "Collectors by address retrieved successfully", responseData, page, limit) +} + +func (h *CollectorHandler) GetCollectorsByTrashCategory(c *fiber.Ctx) error { + trashCategoryID := c.Params("trashCategoryID") + if strings.TrimSpace(trashCategoryID) == "" { + return utils.BadRequest(c, "Trash category ID is required") + } + + limit, offset, page := h.parsePaginationParams(c) + + collectors, total, err := h.collectorService.GetCollectorsByTrashCategory(c.Context(), trashCategoryID, limit, offset) + if err != nil { + return utils.InternalServerError(c, "Failed to get collectors by trash category") + } + + responseData := map[string]interface{}{ + "collectors": collectors, + "total": total, + "trash_category_id": trashCategoryID, + } + + return utils.SuccessWithPagination(c, "Collectors by trash category retrieved successfully", responseData, page, limit) +} + +func (h *CollectorHandler) UpdateJobStatus(c *fiber.Ctx) error { + id := c.Params("id") + if strings.TrimSpace(id) == "" { + return utils.BadRequest(c, "Collector ID is required") + } + + var req struct { + JobStatus string `json:"job_status" binding:"required"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if strings.TrimSpace(req.JobStatus) == "" { + return utils.BadRequest(c, "Job status is required") + } + + jobStatus := strings.ToLower(strings.TrimSpace(req.JobStatus)) + validStatuses := []string{"active", "inactive", "busy"} + if !h.isValidJobStatus(jobStatus, validStatuses) { + return utils.BadRequest(c, "Invalid job status. Valid statuses: active, inactive, busy") + } + + err := h.collectorService.UpdateJobStatus(c.Context(), id, jobStatus) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to update job status") + } + + return utils.Success(c, "Job status updated successfully") +} + +func (h *CollectorHandler) UpdateRating(c *fiber.Ctx) error { + id := c.Params("id") + if strings.TrimSpace(id) == "" { + return utils.BadRequest(c, "Collector ID is required") + } + + var req struct { + Rating float32 `json:"rating" binding:"required"` + } + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if req.Rating < 1.0 || req.Rating > 5.0 { + return utils.BadRequest(c, "Rating must be between 1.0 and 5.0") + } + + err := h.collectorService.UpdateRating(c.Context(), id, req.Rating) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to update rating") + } + + return utils.Success(c, "Rating updated successfully") +} + +func (h *CollectorHandler) UpdateAvailableTrash(c *fiber.Ctx) error { + id := c.Params("id") + if strings.TrimSpace(id) == "" { + return utils.BadRequest(c, "Collector ID is required") + } + + var req BulkUpdateAvailableTrashRequest + req.CollectorID = id + + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + errors, isValid := req.ValidateBulkUpdateAvailableTrashRequest() + if !isValid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors) + } + + err := h.collectorService.UpdateAvailableTrash(c.Context(), id, req.AvailableTrashItems) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Collector not found") + } + return utils.InternalServerError(c, "Failed to update available trash") + } + + return utils.Success(c, "Available trash updated successfully") +} + +func (h *CollectorHandler) parsePaginationParams(c *fiber.Ctx) (limit, offset, page int) { + + limitStr := c.Query("limit", "10") + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + limit = 10 + } + if limit > 100 { + limit = 100 + } + + pageStr := c.Query("page", "1") + page, err = strconv.Atoi(pageStr) + if err != nil || page <= 0 { + page = 1 + } + + offset = (page - 1) * limit + + return limit, offset, page +} + +func (h *CollectorHandler) isValidJobStatus(status string, validStatuses []string) bool { + for _, validStatus := range validStatuses { + if status == validStatus { + return true + } + } + return false +} + +func (h *CollectorHandler) RegisterRoutes(app *fiber.App) { + + collectors := app.Group("/api/v1/collectors") + + collectors.Post("/", h.CreateCollector) + collectors.Get("/:id", h.GetCollectorByID) + collectors.Put("/:id", h.UpdateCollector) + collectors.Delete("/:id", h.DeleteCollector) + + collectors.Get("/", h.ListCollectors) + collectors.Get("/active", h.GetActiveCollectors) + collectors.Get("/user/:userID", h.GetCollectorByUserID) + collectors.Get("/address/:addressID", h.GetCollectorsByAddress) + collectors.Get("/trash-category/:trashCategoryID", h.GetCollectorsByTrashCategory) + + collectors.Patch("/:id/job-status", h.UpdateJobStatus) + collectors.Patch("/:id/rating", h.UpdateRating) + collectors.Put("/:id/available-trash", h.UpdateAvailableTrash) +} diff --git a/internal/collector/collector_repository.go b/internal/collector/collector_repository.go index c87b2bd..5bea6c9 100644 --- a/internal/collector/collector_repository.go +++ b/internal/collector/collector_repository.go @@ -1 +1,337 @@ -package collector \ No newline at end of file +package collector + +import ( + "context" + "fmt" + "rijig/model" + + "gorm.io/gorm" +) + +type CollectorRepository interface { + Create(ctx context.Context, collector *model.Collector) error + GetByID(ctx context.Context, id string) (*model.Collector, error) + GetByUserID(ctx context.Context, userID string) (*model.Collector, error) + Update(ctx context.Context, collector *model.Collector) error + Delete(ctx context.Context, UserID string) error + List(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) + + GetActiveCollectors(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) + GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*model.Collector, int64, error) + GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*model.Collector, int64, error) + UpdateJobStatus(ctx context.Context, id string, jobStatus string) error + UpdateRating(ctx context.Context, id string, rating float32) error + + CreateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error + GetAvailableTrashByCollectorID(ctx context.Context, collectorID string) ([]*model.AvaibleTrashByCollector, error) + UpdateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error + DeleteAvailableTrash(ctx context.Context, id string) error + BulkCreateAvailableTrash(ctx context.Context, availableTrashList []*model.AvaibleTrashByCollector) error + BulkUpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashList []*model.AvaibleTrashByCollector) error + DeleteAvailableTrashByCollectorID(ctx context.Context, collectorID string) error + + WithTx(tx *gorm.DB) CollectorRepository +} + +type collectorRepository struct { + db *gorm.DB +} + +func NewCollectorRepository(db *gorm.DB) CollectorRepository { + return &collectorRepository{ + db: db, + } +} + +func (r *collectorRepository) WithTx(tx *gorm.DB) CollectorRepository { + return &collectorRepository{ + db: tx, + } +} + +func (r *collectorRepository) Create(ctx context.Context, collector *model.Collector) error { + if err := r.db.WithContext(ctx).Create(collector).Error; err != nil { + return fmt.Errorf("failed to create collector: %w", err) + } + return nil +} + +func (r *collectorRepository) GetByID(ctx context.Context, id string) (*model.Collector, error) { + var collector model.Collector + + err := r.db.WithContext(ctx). + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Where("id = ?", id). + First(&collector).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("collector with id %s not found", id) + } + return nil, fmt.Errorf("failed to get collector by id: %w", err) + } + + return &collector, nil +} + +func (r *collectorRepository) GetByUserID(ctx context.Context, userID string) (*model.Collector, error) { + var collector model.Collector + + err := r.db.WithContext(ctx). + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Where("user_id = ?", userID). + First(&collector).Error + + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("collector with user_id %s not found", userID) + } + return nil, fmt.Errorf("failed to get collector by user_id: %w", err) + } + + return &collector, nil +} + +func (r *collectorRepository) Update(ctx context.Context, collector *model.Collector) error { + if err := r.db.WithContext(ctx).Save(collector).Error; err != nil { + return fmt.Errorf("failed to update collector: %w", err) + } + return nil +} + +func (r *collectorRepository) Delete(ctx context.Context, UserID string) error { + result := r.db.WithContext(ctx).Delete(&model.Collector{}, "user_id = ?", UserID) + if result.Error != nil { + return fmt.Errorf("failed to delete collector: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("collector with user_id %s not found", UserID) + } + + return nil +} + +func (r *collectorRepository) List(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) { + var collectors []*model.Collector + var total int64 + + if err := r.db.WithContext(ctx).Model(&model.Collector{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count collectors: %w", err) + } + + err := r.db.WithContext(ctx). + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Limit(limit). + Offset(offset). + Find(&collectors).Error + + if err != nil { + return nil, 0, fmt.Errorf("failed to list collectors: %w", err) + } + + return collectors, total, nil +} + +func (r *collectorRepository) GetActiveCollectors(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) { + var collectors []*model.Collector + var total int64 + + query := r.db.WithContext(ctx).Where("job_status = ?", "active") + + if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count active collectors: %w", err) + } + + err := query. + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Limit(limit). + Offset(offset). + Find(&collectors).Error + + if err != nil { + return nil, 0, fmt.Errorf("failed to get active collectors: %w", err) + } + + return collectors, total, nil +} + +func (r *collectorRepository) GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*model.Collector, int64, error) { + var collectors []*model.Collector + var total int64 + + query := r.db.WithContext(ctx).Where("address_id = ?", addressID) + + if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count collectors by address: %w", err) + } + + err := query. + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Limit(limit). + Offset(offset). + Find(&collectors).Error + + if err != nil { + return nil, 0, fmt.Errorf("failed to get collectors by address: %w", err) + } + + return collectors, total, nil +} + +func (r *collectorRepository) GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*model.Collector, int64, error) { + var collectors []*model.Collector + var total int64 + + subQuery := r.db.WithContext(ctx). + Table("avaible_trash_by_collectors"). + Select("collector_id"). + Where("trash_category_id = ?", trashCategoryID) + + query := r.db.WithContext(ctx). + Where("id IN (?)", subQuery) + + if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil { + return nil, 0, fmt.Errorf("failed to count collectors by trash category: %w", err) + } + + err := query. + Preload("Address"). + Preload("AvaibleTrashByCollector"). + Preload("AvaibleTrashByCollector.TrashCategory"). + Limit(limit). + Offset(offset). + Find(&collectors).Error + + if err != nil { + return nil, 0, fmt.Errorf("failed to get collectors by trash category: %w", err) + } + + return collectors, total, nil +} + +func (r *collectorRepository) UpdateJobStatus(ctx context.Context, id string, jobStatus string) error { + result := r.db.WithContext(ctx). + Model(&model.Collector{}). + Where("id = ?", id). + Update("job_status", jobStatus) + + if result.Error != nil { + return fmt.Errorf("failed to update job status: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("collector with id %s not found", id) + } + + return nil +} + +func (r *collectorRepository) UpdateRating(ctx context.Context, id string, rating float32) error { + result := r.db.WithContext(ctx). + Model(&model.Collector{}). + Where("id = ?", id). + Update("rating", rating) + + if result.Error != nil { + return fmt.Errorf("failed to update rating: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("collector with id %s not found", id) + } + + return nil +} + +func (r *collectorRepository) CreateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error { + if err := r.db.WithContext(ctx).Create(availableTrash).Error; err != nil { + return fmt.Errorf("failed to create available trash: %w", err) + } + return nil +} + +func (r *collectorRepository) GetAvailableTrashByCollectorID(ctx context.Context, collectorID string) ([]*model.AvaibleTrashByCollector, error) { + var availableTrash []*model.AvaibleTrashByCollector + + err := r.db.WithContext(ctx). + Preload("TrashCategory"). + Where("collector_id = ?", collectorID). + Find(&availableTrash).Error + + if err != nil { + return nil, fmt.Errorf("failed to get available trash by collector id: %w", err) + } + + return availableTrash, nil +} + +func (r *collectorRepository) UpdateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error { + if err := r.db.WithContext(ctx).Save(availableTrash).Error; err != nil { + return fmt.Errorf("failed to update available trash: %w", err) + } + return nil +} + +func (r *collectorRepository) DeleteAvailableTrash(ctx context.Context, id string) error { + result := r.db.WithContext(ctx).Delete(&model.AvaibleTrashByCollector{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete available trash: %w", result.Error) + } + + if result.RowsAffected == 0 { + return fmt.Errorf("available trash with id %s not found", id) + } + + return nil +} + +func (r *collectorRepository) BulkCreateAvailableTrash(ctx context.Context, availableTrashList []*model.AvaibleTrashByCollector) error { + if len(availableTrashList) == 0 { + return nil + } + + if err := r.db.WithContext(ctx).CreateInBatches(availableTrashList, 100).Error; err != nil { + return fmt.Errorf("failed to bulk create available trash: %w", err) + } + + return nil +} + +func (r *collectorRepository) BulkUpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashList []*model.AvaibleTrashByCollector) error { + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + + if err := tx.Where("collector_id = ?", collectorID).Delete(&model.AvaibleTrashByCollector{}).Error; err != nil { + return fmt.Errorf("failed to delete existing available trash: %w", err) + } + + if len(availableTrashList) > 0 { + for _, item := range availableTrashList { + item.CollectorID = collectorID + } + + if err := tx.CreateInBatches(availableTrashList, 100).Error; err != nil { + return fmt.Errorf("failed to create new available trash: %w", err) + } + } + + return nil + }) +} + +func (r *collectorRepository) DeleteAvailableTrashByCollectorID(ctx context.Context, collectorID string) error { + if err := r.db.WithContext(ctx).Where("collector_id = ?", collectorID).Delete(&model.AvaibleTrashByCollector{}).Error; err != nil { + return fmt.Errorf("failed to delete available trash by collector id: %w", err) + } + return nil +} diff --git a/internal/collector/collector_service.go b/internal/collector/collector_service.go index c87b2bd..b1776ea 100644 --- a/internal/collector/collector_service.go +++ b/internal/collector/collector_service.go @@ -1 +1,349 @@ -package collector \ No newline at end of file +package collector + +import ( + "context" + "fmt" + "rijig/internal/address" + "rijig/internal/trash" + "rijig/model" + "strings" + "time" + + "gorm.io/gorm" +) + +type CollectorService interface { + CreateCollector(ctx context.Context, req *CreateCollectorRequest, UserID string) (*CollectorResponse, error) + GetCollectorByID(ctx context.Context, id string) (*CollectorResponse, error) + GetCollectorByUserID(ctx context.Context, userID string) (*CollectorResponse, error) + UpdateCollector(ctx context.Context, UserID string, req *UpdateCollectorRequest) (*CollectorResponse, error) + DeleteCollector(ctx context.Context, UserID string) error + ListCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) + + GetActiveCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) + GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*CollectorResponse, int64, error) + GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*CollectorResponse, int64, error) + + UpdateJobStatus(ctx context.Context, id string, jobStatus string) error + UpdateRating(ctx context.Context, id string, rating float32) error + UpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashItems []CreateAvailableTrashRequest) error +} + +type collectorService struct { + collectorRepo CollectorRepository + db *gorm.DB +} + +func NewCollectorService(collectorRepo CollectorRepository, db *gorm.DB) CollectorService { + return &collectorService{ + collectorRepo: collectorRepo, + db: db, + } +} + +func (s *collectorService) CreateCollector(ctx context.Context, req *CreateCollectorRequest, UserID string) (*CollectorResponse, error) { + + existingCollector, err := s.collectorRepo.GetByUserID(ctx, UserID) + if err != nil && !strings.Contains(err.Error(), "not found") { + return nil, fmt.Errorf("failed to check existing collector: %w", err) + } + if existingCollector != nil { + return nil, fmt.Errorf("collector already exists for user_id: %s", req.UserID) + } + + collector := &model.Collector{ + UserID: UserID, + JobStatus: "inactive", + AddressID: req.AddressID, + Rating: 5.0, + } + + if req.JobStatus != "" { + collector.JobStatus = req.JobStatus + } + + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + collectorRepoTx := s.collectorRepo.WithTx(tx) + + if err := collectorRepoTx.Create(ctx, collector); err != nil { + return fmt.Errorf("failed to create collector: %w", err) + } + + if len(req.AvailableTrashItems) > 0 { + availableTrashList := s.buildAvailableTrashList(collector.ID, req.AvailableTrashItems) + if err := collectorRepoTx.BulkCreateAvailableTrash(ctx, availableTrashList); err != nil { + return fmt.Errorf("failed to create available trash items: %w", err) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + createdCollector, err := s.collectorRepo.GetByID(ctx, collector.ID) + if err != nil { + return nil, fmt.Errorf("failed to fetch created collector: %w", err) + } + + return s.toCollectorResponse(createdCollector), nil +} + +func (s *collectorService) GetCollectorByID(ctx context.Context, id string) (*CollectorResponse, error) { + collector, err := s.collectorRepo.GetByID(ctx, id) + if err != nil { + return nil, err + } + + return s.toCollectorResponse(collector), nil +} + +func (s *collectorService) GetCollectorByUserID(ctx context.Context, userID string) (*CollectorResponse, error) { + collector, err := s.collectorRepo.GetByUserID(ctx, userID) + if err != nil { + return nil, err + } + + return s.toCollectorResponse(collector), nil +} + +func (s *collectorService) UpdateCollector(ctx context.Context, UserID string, req *UpdateCollectorRequest) (*CollectorResponse, error) { + + collector, err := s.collectorRepo.GetByUserID(ctx, UserID) + if err != nil { + return nil, fmt.Errorf("failed to get collector: %w", err) + } + + needsUpdate := s.checkCollectorNeedsUpdate(collector, req) + + err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + collectorRepoTx := s.collectorRepo.WithTx(tx) + + if needsUpdate { + s.applyCollectorUpdates(collector, req) + collector.UpdatedAt = time.Now() + + if err := collectorRepoTx.Update(ctx, collector); err != nil { + return fmt.Errorf("failed to update collector: %w", err) + } + } + + if len(req.AvailableTrashItems) > 0 { + availableTrashList := s.buildAvailableTrashList(collector.ID, req.AvailableTrashItems) + if err := collectorRepoTx.BulkUpdateAvailableTrash(ctx, collector.ID, availableTrashList); err != nil { + return fmt.Errorf("failed to update available trash items: %w", err) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + updatedCollector, err := s.collectorRepo.GetByUserID(ctx, UserID) + if err != nil { + return nil, fmt.Errorf("failed to fetch updated collector: %w", err) + } + + return s.toCollectorResponse(updatedCollector), nil +} + +func (s *collectorService) DeleteCollector(ctx context.Context, UserID string) error { + + _, err := s.collectorRepo.GetByUserID(ctx, UserID) + if err != nil { + return fmt.Errorf("collector not found: %w", err) + } + + if err := s.collectorRepo.Delete(ctx, UserID); err != nil { + return fmt.Errorf("failed to delete collector: %w", err) + } + + return nil +} + +func (s *collectorService) ListCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) { + + limit, offset = s.normalizePagination(limit, offset) + + collectors, total, err := s.collectorRepo.List(ctx, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list collectors: %w", err) + } + + return s.buildCollectorResponseList(collectors), total, nil +} + +func (s *collectorService) GetActiveCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) { + + limit, offset = s.normalizePagination(limit, offset) + + collectors, total, err := s.collectorRepo.GetActiveCollectors(ctx, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get active collectors: %w", err) + } + + return s.buildCollectorResponseList(collectors), total, nil +} + +func (s *collectorService) GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*CollectorResponse, int64, error) { + + limit, offset = s.normalizePagination(limit, offset) + + collectors, total, err := s.collectorRepo.GetCollectorsByAddress(ctx, addressID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get collectors by address: %w", err) + } + + return s.buildCollectorResponseList(collectors), total, nil +} + +func (s *collectorService) GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*CollectorResponse, int64, error) { + + limit, offset = s.normalizePagination(limit, offset) + + collectors, total, err := s.collectorRepo.GetCollectorsByTrashCategory(ctx, trashCategoryID, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get collectors by trash category: %w", err) + } + + return s.buildCollectorResponseList(collectors), total, nil +} + +func (s *collectorService) UpdateJobStatus(ctx context.Context, id string, jobStatus string) error { + if err := s.collectorRepo.UpdateJobStatus(ctx, id, jobStatus); err != nil { + return fmt.Errorf("failed to update job status: %w", err) + } + + return nil +} + +func (s *collectorService) UpdateRating(ctx context.Context, id string, rating float32) error { + if err := s.collectorRepo.UpdateRating(ctx, id, rating); err != nil { + return fmt.Errorf("failed to update rating: %w", err) + } + + return nil +} + +func (s *collectorService) UpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashItems []CreateAvailableTrashRequest) error { + availableTrashList := s.buildAvailableTrashList(collectorID, availableTrashItems) + + if err := s.collectorRepo.BulkUpdateAvailableTrash(ctx, collectorID, availableTrashList); err != nil { + return fmt.Errorf("failed to update available trash: %w", err) + } + + return nil +} + +func (s *collectorService) buildAvailableTrashList(collectorID string, items []CreateAvailableTrashRequest) []*model.AvaibleTrashByCollector { + availableTrashList := make([]*model.AvaibleTrashByCollector, 0, len(items)) + for _, item := range items { + availableTrash := &model.AvaibleTrashByCollector{ + CollectorID: collectorID, + TrashCategoryID: item.TrashCategoryID, + Price: item.Price, + } + availableTrashList = append(availableTrashList, availableTrash) + } + return availableTrashList +} + +func (s *collectorService) checkCollectorNeedsUpdate(collector *model.Collector, req *UpdateCollectorRequest) bool { + if req.JobStatus != "" && req.JobStatus != collector.JobStatus { + return true + } + if req.AddressID != "" && req.AddressID != collector.AddressID { + return true + } + return false +} + +func (s *collectorService) applyCollectorUpdates(collector *model.Collector, req *UpdateCollectorRequest) { + if req.JobStatus != "" { + collector.JobStatus = req.JobStatus + } + if req.AddressID != "" { + collector.AddressID = req.AddressID + } +} + +func (s *collectorService) normalizePagination(limit, offset int) (int, int) { + if limit <= 0 { + limit = 10 + } + if offset < 0 { + offset = 0 + } + if limit > 100 { + limit = 100 + } + return limit, offset +} + +func (s *collectorService) buildCollectorResponseList(collectors []*model.Collector) []*CollectorResponse { + responses := make([]*CollectorResponse, 0, len(collectors)) + for _, collector := range collectors { + responses = append(responses, s.toCollectorResponse(collector)) + } + return responses +} + +func (s *collectorService) toCollectorResponse(collector *model.Collector) *CollectorResponse { + response := &CollectorResponse{ + ID: collector.ID, + UserID: collector.UserID, + JobStatus: collector.JobStatus, + Rating: collector.Rating, + AddressID: collector.AddressID, + AvailableTrash: make([]AvailableTrashResponse, 0), + CreatedAt: collector.CreatedAt.Format(time.RFC3339), + UpdatedAt: collector.UpdatedAt.Format(time.RFC3339), + } + + if collector.Address.ID != "" { + response.Address = &address.AddressResponseDTO{ + ID: collector.Address.ID, + UserID: collector.Address.UserID, + Province: collector.Address.Province, + Regency: collector.Address.Regency, + District: collector.Address.District, + Village: collector.Address.Village, + PostalCode: collector.Address.PostalCode, + Detail: collector.Address.Detail, + Latitude: collector.Address.Latitude, + Longitude: collector.Address.Longitude, + CreatedAt: collector.Address.CreatedAt.Format(time.RFC3339), + UpdatedAt: collector.Address.UpdatedAt.Format(time.RFC3339), + } + } + + for _, availableTrash := range collector.AvaibleTrashByCollector { + trashResponse := AvailableTrashResponse{ + ID: availableTrash.ID, + CollectorID: availableTrash.CollectorID, + TrashCategoryID: availableTrash.TrashCategoryID, + Price: availableTrash.Price, + } + + if availableTrash.TrashCategory.ID != "" { + trashResponse.TrashCategory = &trash.ResponseTrashCategoryDTO{ + ID: availableTrash.TrashCategory.ID, + TrashName: availableTrash.TrashCategory.Name, + TrashIcon: availableTrash.TrashCategory.IconTrash, + EstimatedPrice: availableTrash.TrashCategory.EstimatedPrice, + Variety: availableTrash.TrashCategory.Variety, + CreatedAt: availableTrash.TrashCategory.CreatedAt.Format(time.RFC3339), + UpdatedAt: availableTrash.TrashCategory.UpdatedAt.Format(time.RFC3339), + } + } + + response.AvailableTrash = append(response.AvailableTrash, trashResponse) + } + + return response +} diff --git a/internal/handler/about_handler.go b/internal/handler/about_handler.go deleted file mode 100644 index 92fca2c..0000000 --- a/internal/handler/about_handler.go +++ /dev/null @@ -1,176 +0,0 @@ -package handler -/* -import ( - "fmt" - "log" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AboutHandler struct { - AboutService services.AboutService -} - -func NewAboutHandler(aboutService services.AboutService) *AboutHandler { - return &AboutHandler{ - AboutService: aboutService, - } -} - -func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { - var request dto.RequestAboutDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing request body: %v", err) - return utils.ResponseErrorData(c, "Invalid input data") - } - - aboutCoverImage, err := c.FormFile("cover_image") - if err != nil { - log.Printf("Error retrieving cover image about from request: %v", err) - return utils.ErrorResponse(c, "cover_iamge is required") - } - - response, err := h.AboutService.CreateAbout(request, aboutCoverImage) - if err != nil { - log.Printf("Error creating About: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to create About: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully created About") -} - -func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { - id := c.Params("id") - - var request dto.RequestAboutDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing request body: %v", err) - return utils.ErrorResponse(c, "Invalid input data") - } - - aboutCoverImage, err := c.FormFile("cover_image") - if err != nil { - log.Printf("Error retrieving cover image about from request: %v", err) - return utils.ErrorResponse(c, "cover_iamge is required") - } - - response, err := h.AboutService.UpdateAbout(id, request, aboutCoverImage) - if err != nil { - log.Printf("Error updating About: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to update About: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully updated About") -} - -func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error { - - response, err := h.AboutService.GetAllAbout() - if err != nil { - log.Printf("Error fetching all About: %v", err) - return utils.ErrorResponse(c, "Failed to fetch About list") - } - - return utils.PaginatedResponse(c, response, 1, len(response), len(response), "Successfully fetched About list") -} - -func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error { - id := c.Params("id") - - response, err := h.AboutService.GetAboutByID(id) - if err != nil { - log.Printf("Error fetching About by ID: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully fetched About") -} - -func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error { - id := c.Params("id") - - response, err := h.AboutService.GetAboutDetailById(id) - if err != nil { - log.Printf("Error fetching About detail by ID: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch About by ID: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully fetched About") -} - -func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error { - id := c.Params("id") - - if err := h.AboutService.DeleteAbout(id); err != nil { - log.Printf("Error deleting About: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete About: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted About") -} - -func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { - var request dto.RequestAboutDetailDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing request body: %v", err) - return utils.ErrorResponse(c, "Invalid input data") - } - - errors, valid := request.ValidateAboutDetail() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - aboutDetailImage, err := c.FormFile("image_detail") - if err != nil { - log.Printf("Error retrieving image detail from request: %v", err) - return utils.ErrorResponse(c, "image_detail is required") - } - - response, err := h.AboutService.CreateAboutDetail(request, aboutDetailImage) - if err != nil { - log.Printf("Error creating AboutDetail: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to create AboutDetail: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully created AboutDetail") -} - -func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { - id := c.Params("id") - - var request dto.RequestAboutDetailDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing request body: %v", err) - return utils.ErrorResponse(c, "Invalid input data") - } - - aboutDetailImage, err := c.FormFile("image_detail") - if err != nil { - log.Printf("Error retrieving image detail from request: %v", err) - return utils.ErrorResponse(c, "image_detail is required") - } - - response, err := h.AboutService.UpdateAboutDetail(id, request, aboutDetailImage) - if err != nil { - log.Printf("Error updating AboutDetail: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to update AboutDetail: %v", err)) - } - - return utils.SuccessResponse(c, response, "Successfully updated AboutDetail") -} - -func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error { - id := c.Params("id") - - if err := h.AboutService.DeleteAboutDetail(id); err != nil { - log.Printf("Error deleting AboutDetail: %v", err) - return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Successfully deleted AboutDetail") -} - */ \ No newline at end of file diff --git a/internal/handler/address_handler.go b/internal/handler/address_handler.go deleted file mode 100644 index de69c47..0000000 --- a/internal/handler/address_handler.go +++ /dev/null @@ -1,93 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AddressHandler struct { - AddressService services.AddressService -} - -func NewAddressHandler(addressService services.AddressService) *AddressHandler { - return &AddressHandler{AddressService: addressService} -} - -func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error { - var requestAddressDTO dto.CreateAddressDTO - if err := c.BodyParser(&requestAddressDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := requestAddressDTO.ValidateAddress() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - addressResponse, err := h.AddressService.CreateAddress(c.Locals("userID").(string), requestAddressDTO) - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) - } - - return utils.CreateResponse(c, addressResponse, "user address created successfully") -} - -func (h *AddressHandler) GetAddressByUserID(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - addresses, err := h.AddressService.GetAddressByUserID(userID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, addresses, "User addresses fetched successfully") -} - -func (h *AddressHandler) GetAddressByID(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - addressID := c.Params("address_id") - - address, err := h.AddressService.GetAddressByID(userID, addressID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, address, "Address fetched successfully") -} - -func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - addressID := c.Params("address_id") - - var addressDTO dto.CreateAddressDTO - if err := c.BodyParser(&addressDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := addressDTO.ValidateAddress() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - updatedAddress, err := h.AddressService.UpdateAddress(userID, addressID, addressDTO) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, updatedAddress, "User address updated successfully") -} - -func (h *AddressHandler) DeleteAddress(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - addressID := c.Params("address_id") - - err := h.AddressService.DeleteAddress(userID, addressID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusForbidden, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Address deleted successfully") -} diff --git a/internal/handler/article_handler.go b/internal/handler/article_handler.go deleted file mode 100644 index 844e554..0000000 --- a/internal/handler/article_handler.go +++ /dev/null @@ -1,138 +0,0 @@ -package handler - -import ( - "fmt" - "mime/multipart" - "strconv" - - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type ArticleHandler struct { - ArticleService services.ArticleService -} - -func NewArticleHandler(articleService services.ArticleService) *ArticleHandler { - return &ArticleHandler{ArticleService: articleService} -} - -func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error { - var request dto.RequestArticleDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - coverImage, err := c.FormFile("coverImage") - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required") - } - - articleResponse, err := h.ArticleService.CreateArticle(request, coverImage) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.CreateResponse(c, articleResponse, "Article created successfully") -} - -func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error { - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil || page < 1 { - page = 0 - } - - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil || limit < 1 { - limit = 0 - } - - articles, totalArticles, err := h.ArticleService.GetAllArticles(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles") - } - - fmt.Printf("Total Articles: %d\n", totalArticles) - - if page == 0 && limit == 0 { - return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully") - } - - return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully") -} - -func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error { - id := c.Params("article_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required") - } - - article, err := h.ArticleService.GetArticleByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "Article not found") - } - - return utils.SuccessResponse(c, article, "Article fetched successfully") -} - -func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error { - id := c.Params("article_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required") - } - - var request dto.RequestArticleDTO - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - var coverImage *multipart.FileHeader - coverImage, err := c.FormFile("coverImage") - if err != nil && err.Error() != "no such file" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required") - } - - articleResponse, err := h.ArticleService.UpdateArticle(id, request, coverImage) - if err != nil { - if err.Error() == fmt.Sprintf("article with ID %s not found", id) { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - - } - - return utils.SuccessResponse(c, articleResponse, "Article updated successfully") -} - -func (h *ArticleHandler) DeleteArticle(c *fiber.Ctx) error { - id := c.Params("article_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required") - } - - err := h.ArticleService.DeleteArticle(id) - if err != nil { - - if err.Error() == fmt.Sprintf("article with ID %s not found", id) { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Article deleted successfully") -} diff --git a/internal/handler/auth/auth_admin_handler.go b/internal/handler/auth/auth_admin_handler.go deleted file mode 100644 index 3f3d78f..0000000 --- a/internal/handler/auth/auth_admin_handler.go +++ /dev/null @@ -1,81 +0,0 @@ -package handler -/* -import ( - "log" - dto "rijig/dto/auth" - services "rijig/internal/services/auth" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AuthAdminHandler struct { - UserService services.AuthAdminService -} - -func NewAuthAdminHandler(userService services.AuthAdminService) *AuthAdminHandler { - return &AuthAdminHandler{UserService: userService} -} - -func (h *AuthAdminHandler) RegisterAdmin(c *fiber.Ctx) error { - var request dto.RegisterAdminRequest - - if err := c.BodyParser(&request); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to parse request body") - } - - errors, valid := request.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - user, err := h.UserService.RegisterAdmin(&request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) - } - - return utils.SuccessResponse(c, user, "Admin registered successfully") -} - -func (h *AuthAdminHandler) LoginAdmin(c *fiber.Ctx) error { - var request dto.LoginAdminRequest - - if err := c.BodyParser(&request); err != nil { - return utils.InternalServerErrorResponse(c, "Failed to parse request body") - } - - loginResponse, err := h.UserService.LoginAdmin(&request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) - } - - return utils.SuccessResponse(c, loginResponse, "Login successful") -} - -func (h *AuthAdminHandler) LogoutAdmin(c *fiber.Ctx) error { - // Ambil userID dari c.Locals - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - log.Println("Error: UserID is nil or empty") - return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") - } - - // Ambil deviceID dari header atau c.Locals - deviceID, ok := c.Locals("device_id").(string) - if !ok || deviceID == "" { - log.Println("Error: DeviceID is nil or empty") - return utils.ErrorResponse(c, "DeviceID is required") - } - - log.Printf("UserID: %s, DeviceID: %s", userID, deviceID) - - err := h.UserService.LogoutAdmin(userID, deviceID) - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Successfully logged out") -} - - */ \ No newline at end of file diff --git a/internal/handler/auth/auth_masyarakat_handler.go b/internal/handler/auth/auth_masyarakat_handler.go deleted file mode 100644 index cf3750d..0000000 --- a/internal/handler/auth/auth_masyarakat_handler.go +++ /dev/null @@ -1,82 +0,0 @@ -package handler -/* -import ( - "log" - "rijig/dto" - services "rijig/internal/services/auth" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AuthMasyarakatHandler struct { - authMasyarakatService services.AuthMasyarakatService -} - -func NewAuthMasyarakatHandler(authMasyarakatService services.AuthMasyarakatService) *AuthMasyarakatHandler { - return &AuthMasyarakatHandler{authMasyarakatService} -} - -func (h *AuthMasyarakatHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { - var req dto.RegisterRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.Phone == "" { - return utils.ErrorResponse(c, "Phone number is required") - } - - if err := h.authMasyarakatService.RegisterOrLogin(&req); err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "OTP sent successfully") -} - -func (h *AuthMasyarakatHandler) VerifyOTPHandler(c *fiber.Ctx) error { - var req dto.VerifyOTPRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.OTP == "" { - return utils.ErrorResponse(c, "OTP is required") - } - - if req.DeviceID == "" { - return utils.ErrorResponse(c, "DeviceID is required") - } - - response, err := h.authMasyarakatService.VerifyOTP(&req) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, response, "Registration/Login successful") -} - -func (h *AuthMasyarakatHandler) LogoutHandler(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } - - deviceID, ok := c.Locals("device_id").(string) - if !ok || deviceID == "" { - log.Println("Error: DeviceID is nil or empty") - return utils.ErrorResponse(c, "DeviceID is required") - } - - err := h.authMasyarakatService.Logout(userID, deviceID) - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Logged out successfully") -} - */ \ No newline at end of file diff --git a/internal/handler/auth/auth_pengepul_handler.go b/internal/handler/auth/auth_pengepul_handler.go deleted file mode 100644 index f034ab2..0000000 --- a/internal/handler/auth/auth_pengepul_handler.go +++ /dev/null @@ -1,82 +0,0 @@ -package handler -/* -import ( - "log" - "rijig/dto" - services "rijig/internal/services/auth" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AuthPengepulHandler struct { - authPengepulService services.AuthMasyarakatService -} - -func NewAuthPengepulHandler(authPengepulService services.AuthMasyarakatService) *AuthPengepulHandler { - return &AuthPengepulHandler{authPengepulService} -} - -func (h *AuthPengepulHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { - var req dto.RegisterRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.Phone == "" { - return utils.ErrorResponse(c, "Phone number is required") - } - - if err := h.authPengepulService.RegisterOrLogin(&req); err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "OTP sent successfully") -} - -func (h *AuthPengepulHandler) VerifyOTPHandler(c *fiber.Ctx) error { - var req dto.VerifyOTPRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.OTP == "" { - return utils.ErrorResponse(c, "OTP is required") - } - - if req.DeviceID == "" { - return utils.ErrorResponse(c, "DeviceID is required") - } - - response, err := h.authPengepulService.VerifyOTP(&req) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, response, "Registration/Login successful") -} - -func (h *AuthPengepulHandler) LogoutHandler(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } - - deviceID, ok := c.Locals("device_id").(string) - if !ok || deviceID == "" { - log.Println("Error: DeviceID is nil or empty") - return utils.ErrorResponse(c, "DeviceID is required") - } - - err := h.authPengepulService.Logout(userID, deviceID) - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Logged out successfully") -} - */ \ No newline at end of file diff --git a/internal/handler/auth/auth_pnegelola_handler.go b/internal/handler/auth/auth_pnegelola_handler.go deleted file mode 100644 index 5382cf6..0000000 --- a/internal/handler/auth/auth_pnegelola_handler.go +++ /dev/null @@ -1,82 +0,0 @@ -package handler -/* -import ( - "log" - "rijig/dto" - services "rijig/internal/services/auth" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type AuthPengelolaHandler struct { - authPengelolaService services.AuthMasyarakatService -} - -func NewAuthPengelolaHandler(authPengelolaService services.AuthMasyarakatService) *AuthPengelolaHandler { - return &AuthPengelolaHandler{authPengelolaService} -} - -func (h *AuthPengelolaHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { - var req dto.RegisterRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.Phone == "" { - return utils.ErrorResponse(c, "Phone number is required") - } - - if err := h.authPengelolaService.RegisterOrLogin(&req); err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "OTP sent successfully") -} - -func (h *AuthPengelolaHandler) VerifyOTPHandler(c *fiber.Ctx) error { - var req dto.VerifyOTPRequest - - if err := c.BodyParser(&req); err != nil { - return utils.ErrorResponse(c, "Invalid request body") - } - - if req.OTP == "" { - return utils.ErrorResponse(c, "OTP is required") - } - - if req.DeviceID == "" { - return utils.ErrorResponse(c, "DeviceID is required") - } - - response, err := h.authPengelolaService.VerifyOTP(&req) - if err != nil { - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, response, "Registration/Login successful") -} - -func (h *AuthPengelolaHandler) LogoutHandler(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } - - deviceID, ok := c.Locals("device_id").(string) - if !ok || deviceID == "" { - log.Println("Error: DeviceID is nil or empty") - return utils.ErrorResponse(c, "DeviceID is required") - } - - err := h.authPengelolaService.Logout(userID, deviceID) - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Logged out successfully") -} - */ \ No newline at end of file diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go deleted file mode 100644 index 0782d02..0000000 --- a/internal/handler/auth_handler.go +++ /dev/null @@ -1,80 +0,0 @@ -package handler - -// import ( -// "log" -// "rijig/dto" -// "rijig/internal/services" -// "rijig/utils" - -// "github.com/gofiber/fiber/v2" -// ) - -// type AuthHandler struct { -// authService services.AuthService -// } - -// func NewAuthHandler(authService services.AuthService) *AuthHandler { -// return &AuthHandler{authService} -// } - -// func (h *AuthHandler) RegisterOrLoginHandler(c *fiber.Ctx) error { -// var req dto.RegisterRequest - -// if err := c.BodyParser(&req); err != nil { -// return utils.ErrorResponse(c, "Invalid request body") -// } - -// if req.Phone == "" || req.RoleID == "" { -// return utils.ErrorResponse(c, "Phone number and role ID are required") -// } - -// if err := h.authService.RegisterOrLogin(&req); err != nil { -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, nil, "OTP sent successfully") -// } - -// func (h *AuthHandler) VerifyOTPHandler(c *fiber.Ctx) error { -// var req dto.VerifyOTPRequest - -// if err := c.BodyParser(&req); err != nil { -// return utils.ErrorResponse(c, "Invalid request body") -// } - -// if req.OTP == "" { -// return utils.ErrorResponse(c, "OTP is required") -// } - -// response, err := h.authService.VerifyOTP(&req) -// if err != nil { -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, response, "Registration/Login successful") -// } - -// func (h *AuthHandler) LogoutHandler(c *fiber.Ctx) error { - -// userID, ok := c.Locals("userID").(string) -// if !ok || userID == "" { -// return utils.ErrorResponse(c, "User is not logged in or invalid session") -// } - -// phoneKey := "user_phone:" + userID -// phone, err := utils.GetStringData(phoneKey) -// if err != nil || phone == "" { - -// log.Printf("Error retrieving phone from Redis for user %s: %v", userID, err) -// return utils.ErrorResponse(c, "Phone number is missing or invalid session data") -// } - -// err = h.authService.Logout(userID, phone) -// if err != nil { - -// log.Printf("Error during logout process for user %s: %v", userID, err) -// return utils.ErrorResponse(c, err.Error()) -// } - -// return utils.SuccessResponse(c, nil, "Logged out successfully") -// } diff --git a/internal/handler/banner_handler.go b/internal/handler/banner_handler.go deleted file mode 100644 index 731fd1f..0000000 --- a/internal/handler/banner_handler.go +++ /dev/null @@ -1,109 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type BannerHandler struct { - BannerService services.BannerService -} - -func NewBannerHandler(bannerService services.BannerService) *BannerHandler { - return &BannerHandler{BannerService: bannerService} -} - -func (h *BannerHandler) CreateBanner(c *fiber.Ctx) error { - var request dto.RequestBannerDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.ValidateBannerInput() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - bannerImage, err := c.FormFile("bannerimage") - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required") - } - - bannerResponse, err := h.BannerService.CreateBanner(request, bannerImage) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.CreateResponse(c, bannerResponse, "Banner created successfully") -} - -func (h *BannerHandler) GetAllBanners(c *fiber.Ctx) error { - banners, err := h.BannerService.GetAllBanners() - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch banners") - } - - return utils.NonPaginatedResponse(c, banners, len(banners), "Banners fetched successfully") -} - -func (h *BannerHandler) GetBannerByID(c *fiber.Ctx) error { - id := c.Params("banner_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") - } - - banner, err := h.BannerService.GetBannerByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "invalid banner id") - } - - return utils.SuccessResponse(c, banner, "Banner fetched successfully") -} - -func (h *BannerHandler) UpdateBanner(c *fiber.Ctx) error { - id := c.Params("banner_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") - } - - var request dto.RequestBannerDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.ValidateBannerInput() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - bannerImage, err := c.FormFile("bannerimage") - if err != nil && err.Error() != "no such file" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required") - } - - bannerResponse, err := h.BannerService.UpdateBanner(id, request, bannerImage) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, bannerResponse, "Banner updated successfully") -} - -func (h *BannerHandler) DeleteBanner(c *fiber.Ctx) error { - id := c.Params("banner_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required") - } - - err := h.BannerService.DeleteBanner(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Banner deleted successfully") -} diff --git a/internal/handler/cart_handler.go b/internal/handler/cart_handler.go deleted file mode 100644 index 4dd7c25..0000000 --- a/internal/handler/cart_handler.go +++ /dev/null @@ -1,93 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CartHandler struct { - cartService services.CartService -} - -func NewCartHandler(cartService services.CartService) *CartHandler { - return &CartHandler{cartService: cartService} -} - -func (h *CartHandler) AddOrUpdateItem(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - var req dto.RequestCartItemDTO - - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "request": {"Payload tidak valid"}, - }) - } - - hasErrors, _ := req.Amount > 0 && req.TrashID != "", true - if !hasErrors { - errs := make(map[string][]string) - if req.Amount <= 0 { - errs["amount"] = append(errs["amount"], "Amount harus lebih dari 0") - } - if req.TrashID == "" { - errs["trash_id"] = append(errs["trash_id"], "Trash ID tidak boleh kosong") - } - return utils.ValidationErrorResponse(c, errs) - } - - if err := h.cartService.AddOrUpdateItem(c.Context(), userID, req); err != nil { - return utils.InternalServerErrorResponse(c, "Gagal menambahkan item ke keranjang") - } - - return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil ditambahkan ke keranjang") -} - -func (h *CartHandler) GetCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - cart, err := h.cartService.GetCart(c.Context(), userID) - if err != nil { - return utils.ErrorResponse(c, "Gagal mengambil data keranjang") - } - - return utils.SuccessResponse(c, cart, "Berhasil mengambil data keranjang") -} - -func (h *CartHandler) DeleteItem(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - trashID := c.Params("trash_id") - - if trashID == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Trash ID tidak boleh kosong") - } - - if err := h.cartService.DeleteItem(c.Context(), userID, trashID); err != nil { - return utils.InternalServerErrorResponse(c, "Gagal menghapus item dari keranjang") - } - - return utils.GenericResponse(c, fiber.StatusOK, "Item berhasil dihapus dari keranjang") -} - -func (h *CartHandler) Checkout(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - if err := h.cartService.Checkout(c.Context(), userID); err != nil { - return utils.InternalServerErrorResponse(c, "Gagal melakukan checkout keranjang") - } - - return utils.GenericResponse(c, fiber.StatusOK, "Checkout berhasil. Permintaan pickup telah dibuat.") -} - -func (h *CartHandler) ClearCart(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - err := h.cartService.ClearCart(c.Context(), userID) - if err != nil { - return utils.InternalServerErrorResponse(c, "Gagal menghapus keranjang") - } - - return utils.GenericResponse(c, fiber.StatusOK, "Keranjang berhasil dikosongkan") -} \ No newline at end of file diff --git a/internal/handler/collector_handler.go b/internal/handler/collector_handler.go deleted file mode 100644 index 25a73fb..0000000 --- a/internal/handler/collector_handler.go +++ /dev/null @@ -1,194 +0,0 @@ -package handler - -import ( - "context" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CollectorHandler interface { - CreateCollector(c *fiber.Ctx) error - AddTrashToCollector(c *fiber.Ctx) error - GetCollectorByID(c *fiber.Ctx) error - GetCollectorByUserID(c *fiber.Ctx) error - UpdateCollector(c *fiber.Ctx) error - UpdateJobStatus(c *fiber.Ctx) error - UpdateTrash(c *fiber.Ctx) error - DeleteTrash(c *fiber.Ctx) error -} -type collectorHandler struct { - service services.CollectorService -} - -func NewCollectorHandler(service services.CollectorService) CollectorHandler { - return &collectorHandler{service: service} -} - -func (h *collectorHandler) CreateCollector(c *fiber.Ctx) error { - var req dto.RequestCollectorDTO - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if errs, valid := req.ValidateRequestCollector(); !valid { - return utils.ValidationErrorResponse(c, errs) - } - - userID := c.Locals("userID").(string) - err := h.service.CreateCollector(context.Background(), userID, req) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.CreateResponse(c, nil, "Collector berhasil dibuat") -} - -func (h *collectorHandler) AddTrashToCollector(c *fiber.Ctx) error { - collectorID := c.Params("id") - var req dto.RequestAddAvaibleTrash - - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if errs, valid := req.ValidateRequestAddAvaibleTrash(); !valid { - return utils.ValidationErrorResponse(c, errs) - } - - err := h.service.AddTrashToCollector(context.Background(), collectorID, req) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Trash berhasil ditambahkan") -} - -func (h *collectorHandler) GetCollectorByID(c *fiber.Ctx) error { - collectorID := c.Params("id") - result, err := h.service.GetCollectorByID(context.Background(), collectorID) - if err != nil { - return utils.ErrorResponse(c, "Collector tidak ditemukan") - } - return utils.SuccessResponse(c, result, "Data collector berhasil diambil") -} -func (h *collectorHandler) GetCollectorByUserID(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - result, err := h.service.GetCollectorByUserID(context.Background(), userID) - if err != nil { - return utils.ErrorResponse(c, "Collector tidak ditemukan") - } - return utils.SuccessResponse(c, result, "Data collector berhasil diambil") -} - -func (h *collectorHandler) UpdateCollector(c *fiber.Ctx) error { - collectorID := c.Params("id") - var req struct { - JobStatus *string `json:"job_status"` - Rating float32 `json:"rating"` - AddressID string `json:"address_id"` - } - - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if req.AddressID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "address_id": {"tidak boleh kosong"}, - }) - } - - err := h.service.UpdateCollector(context.Background(), collectorID, req.JobStatus, req.Rating, req.AddressID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Collector berhasil diperbarui") -} - -func (h *collectorHandler) UpdateJobStatus(c *fiber.Ctx) error { - collectorID := c.Params("id") - var req struct { - JobStatus string `json:"job_status"` - } - - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if req.JobStatus != "active" && req.JobStatus != "inactive" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "job_status": {"harus bernilai 'active' atau 'inactive'"}, - }) - } - - err := h.service.UpdateCollector(c.Context(), collectorID, &req.JobStatus, 0, "") - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Status collector berhasil diperbarui") -} - -func (h *collectorHandler) UpdateTrash(c *fiber.Ctx) error { - collectorID := c.Params("id") - var req []dto.RequestAvaibleTrashbyCollector - - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - for i, t := range req { - if t.TrashId == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "trash_id": {t.TrashId, "trash_id tidak boleh kosong pada item ke " + string(rune(i))}, - }) - } - if t.TrashPrice <= 0 { - return utils.ValidationErrorResponse(c, map[string][]string{ - "trash_price": {"trash_price harus lebih dari 0 pada item ke " + string(rune(i))}, - }) - } - } - - err := h.service.UpdateAvaibleTrashByCollector(context.Background(), collectorID, req) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Trash berhasil diperbarui") -} - -func (h *collectorHandler) DeleteTrash(c *fiber.Ctx) error { - trashID := c.Params("id") - if trashID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "trash_id": {"tidak boleh kosong"}, - }) - } - - err := h.service.DeleteAvaibleTrash(context.Background(), trashID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Trash berhasil dihapus") -} diff --git a/internal/handler/company_profile_handler.go b/internal/handler/company_profile_handler.go deleted file mode 100644 index 2d8d7aa..0000000 --- a/internal/handler/company_profile_handler.go +++ /dev/null @@ -1,100 +0,0 @@ -package handler - -import ( - "fmt" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CompanyProfileHandler struct { - companyProfileService services.CompanyProfileService -} - -func NewCompanyProfileHandler(service services.CompanyProfileService) *CompanyProfileHandler { - return &CompanyProfileHandler{ - companyProfileService: service, - } -} - -func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - var requestDTO dto.RequestCompanyProfileDTO - if err := c.BodyParser(&requestDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid input data"}}) - } - - companyProfileResponse, err := h.companyProfileService.CreateCompanyProfile(userID, &requestDTO) - if err != nil { - return utils.ErrorResponse(c, fmt.Sprintf("Failed to create company profile: %v", err)) - } - - return utils.SuccessResponse(c, companyProfileResponse, "Company profile created successfully") -} - -func (h *CompanyProfileHandler) GetCompanyProfileByID(c *fiber.Ctx) error { - id := c.Params("company_id") - - companyProfileResponse, err := h.companyProfileService.GetCompanyProfileByID(id) - if err != nil { - return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch company profile: %v", err)) - } - - return utils.SuccessResponse(c, companyProfileResponse, "Company profile fetched successfully") -} - -func (h *CompanyProfileHandler) GetCompanyProfilesByUserID(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - companyProfilesResponse, err := h.companyProfileService.GetCompanyProfilesByUserID(userID) - if err != nil { - return utils.ErrorResponse(c, fmt.Sprintf("Failed to fetch company profiles: %v", err)) - } - - return utils.NonPaginatedResponse(c, companyProfilesResponse, len(companyProfilesResponse), "Company profiles fetched successfully") -} - -func (h *CompanyProfileHandler) UpdateCompanyProfile(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - id := c.Params("company_id") - - var requestDTO dto.RequestCompanyProfileDTO - if err := c.BodyParser(&requestDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid input data"}}) - } - - companyProfileResponse, err := h.companyProfileService.UpdateCompanyProfile(id, &requestDTO) - if err != nil { - return utils.ErrorResponse(c, fmt.Sprintf("Failed to update company profile: %v", err)) - } - - return utils.SuccessResponse(c, companyProfileResponse, "Company profile updated successfully") -} - -func (h *CompanyProfileHandler) DeleteCompanyProfile(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - id := c.Params("company_id") - - err := h.companyProfileService.DeleteCompanyProfile(id) - if err != nil { - return utils.ErrorResponse(c, fmt.Sprintf("Failed to delete company profile: %v", err)) - } - - return utils.SuccessResponse(c, nil, "Company profile deleted successfully") -} diff --git a/internal/handler/coveragearea_handler.go b/internal/handler/coveragearea_handler.go deleted file mode 100644 index 08cb17d..0000000 --- a/internal/handler/coveragearea_handler.go +++ /dev/null @@ -1,93 +0,0 @@ -package handler - -import ( - "fmt" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type CoverageAreaHandler struct { - service services.CoverageAreaService -} - -func NewCoverageAreaHandler(service services.CoverageAreaService) *CoverageAreaHandler { - return &CoverageAreaHandler{service: service} -} - -func (h *CoverageAreaHandler) CreateCoverageArea(c *fiber.Ctx) error { - var request dto.RequestCoverageArea - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"Invalid request body"}, - }) - } - - errors, valid := request.ValidateCoverageArea() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - response, err := h.service.CreateCoverageArea(request) - if err != nil { - return utils.InternalServerErrorResponse(c, fmt.Sprintf("Error creating coverage area: %v", err)) - } - - return utils.SuccessResponse(c, response, "Coverage area created successfully") -} - -func (h *CoverageAreaHandler) GetCoverageAreaByID(c *fiber.Ctx) error { - id := c.Params("id") - - response, err := h.service.GetCoverageAreaByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) - } - - return utils.SuccessResponse(c, response, "Coverage area found") -} - -func (h *CoverageAreaHandler) GetAllCoverageAreas(c *fiber.Ctx) error { - - response, err := h.service.GetAllCoverageAreas() - if err != nil { - return utils.InternalServerErrorResponse(c, "Error fetching coverage areas") - } - - return utils.SuccessResponse(c, response, "Coverage areas fetched successfully") -} - -func (h *CoverageAreaHandler) UpdateCoverageArea(c *fiber.Ctx) error { - id := c.Params("id") - var request dto.RequestCoverageArea - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"Invalid request body"}, - }) - } - - errors, valid := request.ValidateCoverageArea() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - response, err := h.service.UpdateCoverageArea(id, request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) - } - - return utils.SuccessResponse(c, response, "Coverage area updated successfully") -} - -func (h *CoverageAreaHandler) DeleteCoverageArea(c *fiber.Ctx) error { - id := c.Params("id") - - err := h.service.DeleteCoverageArea(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, fmt.Sprintf("Coverage area with ID %s not found", id)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Coverage area deleted successfully") -} diff --git a/internal/handler/identitycard_handler.go b/internal/handler/identitycard_handler.go deleted file mode 100644 index b05ba95..0000000 --- a/internal/handler/identitycard_handler.go +++ /dev/null @@ -1,134 +0,0 @@ -package handler - -import ( - "log" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type IdentityCardHandler struct { - IdentityCardService services.IdentityCardService -} - -func NewIdentityCardHandler(identityCardService services.IdentityCardService) *IdentityCardHandler { - return &IdentityCardHandler{ - IdentityCardService: identityCardService, - } -} - -func (h *IdentityCardHandler) CreateIdentityCard(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") - } - - var request dto.RequestIdentityCardDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing body: %v", err) - return utils.ErrorResponse(c, "Invalid request data") - } - - cardPhoto, err := c.FormFile("cardphoto") - if err != nil { - log.Printf("Error retrieving card photo from request: %v", err) - return utils.ErrorResponse(c, "Card photo is required") - } - - identityCard, err := h.IdentityCardService.CreateIdentityCard(userID, &request, cardPhoto) - if err != nil { - log.Printf("Error creating identity card: %v", err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.CreateResponse(c, identityCard, "Identity card created successfully") -} - -func (h *IdentityCardHandler) UpdateIdentityCard(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") - } - - id := c.Params("identity_id") - if id == "" { - return utils.ErrorResponse(c, "Identity card ID is required") - } - - var request dto.RequestIdentityCardDTO - if err := c.BodyParser(&request); err != nil { - log.Printf("Error parsing body: %v", err) - return utils.ErrorResponse(c, "Invalid request data") - } - - cardPhoto, err := c.FormFile("cardphoto") - if err != nil && err.Error() != "File not found" { - log.Printf("Error retrieving card photo: %v", err) - return utils.ErrorResponse(c, "Card photo is required") - } - - updatedCard, err := h.IdentityCardService.UpdateIdentityCard(userID, id, &request, cardPhoto) - if err != nil { - log.Printf("Error updating identity card: %v", err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, updatedCard, "Identity card updated successfully") -} - -func (h *IdentityCardHandler) GetIdentityCardById(c *fiber.Ctx) error { - - id := c.Params("identity_id") - if id == "" { - return utils.ErrorResponse(c, "Identity card ID is required") - } - - identityCard, err := h.IdentityCardService.GetIdentityCardByID(id) - if err != nil { - log.Printf("Error retrieving identity card: %v", err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, identityCard, "Identity card retrieved successfully") -} - -func (h *IdentityCardHandler) GetIdentityCard(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - identityCard, err := h.IdentityCardService.GetIdentityCardsByUserID(userID) - if err != nil { - log.Printf("Error retrieving identity card: %v", err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, identityCard, "Identity card retrieved successfully") -} - -func (h *IdentityCardHandler) DeleteIdentityCard(c *fiber.Ctx) error { - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "User not authenticated") - } - - id := c.Params("identity_id") - if id == "" { - return utils.ErrorResponse(c, "Identity card ID is required") - } - - err := h.IdentityCardService.DeleteIdentityCard(id) - if err != nil { - log.Printf("Error deleting identity card: %v", err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Identity card deleted successfully") -} diff --git a/internal/handler/initialcoint_handler.go b/internal/handler/initialcoint_handler.go deleted file mode 100644 index c80b466..0000000 --- a/internal/handler/initialcoint_handler.go +++ /dev/null @@ -1,99 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type InitialCointHandler struct { - InitialCointService services.InitialCointService -} - -func NewInitialCointHandler(initialCointService services.InitialCointService) *InitialCointHandler { - return &InitialCointHandler{InitialCointService: initialCointService} -} - -func (h *InitialCointHandler) CreateInitialCoint(c *fiber.Ctx) error { - var request dto.RequestInitialCointDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.ValidateCointInput() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - initialCointResponse, err := h.InitialCointService.CreateInitialCoint(request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.CreateResponse(c, initialCointResponse, "Initial coint created successfully") -} - -func (h *InitialCointHandler) GetAllInitialCoints(c *fiber.Ctx) error { - initialCoints, err := h.InitialCointService.GetAllInitialCoints() - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch initial coints") - } - - return utils.NonPaginatedResponse(c, initialCoints, len(initialCoints), "Initial coints fetched successfully") -} - -func (h *InitialCointHandler) GetInitialCointByID(c *fiber.Ctx) error { - id := c.Params("coin_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") - } - - initialCoint, err := h.InitialCointService.GetInitialCointByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "Invalid coin ID") - } - - return utils.SuccessResponse(c, initialCoint, "Initial coint fetched successfully") -} - -func (h *InitialCointHandler) UpdateInitialCoint(c *fiber.Ctx) error { - id := c.Params("coin_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") - } - - var request dto.RequestInitialCointDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.ValidateCointInput() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - initialCointResponse, err := h.InitialCointService.UpdateInitialCoint(id, request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, initialCointResponse, "Initial coint updated successfully") -} - -func (h *InitialCointHandler) DeleteInitialCoint(c *fiber.Ctx) error { - id := c.Params("coin_id") - if id == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") - } - - err := h.InitialCointService.DeleteInitialCoint(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Initial coint deleted successfully") -} diff --git a/internal/handler/pickup_history_handler.go b/internal/handler/pickup_history_handler.go deleted file mode 100644 index 2525f44..0000000 --- a/internal/handler/pickup_history_handler.go +++ /dev/null @@ -1,37 +0,0 @@ -package handler - -import ( - "context" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type PickupStatusHistoryHandler interface { - GetStatusHistory(c *fiber.Ctx) error -} - -type pickupStatusHistoryHandler struct { - service services.PickupStatusHistoryService -} - -func NewPickupStatusHistoryHandler(service services.PickupStatusHistoryService) PickupStatusHistoryHandler { - return &pickupStatusHistoryHandler{service: service} -} - -func (h *pickupStatusHistoryHandler) GetStatusHistory(c *fiber.Ctx) error { - pickupID := c.Params("id") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID tidak boleh kosong"}, - }) - } - - histories, err := h.service.GetStatusHistory(context.Background(), pickupID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, histories, "Riwayat status pickup berhasil diambil") -} diff --git a/internal/handler/pickup_matching_handler.go b/internal/handler/pickup_matching_handler.go deleted file mode 100644 index ecb39d9..0000000 --- a/internal/handler/pickup_matching_handler.go +++ /dev/null @@ -1,49 +0,0 @@ -package handler - -import ( - "context" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type PickupMatchingHandler interface { - GetNearbyCollectorsForPickup(c *fiber.Ctx) error - GetAvailablePickupForCollector(c *fiber.Ctx) error -} - -type pickupMatchingHandler struct { - service services.PickupMatchingService -} - -func NewPickupMatchingHandler(service services.PickupMatchingService) PickupMatchingHandler { - return &pickupMatchingHandler{service: service} -} - -func (h *pickupMatchingHandler) GetNearbyCollectorsForPickup(c *fiber.Ctx) error { - pickupID := c.Params("pickupID") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID harus disertakan"}, - }) - } - - collectors, err := h.service.FindNearbyCollectorsForPickup(context.Background(), pickupID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, collectors, "Data collector terdekat berhasil diambil") -} - -func (h *pickupMatchingHandler) GetAvailablePickupForCollector(c *fiber.Ctx) error { - collectorID := c.Locals("userID").(string) - - pickups, err := h.service.FindAvailableRequestsForCollector(context.Background(), collectorID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, pickups, "Data request pickup otomatis berhasil diambil") -} diff --git a/internal/handler/product_handler.go b/internal/handler/product_handler.go deleted file mode 100644 index 5c0eccf..0000000 --- a/internal/handler/product_handler.go +++ /dev/null @@ -1,227 +0,0 @@ -package handler - -import ( - "fmt" - "log" - "strconv" - - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type ProductHandler struct { - ProductService services.ProductService -} - -func NewProductHandler(productService services.ProductService) *ProductHandler { - return &ProductHandler{ProductService: productService} -} - -func ConvertStringToInt(value string) (int, error) { - convertedValue, err := strconv.Atoi(value) - if err != nil { - return 0, fmt.Errorf("invalid integer format: %s", value) - } - return convertedValue, nil -} - -func GetPaginationParams(c *fiber.Ctx) (int, int, error) { - pageStr := c.Query("page", "1") - limitStr := c.Query("limit", "50") - - page, err := strconv.Atoi(pageStr) - if err != nil || page <= 0 { - return 0, 0, fmt.Errorf("invalid page value") - } - - limit, err := strconv.Atoi(limitStr) - if err != nil || limit <= 0 { - return 0, 0, fmt.Errorf("invalid limit value") - } - - return page, limit, nil -} - -func (h *ProductHandler) CreateProduct(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") - } - - productName := c.FormValue("product_name") - quantityStr := c.FormValue("quantity") - productImages, err := c.MultipartForm() - if err != nil { - log.Printf("Error parsing form data: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing form data") - } - - quantity, err := ConvertStringToInt(quantityStr) - if err != nil { - log.Printf("Invalid quantity: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid quantity") - } - - productDTO := dto.RequestProductDTO{ - ProductName: productName, - Quantity: quantity, - ProductImages: productImages.File["product_image"], - } - - product, err := h.ProductService.CreateProduct(userID, &productDTO) - if err != nil { - log.Printf("Error creating product: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) - } - - return utils.CreateResponse(c, product, "Product created successfully") -} - -func (h *ProductHandler) GetAllProductsByStoreID(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") - } - - page, limit, err := GetPaginationParams(c) - if err != nil { - log.Printf("Invalid pagination params: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid pagination parameters") - } - - products, total, err := h.ProductService.GetAllProductsByStoreID(userID, page, limit) - if err != nil { - log.Printf("Error fetching products: %v", err) - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.PaginatedResponse(c, products, page, limit, int(total), "Products fetched successfully") -} - -func (h *ProductHandler) GetProductByID(c *fiber.Ctx) error { - - productID := c.Params("product_id") - if productID == "" { - log.Println("Product ID is required") - return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") - } - - product, err := h.ProductService.GetProductByID(productID) - if err != nil { - log.Printf("Error fetching product: %v", err) - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, product, "Product fetched successfully") -} - -func (h *ProductHandler) UpdateProduct(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") - } - - productID := c.Params("product_id") - if productID == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") - } - - var productDTO dto.RequestProductDTO - if err := c.BodyParser(&productDTO); err != nil { - log.Printf("Error parsing body: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") - } - - productImages, err := c.MultipartForm() - if err != nil { - log.Printf("Error parsing form data: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Error parsing form data") - } - - productDTO.ProductImages = productImages.File["product_images"] - - product, err := h.ProductService.UpdateProduct(userID, productID, &productDTO) - if err != nil { - log.Printf("Error updating product: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) - } - - return utils.CreateResponse(c, product, "Product updated successfully") -} - -func (h *ProductHandler) DeleteProduct(c *fiber.Ctx) error { - productID := c.Params("product_id") - if productID == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Product ID is required") - } - - err := h.ProductService.DeleteProduct(productID) - if err != nil { - log.Printf("Error deleting product: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Product deleted successfully") -} - -func (h *ProductHandler) DeleteProducts(c *fiber.Ctx) error { - var productIDs []string - if err := c.BodyParser(&productIDs); err != nil { - log.Printf("Error parsing product IDs: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid input for product IDs") - } - - if len(productIDs) == 0 { - return utils.GenericResponse(c, fiber.StatusBadRequest, "No product IDs provided") - } - - err := h.ProductService.DeleteProducts(productIDs) - if err != nil { - log.Printf("Error deleting products: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete products: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Products deleted successfully") -} - -func (h *ProductHandler) DeleteProductImage(c *fiber.Ctx) error { - imageID := c.Params("image_id") - if imageID == "" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Image ID is required") - } - - err := h.ProductService.DeleteProductImage(imageID) - if err != nil { - log.Printf("Error deleting product image: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product image: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Product image deleted successfully") -} - -func (h *ProductHandler) DeleteProductImages(c *fiber.Ctx) error { - var imageIDs []string - if err := c.BodyParser(&imageIDs); err != nil { - log.Printf("Error parsing image IDs: %v", err) - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid input for image IDs") - } - - if len(imageIDs) == 0 { - return utils.GenericResponse(c, fiber.StatusBadRequest, "No image IDs provided") - } - - err := h.ProductService.DeleteProductImages(imageIDs) - if err != nil { - log.Printf("Error deleting product images: %v", err) - return utils.GenericResponse(c, fiber.StatusConflict, fmt.Sprintf("Failed to delete product images: %v", err)) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Product images deleted successfully") -} diff --git a/internal/handler/rating_handler.go b/internal/handler/rating_handler.go deleted file mode 100644 index 6f64916..0000000 --- a/internal/handler/rating_handler.go +++ /dev/null @@ -1,66 +0,0 @@ -package handler - -import ( - "context" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type PickupRatingHandler interface { - CreateRating(c *fiber.Ctx) error - GetRatingsByCollector(c *fiber.Ctx) error - GetAverageRating(c *fiber.Ctx) error -} - -type pickupRatingHandler struct { - service services.PickupRatingService -} - -func NewPickupRatingHandler(service services.PickupRatingService) PickupRatingHandler { - return &pickupRatingHandler{service: service} -} - -func (h *pickupRatingHandler) CreateRating(c *fiber.Ctx) error { - pickupID := c.Params("id") - userID := c.Locals("userID").(string) - collectorID := c.Query("collector_id") - - var req dto.CreatePickupRatingDTO - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"Format JSON tidak valid"}, - }) - } - - if errs, ok := req.ValidateCreatePickupRatingDTO(); !ok { - return utils.ValidationErrorResponse(c, errs) - } - - err := h.service.CreateRating(context.Background(), userID, pickupID, collectorID, req) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Rating berhasil dikirim") -} - -func (h *pickupRatingHandler) GetRatingsByCollector(c *fiber.Ctx) error { - collectorID := c.Params("id") - ratings, err := h.service.GetRatingsByCollector(context.Background(), collectorID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - return utils.SuccessResponse(c, ratings, "Daftar rating collector berhasil diambil") -} - -func (h *pickupRatingHandler) GetAverageRating(c *fiber.Ctx) error { - collectorID := c.Params("id") - avg, err := h.service.GetAverageRating(context.Background(), collectorID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - return utils.SuccessResponse(c, fiber.Map{"average_rating": avg}, "Rata-rata rating collector") -} diff --git a/internal/handler/request_pickup_handler.go b/internal/handler/request_pickup_handler.go deleted file mode 100644 index f5c2826..0000000 --- a/internal/handler/request_pickup_handler.go +++ /dev/null @@ -1,150 +0,0 @@ -package handler - -import ( - "context" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - "time" - - "github.com/gofiber/fiber/v2" -) - -type RequestPickupHandler interface { - CreateRequestPickup(c *fiber.Ctx) error - SelectCollector(c *fiber.Ctx) error - GetAssignedPickup(c *fiber.Ctx) error - ConfirmPickup(c *fiber.Ctx) error - UpdatePickupStatus(c *fiber.Ctx) error - UpdatePickupItemActualAmount(c *fiber.Ctx) error -} - -type requestPickupHandler struct { - service services.RequestPickupService -} - -func NewRequestPickupHandler(service services.RequestPickupService) RequestPickupHandler { - return &requestPickupHandler{service: service} -} - -func (h *requestPickupHandler) CreateRequestPickup(c *fiber.Ctx) error { - userID := c.Locals("userID").(string) - - var req dto.RequestPickupDTO - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if errs, ok := req.Validate(); !ok { - return utils.ValidationErrorResponse(c, errs) - } - - if err := h.service.ConvertCartToRequestPickup(context.Background(), userID, req); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Request pickup berhasil dibuat") -} - -func (h *requestPickupHandler) SelectCollector(c *fiber.Ctx) error { - pickupID := c.Params("id") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID harus disertakan"}, - }) - } - - var req dto.SelectCollectorDTO - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if errs, ok := req.Validate(); !ok { - return utils.ValidationErrorResponse(c, errs) - } - - if err := h.service.AssignCollectorToRequest(context.Background(), pickupID, req); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Collector berhasil dipilih untuk pickup") -} - -func (h *requestPickupHandler) GetAssignedPickup(c *fiber.Ctx) error { - collectorID := c.Locals("userID").(string) - result, err := h.service.FindRequestsAssignedToCollector(context.Background(), collectorID) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - return utils.SuccessResponse(c, result, "Data pickup yang ditugaskan berhasil diambil") -} - -func (h *requestPickupHandler) ConfirmPickup(c *fiber.Ctx) error { - pickupID := c.Params("id") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID wajib diisi"}, - }) - } - - err := h.service.ConfirmPickupByCollector(context.Background(), pickupID, time.Now()) - if err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - return utils.SuccessResponse(c, nil, "Pickup berhasil dikonfirmasi oleh collector") -} - -func (h *requestPickupHandler) UpdatePickupStatus(c *fiber.Ctx) error { - pickupID := c.Params("id") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID tidak boleh kosong"}, - }) - } - - if err := h.service.UpdatePickupStatusToPickingUp(context.Background(), pickupID); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Status pickup berhasil diperbarui menjadi 'collector_are_picking_up'") -} - -func (h *requestPickupHandler) UpdatePickupItemActualAmount(c *fiber.Ctx) error { - pickupID := c.Params("id") - if pickupID == "" { - return utils.ValidationErrorResponse(c, map[string][]string{ - "pickup_id": {"pickup ID tidak boleh kosong"}, - }) - } - - var req dto.UpdatePickupItemsRequest - if err := c.BodyParser(&req); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{ - "body": {"format JSON tidak valid"}, - }) - } - - if len(req.Items) == 0 { - return utils.ValidationErrorResponse(c, map[string][]string{ - "items": {"daftar item tidak boleh kosong"}, - }) - } - - for _, item := range req.Items { - if item.ItemID == "" || item.Amount <= 0 { - return utils.ValidationErrorResponse(c, map[string][]string{ - "item": {"item_id harus valid dan amount > 0"}, - }) - } - } - - if err := h.service.UpdateActualPickupItems(context.Background(), pickupID, req.Items); err != nil { - return utils.InternalServerErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Berat aktual dan harga berhasil diperbarui") -} \ No newline at end of file diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go deleted file mode 100644 index b399aa1..0000000 --- a/internal/handler/role_handler.go +++ /dev/null @@ -1,47 +0,0 @@ -package handler - -import ( - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type RoleHandler struct { - RoleService services.RoleService -} - -func NewRoleHandler(roleService services.RoleService) *RoleHandler { - return &RoleHandler{RoleService: roleService} -} - -func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { - - // roleID, ok := c.Locals("roleID").(string) - // if !ok || roleID != utils.RoleAdministrator { - // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") - // } - - roles, err := h.RoleService.GetRoles(c.Context()) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, roles, "Roles fetched successfully") -} - -func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { - roleID := c.Params("role_id") - - // roleIDFromSession, ok := c.Locals("roleID").(string) - // if !ok || roleIDFromSession != utils.RoleAdministrator { - // return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource") - // } - - role, err := h.RoleService.GetRoleByID(c.Context(), roleID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "role id tidak ditemukan") - } - - return utils.SuccessResponse(c, role, "Role fetched successfully") -} diff --git a/internal/handler/store_handler.go b/internal/handler/store_handler.go deleted file mode 100644 index dde89f1..0000000 --- a/internal/handler/store_handler.go +++ /dev/null @@ -1,159 +0,0 @@ -package handler - -import ( - "log" - - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type StoreHandler struct { - StoreService services.StoreService -} - -func NewStoreHandler(storeService services.StoreService) *StoreHandler { - return &StoreHandler{StoreService: storeService} -} - -func (h *StoreHandler) CreateStore(c *fiber.Ctx) error { - - 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 { - 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, 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") -} - -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/handler/trash_handler.go b/internal/handler/trash_handler.go deleted file mode 100644 index 3bec83f..0000000 --- a/internal/handler/trash_handler.go +++ /dev/null @@ -1,148 +0,0 @@ -package handler - -import ( - "log" - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type TrashHandler struct { - TrashService services.TrashService -} - -func NewTrashHandler(trashService services.TrashService) *TrashHandler { - return &TrashHandler{TrashService: trashService} -} - -func (h *TrashHandler) CreateCategory(c *fiber.Ctx) error { - var request dto.RequestTrashCategoryDTO - - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := request.ValidateTrashCategoryInput() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - iconTrash, err := c.FormFile("icon") - if err != nil { - log.Printf("Error retrieving card photo from request: %v", err) - return utils.ErrorResponse(c, "Card photo is required") - } - - categoryResponse, err := h.TrashService.CreateCategory(request, iconTrash) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to create category: "+err.Error()) - } - - return utils.CreateResponse(c, categoryResponse, "Category created successfully") -} - -func (h *TrashHandler) AddDetailToCategory(c *fiber.Ctx) error { - var request dto.RequestTrashDetailDTO - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - detailResponse, err := h.TrashService.AddDetailToCategory(request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to add detail to category: "+err.Error()) - } - - return utils.CreateResponse(c, detailResponse, "Trash detail added successfully") -} - -func (h *TrashHandler) GetCategories(c *fiber.Ctx) error { - - categories, err := h.TrashService.GetCategories() - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch categories: "+err.Error()) - } - - return utils.NonPaginatedResponse(c, categories, len(categories), "Categories retrieved successfully") -} - -func (h *TrashHandler) GetCategoryByID(c *fiber.Ctx) error { - id := c.Params("category_id") - - category, err := h.TrashService.GetCategoryByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "Category not found: "+err.Error()) - } - - return utils.SuccessResponse(c, category, "Category retrieved successfully") -} - -func (h *TrashHandler) GetTrashDetailByID(c *fiber.Ctx) error { - id := c.Params("detail_id") - - detail, err := h.TrashService.GetTrashDetailByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, "Trash detail not found: "+err.Error()) - } - - return utils.SuccessResponse(c, detail, "Trash detail retrieved successfully") -} - -func (h *TrashHandler) UpdateCategory(c *fiber.Ctx) error { - id := c.Params("category_id") - - var request dto.RequestTrashCategoryDTO - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) - } - - iconTrash, err := c.FormFile("icon") - if err != nil && err.Error() != "File not found" { - log.Printf("Error retrieving icon trash from request: %v", err) - return utils.ErrorResponse(c, "icon trash is required") - } - - updatedCategory, err := h.TrashService.UpdateCategory(id, request, iconTrash) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating category: "+err.Error()) - } - - return utils.SuccessResponse(c, updatedCategory, "Category updated successfully") -} - -func (h *TrashHandler) UpdateDetail(c *fiber.Ctx) error { - id := c.Params("detail_id") - - var request dto.RequestTrashDetailDTO - if err := c.BodyParser(&request); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}}) - } - - updatedDetail, err := h.TrashService.UpdateDetail(id, request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating detail: "+err.Error()) - } - - return utils.SuccessResponse(c, updatedDetail, "Trash detail updated successfully") -} - -func (h *TrashHandler) DeleteCategory(c *fiber.Ctx) error { - id := c.Params("category_id") - - if err := h.TrashService.DeleteCategory(id); err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error deleting category: "+err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Category deleted successfully") -} - -func (h *TrashHandler) DeleteDetail(c *fiber.Ctx) error { - id := c.Params("detail_id") - - if err := h.TrashService.DeleteDetail(id); err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error deleting detail: "+err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, "Trash detail deleted successfully") -} diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go deleted file mode 100644 index 9b60bec..0000000 --- a/internal/handler/user_handler.go +++ /dev/null @@ -1,101 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - "strconv" - - "github.com/gofiber/fiber/v2" -) - -type UserHandler struct { - userService services.UserService -} - -func NewUserHandler(userService services.UserService) *UserHandler { - return &UserHandler{userService: userService} -} - -func (h *UserHandler) UpdateUserAvatarHandler(c *fiber.Ctx) error { - - userID := c.Locals("userID").(string) - - avatar, err := c.FormFile("avatar") - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file provided") - } - - updatedUser, err := h.userService.UpdateUserAvatar(userID, avatar) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, updatedUser, "Avatar updated successfully") -} - -func (h *UserHandler) GetUserByIDHandler(c *fiber.Ctx) error { - // userID := c.Params("id") - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - user, err := h.userService.GetUserByID(userID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) - } - - return utils.SuccessResponse(c, user, "User retrieved successfully") -} - -func (h *UserHandler) GetAllUsersHandler(c *fiber.Ctx) error { - - page := 1 - limit := 10 - - if p := c.Query("page"); p != "" { - page, _ = strconv.Atoi(p) - } - - if l := c.Query("limit"); l != "" { - limit, _ = strconv.Atoi(l) - } - - users, err := h.userService.GetAllUsers(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.PaginatedResponse(c, users, page, limit, len(users), "Users retrieved successfully") -} - -func (h *UserHandler) UpdateUserHandler(c *fiber.Ctx) error { - var request dto.RequestUserDTO - if err := c.BodyParser(&request); err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") - } - - userID := c.Locals("userID").(string) - updatedUser, err := h.userService.UpdateUser(userID, &request) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, updatedUser, "User profile updated successfully") -} - -func (h *UserHandler) UpdateUserPasswordHandler(c *fiber.Ctx) error { - var request dto.UpdatePasswordDTO - if err := c.BodyParser(&request); err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Invalid request body") - } - - userID := c.Locals("userID").(string) - err := h.userService.UpdateUserPassword(userID, request.OldPassword, request.NewPassword, request.ConfirmNewPassword) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Password updated successfully") -} diff --git a/internal/handler/userpin_handler.go b/internal/handler/userpin_handler.go deleted file mode 100644 index bb65b4b..0000000 --- a/internal/handler/userpin_handler.go +++ /dev/null @@ -1,102 +0,0 @@ -package handler - -import ( - "rijig/dto" - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type UserPinHandler struct { - UserPinService services.UserPinService -} - -func NewUserPinHandler(userPinService services.UserPinService) *UserPinHandler { - return &UserPinHandler{UserPinService: userPinService} -} - -func (h *UserPinHandler) VerifyUserPin(c *fiber.Ctx) error { - var requestUserPinDTO dto.RequestUserPinDTO - if err := c.BodyParser(&requestUserPinDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := requestUserPinDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - message, err := h.UserPinService.VerifyUserPin(userID, requestUserPinDTO.Pin) - if err != nil { - return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, message) -} - -func (h *UserPinHandler) CheckPinStatus(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found") - } - - status, err := h.UserPinService.CheckPinStatus(userID) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - if status == "Pin not created" { - return utils.GenericResponse(c, fiber.StatusBadRequest, "Pin belum dibuat") - } - - return utils.GenericResponse(c, fiber.StatusOK, "Pin sudah dibuat") -} - -func (h *UserPinHandler) CreateUserPin(c *fiber.Ctx) error { - var requestUserPinDTO dto.RequestUserPinDTO - if err := c.BodyParser(&requestUserPinDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := requestUserPinDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - userID := c.Locals("userID").(string) - - message, err := h.UserPinService.CreateUserPin(userID, requestUserPinDTO.Pin) - if err != nil { - - return utils.GenericResponse(c, fiber.StatusConflict, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusCreated, message) -} - -func (h *UserPinHandler) UpdateUserPin(c *fiber.Ctx) error { - var requestUserPinDTO dto.UpdateUserPinDTO - if err := c.BodyParser(&requestUserPinDTO); err != nil { - return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) - } - - errors, valid := requestUserPinDTO.Validate() - if !valid { - return utils.ValidationErrorResponse(c, errors) - } - - userID := c.Locals("userID").(string) - - message, err := h.UserPinService.UpdateUserPin(userID, requestUserPinDTO.OldPin, requestUserPinDTO.NewPin) - if err != nil { - return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error()) - } - - return utils.GenericResponse(c, fiber.StatusOK, message) -} diff --git a/internal/handler/whatsapp_handler.go b/internal/handler/whatsapp_handler.go deleted file mode 100644 index e875f36..0000000 --- a/internal/handler/whatsapp_handler.go +++ /dev/null @@ -1,24 +0,0 @@ -package handler - -import ( - "log" - "rijig/config" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func WhatsAppHandler(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.ErrorResponse(c, "User is not logged in or invalid session") - } - - err := config.LogoutWhatsApp() - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.ErrorResponse(c, err.Error()) - } - - return utils.SuccessResponse(c, nil, "Logged out successfully") -} diff --git a/internal/handler/wilayah_indonesia_handler.go b/internal/handler/wilayah_indonesia_handler.go deleted file mode 100644 index bde943a..0000000 --- a/internal/handler/wilayah_indonesia_handler.go +++ /dev/null @@ -1,200 +0,0 @@ -package handler - -import ( - "strconv" - - "rijig/internal/services" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -type WilayahIndonesiaHandler struct { - WilayahService services.WilayahIndonesiaService -} - -func NewWilayahImportHandler(wilayahService services.WilayahIndonesiaService) *WilayahIndonesiaHandler { - return &WilayahIndonesiaHandler{WilayahService: wilayahService} -} - -func (h *WilayahIndonesiaHandler) ImportWilayahData(c *fiber.Ctx) error { - - err := h.WilayahService.ImportDataFromCSV() - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, fiber.StatusCreated, "Data imported successfully") -} - -func (h *WilayahIndonesiaHandler) GetProvinces(c *fiber.Ctx) error { - - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - provinces, totalProvinces, err := h.WilayahService.GetAllProvinces(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch provinces") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, provinces, page, limit, totalProvinces, "Provinces fetched successfully") - } - - return utils.NonPaginatedResponse(c, provinces, totalProvinces, "Provinces fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetProvinceByID(c *fiber.Ctx) error { - provinceID := c.Params("provinceid") - - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - province, totalRegencies, err := h.WilayahService.GetProvinceByID(provinceID, page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch province") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, province, page, limit, totalRegencies, "Province fetched successfully") - } - - return utils.NonPaginatedResponse(c, province, totalRegencies, "Province fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetAllRegencies(c *fiber.Ctx) error { - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - regencies, totalRegencies, err := h.WilayahService.GetAllRegencies(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch regency") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, regencies, page, limit, totalRegencies, "regency fetched successfully") - } - - return utils.NonPaginatedResponse(c, regencies, totalRegencies, "Provinces fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetRegencyByID(c *fiber.Ctx) error { - regencyId := c.Params("regencyid") - - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - regency, totalDistrict, err := h.WilayahService.GetRegencyByID(regencyId, page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch regency") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, regency, page, limit, totalDistrict, "regency fetched successfully") - } - - return utils.NonPaginatedResponse(c, regency, totalDistrict, "regency fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetAllDistricts(c *fiber.Ctx) error { - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - districts, totalDistricts, err := h.WilayahService.GetAllDistricts(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch districts") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, districts, page, limit, totalDistricts, "districts fetched successfully") - } - - return utils.NonPaginatedResponse(c, districts, totalDistricts, "districts fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetDistrictByID(c *fiber.Ctx) error { - districtId := c.Params("districtid") - - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - district, totalVillages, err := h.WilayahService.GetDistrictByID(districtId, page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch district") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, district, page, limit, totalVillages, "district fetched successfully") - } - - return utils.NonPaginatedResponse(c, district, totalVillages, "district fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetAllVillages(c *fiber.Ctx) error { - page, err := strconv.Atoi(c.Query("page", "0")) - if err != nil { - page = 0 - } - limit, err := strconv.Atoi(c.Query("limit", "0")) - if err != nil { - limit = 0 - } - - villages, totalVillages, err := h.WilayahService.GetAllVillages(page, limit) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch villages") - } - - if page > 0 && limit > 0 { - return utils.PaginatedResponse(c, villages, page, limit, totalVillages, "villages fetched successfully") - } - - return utils.NonPaginatedResponse(c, villages, totalVillages, "villages fetched successfully") -} - -func (h *WilayahIndonesiaHandler) GetVillageByID(c *fiber.Ctx) error { - id := c.Params("villageid") - - village, err := h.WilayahService.GetVillageByID(id) - if err != nil { - return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) - } - - return utils.SuccessResponse(c, village, "Village fetched successfully") -} diff --git a/internal/repositories/about_repo.go b/internal/repositories/about_repo.go deleted file mode 100644 index 29224f5..0000000 --- a/internal/repositories/about_repo.go +++ /dev/null @@ -1,112 +0,0 @@ -package repositories - -import ( - "fmt" - "rijig/model" - - "gorm.io/gorm" -) - -type AboutRepository interface { - CreateAbout(about *model.About) error - CreateAboutDetail(aboutDetail *model.AboutDetail) error - GetAllAbout() ([]model.About, error) - GetAboutByID(id string) (*model.About, error) - GetAboutByIDWithoutPrel(id string) (*model.About, error) - GetAboutDetailByID(id string) (*model.AboutDetail, error) - UpdateAbout(id string, about *model.About) (*model.About, error) - UpdateAboutDetail(id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) - DeleteAbout(id string) error - DeleteAboutDetail(id string) error -} - -type aboutRepository struct { - DB *gorm.DB -} - -func NewAboutRepository(db *gorm.DB) AboutRepository { - return &aboutRepository{DB: db} -} - -func (r *aboutRepository) CreateAbout(about *model.About) error { - if err := r.DB.Create(&about).Error; err != nil { - return fmt.Errorf("failed to create About: %v", err) - } - return nil -} - -func (r *aboutRepository) CreateAboutDetail(aboutDetail *model.AboutDetail) error { - if err := r.DB.Create(&aboutDetail).Error; err != nil { - return fmt.Errorf("failed to create AboutDetail: %v", err) - } - return nil -} - -func (r *aboutRepository) GetAllAbout() ([]model.About, error) { - var abouts []model.About - if err := r.DB.Find(&abouts).Error; err != nil { - return nil, fmt.Errorf("failed to fetch all About records: %v", err) - } - return abouts, nil -} - -func (r *aboutRepository) GetAboutByID(id string) (*model.About, error) { - var about model.About - if err := r.DB.Preload("AboutDetail").Where("id = ?", id).First(&about).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("about with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch About by ID: %v", err) - } - return &about, nil -} - -func (r *aboutRepository) GetAboutByIDWithoutPrel(id string) (*model.About, error) { - var about model.About - if err := r.DB.Where("id = ?", id).First(&about).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("about with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch About by ID: %v", err) - } - return &about, nil -} - -func (r *aboutRepository) GetAboutDetailByID(id string) (*model.AboutDetail, error) { - var aboutDetail model.AboutDetail - if err := r.DB.Where("id = ?", id).First(&aboutDetail).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("aboutdetail with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch About by ID: %v", err) - } - return &aboutDetail, nil -} - -func (r *aboutRepository) UpdateAbout(id string, about *model.About) (*model.About, error) { - if err := r.DB.Model(&about).Where("id = ?", id).Updates(about).Error; err != nil { - return nil, fmt.Errorf("failed to update About: %v", err) - } - return about, nil -} - -func (r *aboutRepository) UpdateAboutDetail(id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) { - if err := r.DB.Model(&aboutDetail).Where("id = ?", id).Updates(aboutDetail).Error; err != nil { - return nil, fmt.Errorf("failed to update AboutDetail: %v", err) - } - return aboutDetail, nil -} - -func (r *aboutRepository) DeleteAbout(id string) error { - if err := r.DB.Where("id = ?", id).Delete(&model.About{}).Error; err != nil { - return fmt.Errorf("failed to delete About: %v", err) - } - return nil -} - -func (r *aboutRepository) DeleteAboutDetail(id string) error { - if err := r.DB.Where("id = ?", id).Delete(&model.AboutDetail{}).Error; err != nil { - return fmt.Errorf("failed to delete AboutDetail: %v", err) - } - return nil -} diff --git a/internal/repositories/address_repo.go b/internal/repositories/address_repo.go deleted file mode 100644 index d8c2cf5..0000000 --- a/internal/repositories/address_repo.go +++ /dev/null @@ -1,61 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type AddressRepository interface { - CreateAddress(address *model.Address) error - FindAddressByUserID(userID string) ([]model.Address, error) - FindAddressByID(id string) (*model.Address, error) - UpdateAddress(address *model.Address) error - DeleteAddress(id string) error -} - -type addressRepository struct { - DB *gorm.DB -} - -func NewAddressRepository(db *gorm.DB) AddressRepository { - return &addressRepository{DB: db} -} - -func (r *addressRepository) CreateAddress(address *model.Address) error { - return r.DB.Create(address).Error -} - -func (r *addressRepository) FindAddressByUserID(userID string) ([]model.Address, error) { - var addresses []model.Address - err := r.DB.Where("user_id = ?", userID).Find(&addresses).Error - if err != nil { - return nil, err - } - return addresses, nil -} - -func (r *addressRepository) FindAddressByID(id string) (*model.Address, error) { - var address model.Address - err := r.DB.Where("id = ?", id).First(&address).Error - if err != nil { - return nil, err - } - return &address, nil -} - -func (r *addressRepository) UpdateAddress(address *model.Address) error { - err := r.DB.Save(address).Error - if err != nil { - return err - } - return nil -} - -func (r *addressRepository) DeleteAddress(id string) error { - err := r.DB.Where("id = ?", id).Delete(&model.Address{}).Error - if err != nil { - return err - } - return nil -} diff --git a/internal/repositories/article_repo.go b/internal/repositories/article_repo.go deleted file mode 100644 index d9a1222..0000000 --- a/internal/repositories/article_repo.go +++ /dev/null @@ -1,75 +0,0 @@ -package repositories - -import ( - "fmt" - - "rijig/model" - - "gorm.io/gorm" -) - -type ArticleRepository interface { - CreateArticle(article *model.Article) error - FindArticleByID(id string) (*model.Article, error) - FindAllArticles(page, limit int) ([]model.Article, int, error) - UpdateArticle(id string, article *model.Article) error - DeleteArticle(id string) error -} - -type articleRepository struct { - DB *gorm.DB -} - -func NewArticleRepository(db *gorm.DB) ArticleRepository { - return &articleRepository{DB: db} -} - -func (r *articleRepository) CreateArticle(article *model.Article) error { - return r.DB.Create(article).Error -} - -func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) { - var article model.Article - err := r.DB.Where("id = ?", id).First(&article).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("article with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch article: %v", err) - } - return &article, nil -} - -func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, int, error) { - var articles []model.Article - var total int64 - - if err := r.DB.Model(&model.Article{}).Count(&total).Error; err != nil { - return nil, 0, fmt.Errorf("failed to count articles: %v", err) - } - - fmt.Printf("Total Articles Count: %d\n", total) - - if page > 0 && limit > 0 { - err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) - } - } else { - err := r.DB.Find(&articles).Error - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) - } - } - - return articles, int(total), nil -} - -func (r *articleRepository) UpdateArticle(id string, article *model.Article) error { - return r.DB.Model(&model.Article{}).Where("id = ?", id).Updates(article).Error -} - -func (r *articleRepository) DeleteArticle(id string) error { - result := r.DB.Delete(&model.Article{}, "id = ?", id) - return result.Error -} diff --git a/internal/repositories/auth/auth_admin_repo.go b/internal/repositories/auth/auth_admin_repo.go deleted file mode 100644 index 4652e1d..0000000 --- a/internal/repositories/auth/auth_admin_repo.go +++ /dev/null @@ -1,93 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type AuthAdminRepository interface { - FindByEmail(email string) (*model.User, error) - FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) - FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) - FindByPhone(phone string) (*model.User, error) - FindByEmailOrPhone(identifier string) (*model.User, error) - FindRoleByName(roleName string) (*model.Role, error) - CreateUser(user *model.User) (*model.User, error) -} - -type authAdminRepository struct { - DB *gorm.DB -} - -func NewAuthAdminRepository(db *gorm.DB) AuthAdminRepository { - return &authAdminRepository{DB: db} -} - -func (r *authAdminRepository) FindByEmail(email string) (*model.User, error) { - var user model.User - err := r.DB.Preload("Role").Where("email = ?", email).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *authAdminRepository) FindAdminByEmailandRoleid(email, roleId string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? AND role_id = ?", email, roleId).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} - -func (r *authAdminRepository) FindAdminByPhoneandRoleid(phone, roleId string) (*model.User, error) { - var user model.User - err := r.DB.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} - -func (r *authAdminRepository) FindByPhone(phone string) (*model.User, error) { - var user model.User - err := r.DB.Where("phone = ?", phone).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *authAdminRepository) FindByEmailOrPhone(identifier string) (*model.User, error) { - var user model.User - err := r.DB.Where("email = ? OR phone = ?", identifier, identifier).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *authAdminRepository) CreateUser(user *model.User) (*model.User, error) { - err := r.DB.Create(user).Error - if err != nil { - return nil, err - } - return user, nil -} - -func (r *authAdminRepository) FindRoleByName(roleName string) (*model.Role, error) { - var role model.Role - err := r.DB.Where("role_name = ?", roleName).First(&role).Error - if err != nil { - return nil, err - } - return &role, nil -} diff --git a/internal/repositories/auth/auth_masyarakat_repo.go b/internal/repositories/auth/auth_masyarakat_repo.go deleted file mode 100644 index f49bfd0..0000000 --- a/internal/repositories/auth/auth_masyarakat_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type AuthMasyarakatRepository interface { - CreateUser(user *model.User) (*model.User, error) - GetUserByPhone(phone string) (*model.User, error) - GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) -} - -type authMasyarakatRepository struct { - db *gorm.DB -} - -func NewAuthMasyarakatRepositories(db *gorm.DB) AuthMasyarakatRepository { - return &authMasyarakatRepository{db} -} - -func (r *authMasyarakatRepository) CreateUser(user *model.User) (*model.User, error) { - if err := r.db.Create(user).Error; err != nil { - return nil, err - } - return user, nil -} - -func (r *authMasyarakatRepository) GetUserByPhone(phone string) (*model.User, error) { - var user model.User - if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -func (r *authMasyarakatRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { - var user model.User - err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} diff --git a/internal/repositories/auth/auth_pengelola_repo.go b/internal/repositories/auth/auth_pengelola_repo.go deleted file mode 100644 index f7b561a..0000000 --- a/internal/repositories/auth/auth_pengelola_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type AuthPengelolaRepository interface { - CreateUser(user *model.User) (*model.User, error) - GetUserByPhone(phone string) (*model.User, error) - GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) -} - -type authPengelolaRepository struct { - db *gorm.DB -} - -func NewAuthPengelolaRepositories(db *gorm.DB) AuthPengelolaRepository { - return &authPengelolaRepository{db} -} - -func (r *authPengelolaRepository) CreateUser(user *model.User) (*model.User, error) { - if err := r.db.Create(user).Error; err != nil { - return nil, err - } - return user, nil -} - -func (r *authPengelolaRepository) GetUserByPhone(phone string) (*model.User, error) { - var user model.User - if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -func (r *authPengelolaRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { - var user model.User - err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} diff --git a/internal/repositories/auth/auth_pengepul_repo.go b/internal/repositories/auth/auth_pengepul_repo.go deleted file mode 100644 index 5253ee1..0000000 --- a/internal/repositories/auth/auth_pengepul_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type AuthPengepulRepository interface { - CreateUser(user *model.User) (*model.User, error) - GetUserByPhone(phone string) (*model.User, error) - GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) -} - -type authPengepulRepository struct { - db *gorm.DB -} - -func NewAuthPengepulRepositories(db *gorm.DB) AuthPengepulRepository { - return &authPengepulRepository{db} -} - -func (r *authPengepulRepository) CreateUser(user *model.User) (*model.User, error) { - if err := r.db.Create(user).Error; err != nil { - return nil, err - } - return user, nil -} - -func (r *authPengepulRepository) GetUserByPhone(phone string) (*model.User, error) { - var user model.User - if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -func (r *authPengepulRepository) GetUserByPhoneAndRole(phone string, roleId string) (*model.User, error) { - var user model.User - err := r.db.Where("phone = ? AND role_id = ?", phone, roleId).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} diff --git a/internal/repositories/auth_repo.go b/internal/repositories/auth_repo.go deleted file mode 100644 index a9466ca..0000000 --- a/internal/repositories/auth_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type UserRepository interface { - CreateUser(user *model.User) (*model.User, error) - GetUserByPhone(phone string) (*model.User, error) - GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error) -} - -type userRepository struct { - db *gorm.DB -} - -func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{db} -} - -func (r *userRepository) CreateUser(user *model.User) (*model.User, error) { - if err := r.db.Create(user).Error; err != nil { - return nil, err - } - return user, nil -} - -func (r *userRepository) GetUserByPhone(phone string) (*model.User, error) { - var user model.User - if err := r.db.Where("phone = ?", phone).First(&user).Error; err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) GetUserByPhoneAndRole(phone string, roleID string) (*model.User, error) { - var user model.User - err := r.db.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &user, nil -} diff --git a/internal/repositories/banner_repo.go b/internal/repositories/banner_repo.go deleted file mode 100644 index 41803de..0000000 --- a/internal/repositories/banner_repo.go +++ /dev/null @@ -1,70 +0,0 @@ -package repositories - -import ( - "fmt" - - "rijig/model" - - "gorm.io/gorm" -) - -type BannerRepository interface { - CreateBanner(banner *model.Banner) error - FindBannerByID(id string) (*model.Banner, error) - FindAllBanners() ([]model.Banner, error) - UpdateBanner(id string, banner *model.Banner) error - DeleteBanner(id string) error -} - -type bannerRepository struct { - DB *gorm.DB -} - -func NewBannerRepository(db *gorm.DB) BannerRepository { - return &bannerRepository{DB: db} -} - -func (r *bannerRepository) CreateBanner(banner *model.Banner) error { - if err := r.DB.Create(banner).Error; err != nil { - return fmt.Errorf("failed to create banner: %v", err) - } - return nil -} - -func (r *bannerRepository) FindBannerByID(id string) (*model.Banner, error) { - var banner model.Banner - err := r.DB.Where("id = ?", id).First(&banner).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("banner with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch banner by ID: %v", err) - } - return &banner, nil -} - -func (r *bannerRepository) FindAllBanners() ([]model.Banner, error) { - var banners []model.Banner - err := r.DB.Find(&banners).Error - if err != nil { - return nil, fmt.Errorf("failed to fetch banners: %v", err) - } - - return banners, nil -} - -func (r *bannerRepository) UpdateBanner(id string, banner *model.Banner) error { - err := r.DB.Model(&model.Banner{}).Where("id = ?", id).Updates(banner).Error - if err != nil { - return fmt.Errorf("failed to update banner: %v", err) - } - return nil -} - -func (r *bannerRepository) DeleteBanner(id string) error { - result := r.DB.Delete(&model.Banner{}, "id = ?", id) - if result.Error != nil { - return fmt.Errorf("failed to delete banner: %v", result.Error) - } - return nil -} diff --git a/internal/repositories/collector_repo.go b/internal/repositories/collector_repo.go deleted file mode 100644 index 7cfc218..0000000 --- a/internal/repositories/collector_repo.go +++ /dev/null @@ -1,135 +0,0 @@ -package repositories - -import ( - "context" - "errors" - - "rijig/config" - "rijig/model" -) - -type CollectorRepository interface { - CreateCollector(ctx context.Context, collector *model.Collector) error - AddAvaibleTrash(ctx context.Context, trashItems []model.AvaibleTrashByCollector) error - GetCollectorByID(ctx context.Context, collectorID string) (*model.Collector, error) - GetCollectorByUserID(ctx context.Context, userID string) (*model.Collector, error) - GetTrashItemByID(ctx context.Context, id string) (*model.AvaibleTrashByCollector, error) - UpdateCollector(ctx context.Context, collector *model.Collector, updates map[string]interface{}) error - UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []model.AvaibleTrashByCollector) error - DeleteAvaibleTrash(ctx context.Context, trashID string) error - - GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) - GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) -} - -type collectorRepository struct { -} - -func NewCollectorRepository() CollectorRepository { - return &collectorRepository{} -} - -func (r *collectorRepository) CreateCollector(ctx context.Context, collector *model.Collector) error { - return config.DB.WithContext(ctx).Create(collector).Error -} - -func (r *collectorRepository) AddAvaibleTrash(ctx context.Context, trashItems []model.AvaibleTrashByCollector) error { - if len(trashItems) == 0 { - return nil - } - return config.DB.WithContext(ctx).Create(&trashItems).Error -} - -func (r *collectorRepository) GetCollectorByID(ctx context.Context, collectorID string) (*model.Collector, error) { - var collector model.Collector - err := config.DB.WithContext(ctx). - Preload("User"). - Preload("Address"). - Preload("AvaibleTrashByCollector.TrashCategory"). - First(&collector, "id = ?", collectorID).Error - - if err != nil { - return nil, err - } - return &collector, nil -} - -func (r *collectorRepository) GetCollectorByUserID(ctx context.Context, userID string) (*model.Collector, error) { - var collector model.Collector - err := config.DB.WithContext(ctx). - Preload("User"). - Preload("Address"). - Preload("AvaibleTrashByCollector.TrashCategory"). - First(&collector, "user_id = ?", userID).Error - - if err != nil { - return nil, err - } - return &collector, nil -} - -func (r *collectorRepository) GetTrashItemByID(ctx context.Context, id string) (*model.AvaibleTrashByCollector, error) { - var item model.AvaibleTrashByCollector - if err := config.DB.WithContext(ctx).First(&item, "id = ?", id).Error; err != nil { - return nil, err - } - return &item, nil -} - -func (r *collectorRepository) UpdateCollector(ctx context.Context, collector *model.Collector, updates map[string]interface{}) error { - return config.DB.WithContext(ctx). - Model(&model.Collector{}). - Where("id = ?", collector.ID). - Updates(updates).Error -} - -func (r *collectorRepository) UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []model.AvaibleTrashByCollector) error { - for _, trash := range updatedTrash { - err := config.DB.WithContext(ctx). - Model(&model.AvaibleTrashByCollector{}). - Where("collector_id = ? AND trash_category_id = ?", collectorID, trash.TrashCategoryID). - Update("price", trash.Price).Error - if err != nil { - return err - } - } - return nil -} - -func (r *collectorRepository) DeleteAvaibleTrash(ctx context.Context, trashID string) error { - if trashID == "" { - return errors.New("trash_id cannot be empty") - } - return config.DB.WithContext(ctx). - Delete(&model.AvaibleTrashByCollector{}, "id = ?", trashID).Error -} - -func (r *collectorRepository) GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) { - var collectors []model.Collector - err := config.DB.WithContext(ctx). - Preload("User"). - Preload("Address"). - Preload("AvaibleTrashbyCollector.TrashCategory"). - Where("job_status = ?", "active"). - Find(&collectors).Error - - if err != nil { - return nil, err - } - - return collectors, nil -} - -func (r *collectorRepository) GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) { - var collector model.Collector - err := config.DB.WithContext(ctx). - Preload("Address"). - Preload("AvaibleTrashbyCollector"). - Where("id = ?", collectorID). - First(&collector).Error - - if err != nil { - return nil, err - } - return &collector, nil -} diff --git a/internal/repositories/company_profile_repo.go b/internal/repositories/company_profile_repo.go deleted file mode 100644 index bf0d4ea..0000000 --- a/internal/repositories/company_profile_repo.go +++ /dev/null @@ -1,78 +0,0 @@ -package repositories - -import ( - "fmt" - "rijig/model" - - "gorm.io/gorm" -) - -type CompanyProfileRepository interface { - CreateCompanyProfile(companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) - GetCompanyProfileByID(id string) (*model.CompanyProfile, error) - GetCompanyProfilesByUserID(userID string) ([]model.CompanyProfile, error) - UpdateCompanyProfile(id string, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) - DeleteCompanyProfile(id string) error -} - -type companyProfileRepository struct { - DB *gorm.DB -} - -func NewCompanyProfileRepository(db *gorm.DB) CompanyProfileRepository { - return &companyProfileRepository{ - DB: db, - } -} - -func (r *companyProfileRepository) CreateCompanyProfile(companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) { - err := r.DB.Create(companyProfile).Error - if err != nil { - return nil, fmt.Errorf("failed to create company profile: %v", err) - } - return companyProfile, nil -} - -func (r *companyProfileRepository) GetCompanyProfileByID(id string) (*model.CompanyProfile, error) { - var companyProfile model.CompanyProfile - err := r.DB.Preload("User").First(&companyProfile, "id = ?", id).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("company profile with ID %s not found", id) - } - return nil, fmt.Errorf("error fetching company profile: %v", err) - } - return &companyProfile, nil -} - -func (r *companyProfileRepository) GetCompanyProfilesByUserID(userID string) ([]model.CompanyProfile, error) { - var companyProfiles []model.CompanyProfile - err := r.DB.Preload("User").Where("user_id = ?", userID).Find(&companyProfiles).Error - if err != nil { - return nil, fmt.Errorf("error fetching company profiles for userID %s: %v", userID, err) - } - return companyProfiles, nil -} - -func (r *companyProfileRepository) UpdateCompanyProfile(id string, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) { - var existingProfile model.CompanyProfile - err := r.DB.First(&existingProfile, "id = ?", id).Error - if err != nil { - return nil, fmt.Errorf("company profile not found: %v", err) - } - - err = r.DB.Model(&existingProfile).Updates(companyProfile).Error - if err != nil { - return nil, fmt.Errorf("failed to update company profile: %v", err) - } - - return &existingProfile, nil -} - -func (r *companyProfileRepository) DeleteCompanyProfile(id string) error { - err := r.DB.Delete(&model.CompanyProfile{}, "id = ?", id).Error - if err != nil { - return fmt.Errorf("failed to delete company profile: %v", err) - } - return nil -} diff --git a/internal/repositories/coveragearea_repo.go b/internal/repositories/coveragearea_repo.go deleted file mode 100644 index 02674a9..0000000 --- a/internal/repositories/coveragearea_repo.go +++ /dev/null @@ -1,82 +0,0 @@ -package repositories - -import ( - "fmt" - "rijig/model" - - "gorm.io/gorm" -) - -type CoverageAreaRepository interface { - FindCoverageByProvinceAndRegency(province, regency string) (*model.CoverageArea, error) - CreateCoverage(coverage *model.CoverageArea) error - FindCoverageById(id string) (*model.CoverageArea, error) - FindAllCoverage() ([]model.CoverageArea, error) - UpdateCoverage(id string, coverage *model.CoverageArea) error - DeleteCoverage(id string) error -} - -type coverageAreaRepository struct { - DB *gorm.DB -} - -func NewCoverageAreaRepository(db *gorm.DB) CoverageAreaRepository { - return &coverageAreaRepository{DB: db} -} - -func (r *coverageAreaRepository) FindCoverageByProvinceAndRegency(province, regency string) (*model.CoverageArea, error) { - var coverage model.CoverageArea - err := r.DB.Where("province = ? AND regency = ?", province, regency).First(&coverage).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &coverage, nil -} - -func (r *coverageAreaRepository) CreateCoverage(coverage *model.CoverageArea) error { - if err := r.DB.Create(coverage).Error; err != nil { - return fmt.Errorf("failed to create coverage: %v", err) - } - return nil -} - -func (r *coverageAreaRepository) FindCoverageById(id string) (*model.CoverageArea, error) { - var coverage model.CoverageArea - err := r.DB.Where("id = ?", id).First(&coverage).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("coverage with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch coverage by ID: %v", err) - } - return &coverage, nil -} - -func (r *coverageAreaRepository) FindAllCoverage() ([]model.CoverageArea, error) { - var coverage []model.CoverageArea - err := r.DB.Find(&coverage).Error - if err != nil { - return nil, fmt.Errorf("failed to fetch coverage: %v", err) - } - - return coverage, nil -} - -func (r *coverageAreaRepository) UpdateCoverage(id string, coverage *model.CoverageArea) error { - err := r.DB.Model(&model.CoverageArea{}).Where("id = ?", id).Updates(coverage).Error - if err != nil { - return fmt.Errorf("failed to update coverage: %v", err) - } - return nil -} - -func (r *coverageAreaRepository) DeleteCoverage(id string) error { - result := r.DB.Delete(&model.CoverageArea{}, "id = ?", id) - if result.Error != nil { - return fmt.Errorf("failed to delete coverage: %v", result.Error) - } - return nil -} diff --git a/internal/repositories/identitycard_repo.go b/internal/repositories/identitycard_repo.go deleted file mode 100644 index 763791a..0000000 --- a/internal/repositories/identitycard_repo.go +++ /dev/null @@ -1,82 +0,0 @@ -package repositories - -import ( - "errors" - "fmt" - "log" - "rijig/model" - - "gorm.io/gorm" -) - -type IdentityCardRepository interface { - CreateIdentityCard(identityCard *model.IdentityCard) (*model.IdentityCard, error) - GetIdentityCardByID(id string) (*model.IdentityCard, error) - GetIdentityCardsByUserID(userID string) ([]model.IdentityCard, error) - UpdateIdentityCard(id string, updatedCard *model.IdentityCard) (*model.IdentityCard, error) - DeleteIdentityCard(id string) error -} - -type identityCardRepository struct { - db *gorm.DB -} - -func NewIdentityCardRepository(db *gorm.DB) IdentityCardRepository { - return &identityCardRepository{ - db: db, - } -} - -func (r *identityCardRepository) CreateIdentityCard(identityCard *model.IdentityCard) (*model.IdentityCard, error) { - if err := r.db.Create(identityCard).Error; err != nil { - log.Printf("Error creating identity card: %v", err) - return nil, fmt.Errorf("failed to create identity card: %w", err) - } - return identityCard, nil -} - -func (r *identityCardRepository) GetIdentityCardByID(id string) (*model.IdentityCard, error) { - var identityCard model.IdentityCard - if err := r.db.Where("id = ?", id).First(&identityCard).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("identity card not found with id %s", id) - } - log.Printf("Error fetching identity card by ID: %v", err) - return nil, fmt.Errorf("error fetching identity card by ID: %w", err) - } - return &identityCard, nil -} - -func (r *identityCardRepository) GetIdentityCardsByUserID(userID string) ([]model.IdentityCard, error) { - var identityCards []model.IdentityCard - if err := r.db.Where("user_id = ?", userID).Find(&identityCards).Error; err != nil { - log.Printf("Error fetching identity cards by userID: %v", err) - return nil, fmt.Errorf("error fetching identity cards by userID: %w", err) - } - return identityCards, nil -} - -func (r *identityCardRepository) UpdateIdentityCard(id string, updatedCard *model.IdentityCard) (*model.IdentityCard, error) { - var existingCard model.IdentityCard - if err := r.db.Where("id = ?", id).First(&existingCard).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("identity card with ID %s not found", id) - } - log.Printf("Error fetching identity card for update: %v", err) - return nil, fmt.Errorf("error fetching identity card for update: %w", err) - } - - if err := r.db.Save(&existingCard).Error; err != nil { - log.Printf("Error updating identity card: %v", err) - return nil, fmt.Errorf("failed to update identity card: %w", err) - } - return &existingCard, nil -} - -func (r *identityCardRepository) DeleteIdentityCard(id string) error { - if err := r.db.Where("id = ?", id).Delete(&model.IdentityCard{}).Error; err != nil { - log.Printf("Error deleting identity card: %v", err) - return fmt.Errorf("failed to delete identity card: %w", err) - } - return nil -} diff --git a/internal/repositories/initialcoint_repo.go b/internal/repositories/initialcoint_repo.go deleted file mode 100644 index ec03479..0000000 --- a/internal/repositories/initialcoint_repo.go +++ /dev/null @@ -1,68 +0,0 @@ -package repositories - -import ( - "fmt" - "rijig/model" - - "gorm.io/gorm" -) - -type InitialCointRepository interface { - CreateInitialCoint(coint *model.InitialCoint) error - FindInitialCointByID(id string) (*model.InitialCoint, error) - FindAllInitialCoints() ([]model.InitialCoint, error) - UpdateInitialCoint(id string, coint *model.InitialCoint) error - DeleteInitialCoint(id string) error -} - -type initialCointRepository struct { - DB *gorm.DB -} - -func NewInitialCointRepository(db *gorm.DB) InitialCointRepository { - return &initialCointRepository{DB: db} -} - -func (r *initialCointRepository) CreateInitialCoint(coint *model.InitialCoint) error { - if err := r.DB.Create(coint).Error; err != nil { - return fmt.Errorf("failed to create initial coint: %v", err) - } - return nil -} - -func (r *initialCointRepository) FindInitialCointByID(id string) (*model.InitialCoint, error) { - var coint model.InitialCoint - err := r.DB.Where("id = ?", id).First(&coint).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("initial coint with ID %s not found", id) - } - return nil, fmt.Errorf("failed to fetch initial coint by ID: %v", err) - } - return &coint, nil -} - -func (r *initialCointRepository) FindAllInitialCoints() ([]model.InitialCoint, error) { - var coints []model.InitialCoint - err := r.DB.Find(&coints).Error - if err != nil { - return nil, fmt.Errorf("failed to fetch initial coints: %v", err) - } - return coints, nil -} - -func (r *initialCointRepository) UpdateInitialCoint(id string, coint *model.InitialCoint) error { - err := r.DB.Model(&model.InitialCoint{}).Where("id = ?", id).Updates(coint).Error - if err != nil { - return fmt.Errorf("failed to update initial coint: %v", err) - } - return nil -} - -func (r *initialCointRepository) DeleteInitialCoint(id string) error { - result := r.DB.Delete(&model.InitialCoint{}, "id = ?", id) - if result.Error != nil { - return fmt.Errorf("failed to delete initial coint: %v", result.Error) - } - return nil -} diff --git a/internal/repositories/pickup_history_repo.go b/internal/repositories/pickup_history_repo.go deleted file mode 100644 index 536ecf7..0000000 --- a/internal/repositories/pickup_history_repo.go +++ /dev/null @@ -1,34 +0,0 @@ -package repositories - -import ( - "context" - "rijig/config" - "rijig/model" -) - -type PickupStatusHistoryRepository interface { - CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error - GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) -} - -type pickupStatusHistoryRepository struct{} - -func NewPickupStatusHistoryRepository() PickupStatusHistoryRepository { - return &pickupStatusHistoryRepository{} -} - -func (r *pickupStatusHistoryRepository) CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error { - return config.DB.WithContext(ctx).Create(&history).Error -} - -func (r *pickupStatusHistoryRepository) GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) { - var histories []model.PickupStatusHistory - err := config.DB.WithContext(ctx). - Where("request_id = ?", requestID). - Order("changed_at asc"). - Find(&histories).Error - if err != nil { - return nil, err - } - return histories, nil -} diff --git a/internal/repositories/product_repo.go b/internal/repositories/product_repo.go deleted file mode 100644 index 08f1b1f..0000000 --- a/internal/repositories/product_repo.go +++ /dev/null @@ -1,139 +0,0 @@ -package repositories - -import ( - "fmt" - - "rijig/model" - - "gorm.io/gorm" -) - -type ProductRepository interface { - CountProductsByStoreID(storeID string) (int64, error) - CreateProduct(product *model.Product) error - GetProductByID(productID string) (*model.Product, error) - GetProductsByStoreID(storeID string) ([]model.Product, error) - FindProductsByStoreID(storeID string, page, limit int) ([]model.Product, error) - FindProductImagesByProductID(productID string) ([]model.ProductImage, error) - GetProductImageByID(imageID string) (*model.ProductImage, error) - UpdateProduct(product *model.Product) error - DeleteProduct(productID string) error - DeleteProductsByID(productIDs []string) error - AddProductImages(images []model.ProductImage) error - DeleteProductImagesByProductID(productID string) error - DeleteProductImagesByID(imageIDs []string) error - DeleteProductImageByID(imageID string) error -} - -type productRepository struct { - DB *gorm.DB -} - -func NewProductRepository(DB *gorm.DB) ProductRepository { - return &productRepository{DB} -} - -func (r *productRepository) CreateProduct(product *model.Product) error { - return r.DB.Create(product).Error -} - -func (r *productRepository) CountProductsByStoreID(storeID string) (int64, error) { - var count int64 - if err := r.DB.Model(&model.Product{}).Where("store_id = ?", storeID).Count(&count).Error; err != nil { - return 0, err - } - return count, nil -} - -func (r *productRepository) GetProductByID(productID string) (*model.Product, error) { - var product model.Product - if err := r.DB.Preload("ProductImages").Where("id = ?", productID).First(&product).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &product, nil -} - -func (r *productRepository) GetProductsByStoreID(storeID string) ([]model.Product, error) { - var products []model.Product - if err := r.DB.Where("store_id = ?", storeID).Preload("ProductImages").Find(&products).Error; err != nil { - return nil, err - } - return products, nil -} - -func (r *productRepository) FindProductsByStoreID(storeID string, page, limit int) ([]model.Product, error) { - var products []model.Product - offset := (page - 1) * limit - - if err := r.DB. - Where("store_id = ?", storeID). - Limit(limit). - Offset(offset). - Find(&products).Error; err != nil { - return nil, err - } - - return products, nil -} - -func (r *productRepository) FindProductImagesByProductID(productID string) ([]model.ProductImage, error) { - var productImages []model.ProductImage - if err := r.DB.Where("product_id = ?", productID).Find(&productImages).Error; err != nil { - return nil, err - } - return productImages, nil -} - -func (r *productRepository) GetProductImageByID(imageID string) (*model.ProductImage, error) { - var productImage model.ProductImage - if err := r.DB.Where("id = ?", imageID).First(&productImage).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return nil, nil - } - return nil, err - } - return &productImage, nil -} - -func (r *productRepository) UpdateProduct(product *model.Product) error { - return r.DB.Save(product).Error -} - -func (r *productRepository) DeleteProduct(productID string) error { - return r.DB.Delete(&model.Product{}, "id = ?", productID).Error -} - -func (r *productRepository) DeleteProductsByID(productIDs []string) error { - if err := r.DB.Where("id IN ?", productIDs).Delete(&model.Product{}).Error; err != nil { - return fmt.Errorf("failed to delete products: %v", err) - } - return nil -} - -func (r *productRepository) AddProductImages(images []model.ProductImage) error { - if len(images) == 0 { - return nil - } - return r.DB.Create(&images).Error -} - -func (r *productRepository) DeleteProductImagesByProductID(productID string) error { - return r.DB.Where("product_id = ?", productID).Delete(&model.ProductImage{}).Error -} - -func (r *productRepository) DeleteProductImagesByID(imageIDs []string) error { - if err := r.DB.Where("id IN ?", imageIDs).Delete(&model.ProductImage{}).Error; err != nil { - return fmt.Errorf("failed to delete product images: %v", err) - } - return nil -} - -func (r *productRepository) DeleteProductImageByID(imageID string) error { - if err := r.DB.Where("id = ?", imageID).Delete(&model.ProductImage{}).Error; err != nil { - return fmt.Errorf("failed to delete product image: %v", err) - } - return nil -} diff --git a/internal/repositories/rating_repo.go b/internal/repositories/rating_repo.go deleted file mode 100644 index b593cd8..0000000 --- a/internal/repositories/rating_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package repositories - -import ( - "context" - "rijig/config" - "rijig/model" -) - -type PickupRatingRepository interface { - CreateRating(ctx context.Context, rating model.PickupRating) error - GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) - CalculateAverageRating(ctx context.Context, collectorID string) (float32, error) -} - -type pickupRatingRepository struct{} - -func NewPickupRatingRepository() PickupRatingRepository { - return &pickupRatingRepository{} -} - -func (r *pickupRatingRepository) CreateRating(ctx context.Context, rating model.PickupRating) error { - return config.DB.WithContext(ctx).Create(&rating).Error -} - -func (r *pickupRatingRepository) GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) { - var ratings []model.PickupRating - err := config.DB.WithContext(ctx). - Where("collector_id = ?", collectorID). - Order("created_at desc"). - Find(&ratings).Error - if err != nil { - return nil, err - } - return ratings, nil -} - -func (r *pickupRatingRepository) CalculateAverageRating(ctx context.Context, collectorID string) (float32, error) { - var avg float32 - err := config.DB.WithContext(ctx). - Model(&model.PickupRating{}). - Select("AVG(rating)"). - Where("collector_id = ?", collectorID). - Scan(&avg).Error - if err != nil { - return 0, err - } - return avg, nil -} diff --git a/internal/repositories/request_pickup_repo.go b/internal/repositories/request_pickup_repo.go deleted file mode 100644 index 25f3225..0000000 --- a/internal/repositories/request_pickup_repo.go +++ /dev/null @@ -1,143 +0,0 @@ -package repositories - -import ( - "context" - "rijig/config" - "rijig/dto" - "rijig/model" - "time" -) - -type RequestPickupRepository interface { - CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error - GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) - GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) - UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error - GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) - UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error - UpdatePickupStatus(ctx context.Context, pickupID string, status string) error - UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error -} - -type requestPickupRepository struct{} - -func NewRequestPickupRepository() RequestPickupRepository { - return &requestPickupRepository{} -} - -func (r *requestPickupRepository) CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error { - return config.DB.WithContext(ctx).Create(pickup).Error -} - -func (r *requestPickupRepository) GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) { - var pickup model.RequestPickup - err := config.DB.WithContext(ctx). - Preload("RequestItems"). - Preload("Address"). - Where("id = ?", id). - First(&pickup).Error - - if err != nil { - return nil, err - } - return &pickup, nil -} - -func (r *requestPickupRepository) UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error { - return config.DB.WithContext(ctx). - Model(&model.RequestPickup{}). - Where("id = ?", pickupID). - Update("collector_id", collectorID). - Error -} - -func (r *requestPickupRepository) GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) { - var pickups []model.RequestPickup - err := config.DB.WithContext(ctx). - Preload("RequestItems"). - Preload("Address"). - Where("request_method = ?", "otomatis"). - Find(&pickups).Error - - if err != nil { - return nil, err - } - return pickups, nil -} - -func (r *requestPickupRepository) GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) { - var pickups []model.RequestPickup - err := config.DB.WithContext(ctx). - Preload("User"). - Preload("Address"). - Preload("RequestItems"). - Where("collector_id = ? AND status_pickup = ?", collectorID, "waiting_collector"). - Find(&pickups).Error - - if err != nil { - return nil, err - } - return pickups, nil -} - -func (r *requestPickupRepository) UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error { - return config.DB.WithContext(ctx). - Model(&model.RequestPickup{}). - Where("id = ?", pickupID). - Updates(map[string]interface{}{ - "status_pickup": status, - "confirmed_by_collector_at": confirmedAt, - }).Error -} - -func (r *requestPickupRepository) UpdatePickupStatus(ctx context.Context, pickupID string, status string) error { - return config.DB.WithContext(ctx). - Model(&model.RequestPickup{}). - Where("id = ?", pickupID). - Update("status_pickup", status). - Error -} - -func (r *requestPickupRepository) UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error { - // ambil collector_id dulu dari pickup - var pickup model.RequestPickup - if err := config.DB.WithContext(ctx). - Select("collector_id"). - Where("id = ?", pickupID). - First(&pickup).Error; err != nil { - return err - } - - for _, item := range items { - var pickupItem model.RequestPickupItem - err := config.DB.WithContext(ctx). - Where("id = ? AND request_pickup_id = ?", item.ItemID, pickupID). - First(&pickupItem).Error - if err != nil { - return err - } - - var price float64 - err = config.DB.WithContext(ctx). - Model(&model.AvaibleTrashByCollector{}). - Where("collector_id = ? AND trash_category_id = ?", pickup.CollectorID, pickupItem.TrashCategoryId). - Select("price"). - Scan(&price).Error - if err != nil { - return err - } - - finalPrice := item.Amount * price - err = config.DB.WithContext(ctx). - Model(&model.RequestPickupItem{}). - Where("id = ?", item.ItemID). - Updates(map[string]interface{}{ - "estimated_amount": item.Amount, - "final_price": finalPrice, - }).Error - if err != nil { - return err - } - } - return nil -} diff --git a/internal/repositories/role_repo.go b/internal/repositories/role_repo.go deleted file mode 100644 index e048df8..0000000 --- a/internal/repositories/role_repo.go +++ /dev/null @@ -1,49 +0,0 @@ -package repositories - -import ( - "context" - "rijig/model" - - "gorm.io/gorm" -) - -type RoleRepository interface { - FindByID(ctx context.Context, id string) (*model.Role, error) - FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) - FindAll(ctx context.Context) ([]model.Role, error) -} - -type roleRepository struct { - db *gorm.DB -} - -func NewRoleRepository(db *gorm.DB) RoleRepository { - return &roleRepository{db} -} - -func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) { - var role model.Role - err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error - if err != nil { - return nil, err - } - return &role, nil -} - -func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) { - var role model.Role - err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error - if err != nil { - return nil, err - } - return &role, nil -} - -func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) { - var roles []model.Role - err := r.db.WithContext(ctx).Find(&roles).Error - if err != nil { - return nil, err - } - return roles, nil -} diff --git a/internal/repositories/store_repo.go b/internal/repositories/store_repo.go deleted file mode 100644 index 9b02bb7..0000000 --- a/internal/repositories/store_repo.go +++ /dev/null @@ -1,88 +0,0 @@ -package repositories - -import ( - "fmt" - - "rijig/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 { - 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) 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 { - 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 -} - -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/repositories/trash_repo.go b/internal/repositories/trash_repo.go deleted file mode 100644 index 19bee10..0000000 --- a/internal/repositories/trash_repo.go +++ /dev/null @@ -1,175 +0,0 @@ -package repositories - -import ( - "context" - "errors" - "fmt" - "log" - - "rijig/config" - "rijig/model" - - "gorm.io/gorm" -) - -type TrashRepository interface { - CreateCategory(category *model.TrashCategory) error - AddDetailToCategory(detail *model.TrashDetail) error - GetCategories() ([]model.TrashCategory, error) - GetCategoryByID(id string) (*model.TrashCategory, error) - FindCategoryId(id string) (*model.TrashCategory, error) - GetTrashCategoryByName(name string) (*model.TrashCategory, error) - GetTrashDetailByID(id string) (*model.TrashDetail, error) - GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) - GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) - UpdateCategoryName(id string, newName string) error - UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) - UpdateTrashDetail(id string, description string, price float64) error - UpdateEstimatedPrice(ctx context.Context, trashCategoryID string) error - DeleteCategory(id string) error - DeleteTrashDetail(id string) error -} - -type trashRepository struct { - DB *gorm.DB -} - -func NewTrashRepository(db *gorm.DB) TrashRepository { - return &trashRepository{DB: db} -} - -func (r *trashRepository) CreateCategory(category *model.TrashCategory) error { - if err := r.DB.Create(category).Error; err != nil { - return fmt.Errorf("failed to create category: %v", err) - } - return nil -} - -func (r *trashRepository) AddDetailToCategory(detail *model.TrashDetail) error { - if err := r.DB.Create(detail).Error; err != nil { - return fmt.Errorf("failed to add detail to category: %v", err) - } - return nil -} - -func (r *trashRepository) GetCategories() ([]model.TrashCategory, error) { - var categories []model.TrashCategory - if err := r.DB.Preload("Details").Find(&categories).Error; err != nil { - return nil, fmt.Errorf("failed to fetch categories: %v", err) - } - return categories, nil -} - -func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, error) { - var category model.TrashCategory - - if err := r.DB.Preload("Details").First(&category, "id = ?", id).Error; err != nil { - return nil, fmt.Errorf("category not found: %v", err) - } - return &category, nil -} - -func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { - var trash model.TrashCategory - if err := config.DB.WithContext(ctx).First(&trash, "id = ?", id).Error; err != nil { - return nil, err - } - return &trash, nil -} - -func (r *trashRepository) FindCategoryId(id string) (*model.TrashCategory, error) { - var category model.TrashCategory - - if err := r.DB.First(&category, "id = ?", id).Error; err != nil { - return nil, fmt.Errorf("category not found: %v", err) - } - return &category, nil -} - -func (r *trashRepository) GetTrashCategoryByName(name string) (*model.TrashCategory, error) { - var category model.TrashCategory - - if err := r.DB.Find(&category, "name = ?", name).Error; err != nil { - return nil, fmt.Errorf("category not found: %v", err) - } - return &category, nil -} - -func (r *trashRepository) GetTrashDetailByID(id string) (*model.TrashDetail, error) { - var detail model.TrashDetail - if err := r.DB.First(&detail, "id = ?", id).Error; err != nil { - return nil, fmt.Errorf("trash detail not found: %v", err) - } - return &detail, nil -} - -func (r *trashRepository) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) { - var details []model.TrashDetail - - if err := r.DB.Where("category_id = ?", categoryID).Find(&details).Error; err != nil { - return nil, fmt.Errorf("failed to fetch details for category %s: %v", categoryID, err) - } - return details, nil -} - -func (r *trashRepository) UpdateCategoryName(id string, newName string) error { - if err := r.DB.Model(&model.TrashCategory{}).Where("id = ?", id).Update("name", newName).Error; err != nil { - return fmt.Errorf("failed to update category name: %v", err) - } - return nil -} - -func (r *trashRepository) UpdateCategory(id string, updateTrashCategory *model.TrashCategory) (*model.TrashCategory, error) { - var existingtrashCtgry model.TrashCategory - if err := r.DB.Where("id = ?", id).First(&existingtrashCtgry).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("trashCategory with ID %s not found", id) - } - log.Printf("Error fetching trash category for update: %v", err) - return nil, fmt.Errorf("error fetching trash category for update: %w", err) - } - - if err := r.DB.Save(&existingtrashCtgry).Error; err != nil { - log.Printf("Error updating trash category: %v", err) - return nil, fmt.Errorf("failed to update trash category: %w", err) - } - return &existingtrashCtgry, nil -} - -func (r *trashRepository) UpdateTrashDetail(id string, description string, price float64) error { - if err := r.DB.Model(&model.TrashDetail{}).Where("id = ?", id).Updates(model.TrashDetail{Description: description, Price: price}).Error; err != nil { - return fmt.Errorf("failed to update trash detail: %v", err) - } - return nil -} - -func (r *trashRepository) UpdateEstimatedPrice(ctx context.Context, trashCategoryID string) error { - var avg float64 - - err := config.DB.WithContext(ctx). - Model(&model.AvaibleTrashByCollector{}). - Where("trash_category_id = ?", trashCategoryID). - Select("AVG(price)").Scan(&avg).Error - if err != nil { - return err - } - - return config.DB.WithContext(ctx). - Model(&model.TrashCategory{}). - Where("id = ?", trashCategoryID). - Update("estimated_price", avg).Error -} - -func (r *trashRepository) DeleteCategory(id string) error { - if err := r.DB.Delete(&model.TrashCategory{}, "id = ?", id).Error; err != nil { - return fmt.Errorf("failed to delete category: %v", err) - } - return nil -} - -func (r *trashRepository) DeleteTrashDetail(id string) error { - if err := r.DB.Delete(&model.TrashDetail{}, "id = ?", id).Error; err != nil { - return fmt.Errorf("failed to delete trash detail: %v", err) - } - return nil -} diff --git a/internal/repositories/trashcart_repo.go b/internal/repositories/trashcart_repo.go deleted file mode 100644 index 81c7e20..0000000 --- a/internal/repositories/trashcart_repo.go +++ /dev/null @@ -1,166 +0,0 @@ -package repositories - -import ( - "context" - "errors" - "fmt" - - "rijig/config" - "rijig/model" - - "gorm.io/gorm" -) - -type CartRepository interface { - FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) - AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error - DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error - GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) - UpdateCartTotals(ctx context.Context, cartID string) error - DeleteCart(ctx context.Context, userID string) error - // New method for batch cart creation - CreateCartWithItems(ctx context.Context, cart *model.Cart) error - // Check if user already has a cart - HasExistingCart(ctx context.Context, userID string) (bool, error) -} - -type cartRepository struct{} - -func NewCartRepository() CartRepository { - return &cartRepository{} -} - -func (r *cartRepository) FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) { - var cart model.Cart - db := config.DB.WithContext(ctx) - - err := db. - Preload("CartItems.TrashCategory"). - Where("user_id = ?", userID). - First(&cart).Error - - if err == nil { - return &cart, nil - } - - if errors.Is(err, gorm.ErrRecordNotFound) { - newCart := model.Cart{ - UserID: userID, - TotalAmount: 0, - EstimatedTotalPrice: 0, - } - if err := db.Create(&newCart).Error; err != nil { - return nil, err - } - return &newCart, nil - } - - return nil, err -} - -func (r *cartRepository) AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error { - db := config.DB.WithContext(ctx) - - var item model.CartItem - err := db. - Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). - First(&item).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - newItem := model.CartItem{ - CartID: cartID, - TrashCategoryID: trashCategoryID, - Amount: amount, - SubTotalEstimatedPrice: amount * estimatedPrice, - } - return db.Create(&newItem).Error - } - - if err != nil { - return err - } - - item.Amount = amount - item.SubTotalEstimatedPrice = amount * estimatedPrice - return db.Save(&item).Error -} - -func (r *cartRepository) DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error { - db := config.DB.WithContext(ctx) - return db.Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID). - Delete(&model.CartItem{}).Error -} - -func (r *cartRepository) GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) { - var cart model.Cart - db := config.DB.WithContext(ctx) - - err := db. - Preload("CartItems.TrashCategory"). - Where("user_id = ?", userID). - First(&cart).Error - - if err != nil { - return nil, err - } - return &cart, nil -} - -func (r *cartRepository) UpdateCartTotals(ctx context.Context, cartID string) error { - db := config.DB.WithContext(ctx) - - var items []model.CartItem - if err := db.Where("cart_id = ?", cartID).Find(&items).Error; err != nil { - return err - } - - var totalAmount float64 - var totalPrice float64 - - for _, item := range items { - totalAmount += item.Amount - totalPrice += item.SubTotalEstimatedPrice - } - - return db.Model(&model.Cart{}). - Where("id = ?", cartID). - Updates(map[string]interface{}{ - "total_amount": totalAmount, - "estimated_total_price": totalPrice, - }).Error -} - -func (r *cartRepository) DeleteCart(ctx context.Context, userID string) error { - db := config.DB.WithContext(ctx) - var cart model.Cart - if err := db.Where("user_id = ?", userID).First(&cart).Error; err != nil { - return err - } - return db.Delete(&cart).Error -} - -// New method for batch cart creation with transaction -func (r *cartRepository) CreateCartWithItems(ctx context.Context, cart *model.Cart) error { - db := config.DB.WithContext(ctx) - - // Use transaction to ensure data consistency - return db.Transaction(func(tx *gorm.DB) error { - if err := tx.Create(cart).Error; err != nil { - return fmt.Errorf("failed to create cart: %w", err) - } - return nil - }) -} - -// Check if user already has a cart -func (r *cartRepository) HasExistingCart(ctx context.Context, userID string) (bool, error) { - db := config.DB.WithContext(ctx) - - var count int64 - err := db.Model(&model.Cart{}).Where("user_id = ?", userID).Count(&count).Error - if err != nil { - return false, err - } - - return count > 0, nil -} diff --git a/internal/repositories/user_repo.go b/internal/repositories/user_repo.go deleted file mode 100644 index 8c7e085..0000000 --- a/internal/repositories/user_repo.go +++ /dev/null @@ -1,77 +0,0 @@ -package repositories - -import ( - "fmt" - "rijig/model" - - "gorm.io/gorm" -) - -type UserProfilRepository interface { - FindByID(userID string) (*model.User, error) - FindAll(page, limit int) ([]model.User, error) - Update(user *model.User) error - UpdateAvatar(userID, avatarURL string) error - UpdatePassword(userID string, newPassword string) error -} - -type userProfilRepository struct { - DB *gorm.DB -} - -func NewUserProfilRepository(db *gorm.DB) UserProfilRepository { - return &userProfilRepository{DB: db} -} - -func (r *userProfilRepository) FindByID(userID string) (*model.User, error) { - var user model.User - err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, fmt.Errorf("user with ID %s not found", userID) - } - return nil, fmt.Errorf("error finding user with ID %s: %v", userID, err) - } - - if user.Role == nil { - return nil, fmt.Errorf("role not found for user ID %s", userID) - } - - return &user, nil -} - -func (r *userProfilRepository) FindAll(page, limit int) ([]model.User, error) { - var users []model.User - offset := (page - 1) * limit - err := r.DB.Preload("Role").Offset(offset).Limit(limit).Find(&users).Error - if err != nil { - return nil, fmt.Errorf("error finding all users: %v", err) - } - return users, nil -} - -func (r *userProfilRepository) Update(user *model.User) error { - err := r.DB.Save(user).Error - if err != nil { - return fmt.Errorf("error updating user: %v", err) - } - return nil -} - -func (r *userProfilRepository) UpdateAvatar(userID, avatarURL string) error { - var user model.User - err := r.DB.Model(&user).Where("id = ?", userID).Update("avatar", avatarURL).Error - if err != nil { - return fmt.Errorf("error updating avatar for user ID %s: %v", userID, err) - } - return nil -} - -func (r *userProfilRepository) UpdatePassword(userID string, newPassword string) error { - var user model.User - err := r.DB.Model(&user).Where("id = ?", userID).Update("password", newPassword).Error - if err != nil { - return fmt.Errorf("error updating password for user ID %s: %v", userID, err) - } - return nil -} diff --git a/internal/repositories/userpin_repo.go b/internal/repositories/userpin_repo.go deleted file mode 100644 index 47d7118..0000000 --- a/internal/repositories/userpin_repo.go +++ /dev/null @@ -1,60 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type UserPinRepository interface { - FindByUserID(userID string) (*model.UserPin, error) - FindByPin(userPin string) (*model.UserPin, error) - Create(userPin *model.UserPin) error - Update(userPin *model.UserPin) error -} - -type userPinRepository struct { - DB *gorm.DB -} - -func NewUserPinRepository(db *gorm.DB) UserPinRepository { - return &userPinRepository{DB: db} -} - -func (r *userPinRepository) FindByUserID(userID string) (*model.UserPin, error) { - var userPin model.UserPin - err := r.DB.Where("user_id = ?", userID).First(&userPin).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - - return nil, nil - } - return nil, err - } - return &userPin, nil -} - -func (r *userPinRepository) FindByPin(pin string) (*model.UserPin, error) { - var userPin model.UserPin - err := r.DB.Where("pin = ?", pin).First(&userPin).Error - if err != nil { - return nil, err - } - return &userPin, nil -} - -func (r *userPinRepository) Create(userPin *model.UserPin) error { - err := r.DB.Create(userPin).Error - if err != nil { - return err - } - return nil -} - -func (r *userPinRepository) Update(userPin *model.UserPin) error { - err := r.DB.Save(userPin).Error - if err != nil { - return err - } - return nil -} diff --git a/internal/repositories/wilayah_indonesia_repo.go b/internal/repositories/wilayah_indonesia_repo.go deleted file mode 100644 index 60a53bf..0000000 --- a/internal/repositories/wilayah_indonesia_repo.go +++ /dev/null @@ -1,244 +0,0 @@ -package repositories - -import ( - "rijig/model" - - "gorm.io/gorm" -) - -type WilayahIndonesiaRepository interface { - ImportProvinces(provinces []model.Province) error - ImportRegencies(regencies []model.Regency) error - ImportDistricts(districts []model.District) error - ImportVillages(villages []model.Village) error - - FindAllProvinces(page, limit int) ([]model.Province, int, error) - FindProvinceByID(id string, page, limit int) (*model.Province, int, error) - - FindAllRegencies(page, limit int) ([]model.Regency, int, error) - FindRegencyByID(id string, page, limit int) (*model.Regency, int, error) - - FindAllDistricts(page, limit int) ([]model.District, int, error) - FindDistrictByID(id string, page, limit int) (*model.District, int, error) - - FindAllVillages(page, limit int) ([]model.Village, int, error) - FindVillageByID(id string) (*model.Village, error) -} - -type wilayahIndonesiaRepository struct { - DB *gorm.DB -} - -func NewWilayahIndonesiaRepository(db *gorm.DB) WilayahIndonesiaRepository { - return &wilayahIndonesiaRepository{DB: db} -} - -func (r *wilayahIndonesiaRepository) ImportProvinces(provinces []model.Province) error { - for _, province := range provinces { - if err := r.DB.Create(&province).Error; err != nil { - return err - } - } - return nil -} - -func (r *wilayahIndonesiaRepository) ImportRegencies(regencies []model.Regency) error { - for _, regency := range regencies { - if err := r.DB.Create(®ency).Error; err != nil { - return err - } - } - return nil -} - -func (r *wilayahIndonesiaRepository) ImportDistricts(districts []model.District) error { - for _, district := range districts { - if err := r.DB.Create(&district).Error; err != nil { - return err - } - } - return nil -} - -func (r *wilayahIndonesiaRepository) ImportVillages(villages []model.Village) error { - for _, village := range villages { - if err := r.DB.Create(&village).Error; err != nil { - return err - } - } - return nil -} - -func (r *wilayahIndonesiaRepository) FindAllProvinces(page, limit int) ([]model.Province, int, error) { - var provinces []model.Province - var total int64 - - err := r.DB.Model(&model.Province{}).Count(&total).Error - if err != nil { - return nil, 0, err - } - - if page > 0 && limit > 0 { - err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&provinces).Error - if err != nil { - return nil, 0, err - } - } else { - - err := r.DB.Find(&provinces).Error - if err != nil { - return nil, 0, err - } - } - - return provinces, int(total), nil -} - -func (r *wilayahIndonesiaRepository) FindProvinceByID(id string, page, limit int) (*model.Province, int, error) { - var province model.Province - - err := r.DB.Preload("Regencies", func(db *gorm.DB) *gorm.DB { - if page > 0 && limit > 0 { - - return db.Offset((page - 1) * limit).Limit(limit) - } - - return db - }).Where("id = ?", id).First(&province).Error - if err != nil { - return nil, 0, err - } - - var totalRegencies int64 - r.DB.Model(&model.Regency{}).Where("province_id = ?", id).Count(&totalRegencies) - - return &province, int(totalRegencies), nil -} - -func (r *wilayahIndonesiaRepository) FindAllRegencies(page, limit int) ([]model.Regency, int, error) { - var regencies []model.Regency - var total int64 - - err := r.DB.Model(&model.Regency{}).Count(&total).Error - if err != nil { - return nil, 0, err - } - - if page > 0 && limit > 0 { - err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(®encies).Error - if err != nil { - return nil, 0, err - } - } else { - - err := r.DB.Find(®encies).Error - if err != nil { - return nil, 0, err - } - } - - return regencies, int(total), nil -} - -func (r *wilayahIndonesiaRepository) FindRegencyByID(id string, page, limit int) (*model.Regency, int, error) { - var regency model.Regency - - err := r.DB.Preload("Districts", func(db *gorm.DB) *gorm.DB { - if page > 0 && limit > 0 { - return db.Offset((page - 1) * limit).Limit(limit) - } - return db - }).Where("id = ?", id).First(®ency).Error - - if err != nil { - return nil, 0, err - } - - var totalDistricts int64 - err = r.DB.Model(&model.District{}).Where("regency_id = ?", id).Count(&totalDistricts).Error - if err != nil { - return nil, 0, err - } - - return ®ency, int(totalDistricts), nil -} - -func (r *wilayahIndonesiaRepository) FindAllDistricts(page, limit int) ([]model.District, int, error) { - var district []model.District - var total int64 - - err := r.DB.Model(&model.District{}).Count(&total).Error - if err != nil { - return nil, 0, err - } - - if page > 0 && limit > 0 { - err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&district).Error - if err != nil { - return nil, 0, err - } - } else { - - err := r.DB.Find(&district).Error - if err != nil { - return nil, 0, err - } - } - - return district, int(total), nil -} - -func (r *wilayahIndonesiaRepository) FindDistrictByID(id string, page, limit int) (*model.District, int, error) { - var district model.District - - err := r.DB.Preload("Villages", func(db *gorm.DB) *gorm.DB { - if page > 0 && limit > 0 { - - return db.Offset((page - 1) * limit).Limit(limit) - } - - return db - }).Where("id = ?", id).First(&district).Error - if err != nil { - return nil, 0, err - } - - var totalVillage int64 - r.DB.Model(&model.Village{}).Where("district_id = ?", id).Count(&totalVillage) - - return &district, int(totalVillage), nil -} - -func (r *wilayahIndonesiaRepository) FindAllVillages(page, limit int) ([]model.Village, int, error) { - var villages []model.Village - var total int64 - - err := r.DB.Model(&model.Village{}).Count(&total).Error - if err != nil { - return nil, 0, err - } - - if page > 0 && limit > 0 { - err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&villages).Error - if err != nil { - return nil, 0, err - } - } else { - - err := r.DB.Find(&villages).Error - if err != nil { - return nil, 0, err - } - } - - return villages, int(total), nil -} - -func (r *wilayahIndonesiaRepository) FindVillageByID(id string) (*model.Village, error) { - var village model.Village - err := r.DB.Where("id = ?", id).First(&village).Error - if err != nil { - return nil, err - } - return &village, nil -} diff --git a/internal/services/about_service.go b/internal/services/about_service.go deleted file mode 100644 index 27a7f17..0000000 --- a/internal/services/about_service.go +++ /dev/null @@ -1,436 +0,0 @@ -package services - -import ( - "fmt" - "log" - "mime/multipart" - "os" - "path/filepath" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type AboutService interface { - CreateAbout(request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) - UpdateAbout(id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) - GetAllAbout() ([]dto.ResponseAboutDTO, error) - GetAboutByID(id string) (*dto.ResponseAboutDTO, error) - GetAboutDetailById(id string) (*dto.ResponseAboutDetailDTO, error) - DeleteAbout(id string) error - - CreateAboutDetail(request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) - UpdateAboutDetail(id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) - DeleteAboutDetail(id string) error -} - -type aboutService struct { - aboutRepo repositories.AboutRepository -} - -func NewAboutService(aboutRepo repositories.AboutRepository) AboutService { - return &aboutService{aboutRepo: aboutRepo} -} - -func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutDetailDTO, error) { - createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) - - response := &dto.ResponseAboutDetailDTO{ - ID: about.ID, - AboutID: about.AboutID, - ImageDetail: about.ImageDetail, - Description: about.Description, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return response, nil -} - -func formatResponseAboutDTO(about *model.About) (*dto.ResponseAboutDTO, error) { - createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) - - response := &dto.ResponseAboutDTO{ - ID: about.ID, - Title: about.Title, - CoverImage: about.CoverImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return response, nil -} - -func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader) (string, error) { - pathImage := "/uploads/coverabout/" - coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage - if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { - - if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { - return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} - extension := filepath.Ext(coverImageAbout.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - coverImageFileName := fmt.Sprintf("%s_coverabout%s", uuid.New().String(), extension) - coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) - - src, err := coverImageAbout.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(coverImagePath) - if err != nil { - return "", fmt.Errorf("failed to create cover image about file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save cover image about: %v", err) - } - - coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) - - return coverImageAboutUrl, nil -} - -func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) { - pathImage := "/uploads/coverabout/coveraboutdetail/" - coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage - if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) { - - if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil { - return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} - extension := filepath.Ext(coverImageAbout.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - coverImageFileName := fmt.Sprintf("%s_coveraboutdetail_%s", uuid.New().String(), extension) - coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName) - - src, err := coverImageAbout.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(coverImagePath) - if err != nil { - return "", fmt.Errorf("failed to create cover image about file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save cover image about: %v", err) - } - - coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName) - - return coverImageAboutUrl, nil -} - -func deleteCoverImageAbout(coverimageAboutPath string) error { - if coverimageAboutPath == "" { - return nil - } - - baseDir := "./public/" + os.Getenv("BASE_URL") - absolutePath := baseDir + coverimageAboutPath - - if _, err := os.Stat(absolutePath); os.IsNotExist(err) { - return fmt.Errorf("image file not found: %v", err) - } - - err := os.Remove(absolutePath) - if err != nil { - return fmt.Errorf("failed to delete image: %v", err) - } - - log.Printf("Image deleted successfully: %s", absolutePath) - return nil -} - -func (s *aboutService) CreateAbout(request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { - errors, valid := request.ValidateAbout() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - coverImageAboutPath, err := s.saveCoverImageAbout(coverImageAbout) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan cover image about: %v ", err) - } - - about := model.About{ - Title: request.Title, - CoverImage: coverImageAboutPath, - } - - if err := s.aboutRepo.CreateAbout(&about); err != nil { - return nil, fmt.Errorf("failed to create About: %v", err) - } - - response, err := formatResponseAboutDTO(&about) - if err != nil { - return nil, fmt.Errorf("error formatting About response: %v", err) - } - - return response, nil -} - -func (s *aboutService) UpdateAbout(id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { - - errors, valid := request.ValidateAbout() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - about, err := s.aboutRepo.GetAboutByID(id) - if err != nil { - return nil, fmt.Errorf("about not found: %v", err) - } - - if about.CoverImage != "" { - err := deleteCoverImageAbout(about.CoverImage) - if err != nil { - return nil, fmt.Errorf("gagal mengahpus gambar lama: %v", err) - } - } - - var coverImageAboutPath string - if coverImageAbout != nil { - coverImageAboutPath, err = s.saveCoverImageAbout(coverImageAbout) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) - } - } - - about.Title = request.Title - if coverImageAboutPath != "" { - about.CoverImage = coverImageAboutPath - } - - updatedAbout, err := s.aboutRepo.UpdateAbout(id, about) - if err != nil { - return nil, fmt.Errorf("failed to update About: %v", err) - } - - response, err := formatResponseAboutDTO(updatedAbout) - if err != nil { - return nil, fmt.Errorf("error formatting About response: %v", err) - } - - return response, nil -} - -func (s *aboutService) GetAllAbout() ([]dto.ResponseAboutDTO, error) { - - aboutList, err := s.aboutRepo.GetAllAbout() - if err != nil { - return nil, fmt.Errorf("failed to get About list: %v", err) - } - - var aboutDTOList []dto.ResponseAboutDTO - for _, about := range aboutList { - response, err := formatResponseAboutDTO(&about) - if err != nil { - log.Printf("Error formatting About response: %v", err) - continue - } - aboutDTOList = append(aboutDTOList, *response) - } - - return aboutDTOList, nil -} - -func (s *aboutService) GetAboutByID(id string) (*dto.ResponseAboutDTO, error) { - - about, err := s.aboutRepo.GetAboutByID(id) - if err != nil { - return nil, fmt.Errorf("about not found: %v", err) - } - - response, err := formatResponseAboutDTO(about) - if err != nil { - return nil, fmt.Errorf("error formatting About response: %v", err) - } - - var responseDetails []dto.ResponseAboutDetailDTO - for _, detail := range about.AboutDetail { - formattedDetail, err := formatResponseAboutDetailDTO(&detail) - if err != nil { - return nil, fmt.Errorf("error formatting AboutDetail response: %v", err) - } - responseDetails = append(responseDetails, *formattedDetail) - } - - response.AboutDetail = &responseDetails - - return response, nil -} - -func (s *aboutService) GetAboutDetailById(id string) (*dto.ResponseAboutDetailDTO, error) { - - about, err := s.aboutRepo.GetAboutDetailByID(id) - if err != nil { - return nil, fmt.Errorf("about not found: %v", err) - } - - response, err := formatResponseAboutDetailDTO(about) - if err != nil { - return nil, fmt.Errorf("error formatting About response: %v", err) - } - - return response, nil -} - -func (s *aboutService) DeleteAbout(id string) error { - about, err := s.aboutRepo.GetAboutByID(id) - if err != nil { - return fmt.Errorf("about not found: %v", err) - } - - if about.CoverImage != "" { - err := deleteCoverImageAbout(about.CoverImage) - if err != nil { - return fmt.Errorf("gagal mengahpus gambar lama: %v", err) - } - } - - if err := s.aboutRepo.DeleteAbout(id); err != nil { - return fmt.Errorf("failed to delete About: %v", err) - } - - return nil -} - -func (s *aboutService) CreateAboutDetail(request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { - - errors, valid := request.ValidateAboutDetail() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - _, err := s.aboutRepo.GetAboutByIDWithoutPrel(request.AboutId) - if err != nil { - return nil, fmt.Errorf("about_id tidak ditemukan: %v", err) - } - - coverImageAboutDetailPath, err := s.saveCoverImageAboutDetail(coverImageAboutDetail) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan cover image about detail: %v ", err) - } - - aboutDetail := model.AboutDetail{ - AboutID: request.AboutId, - ImageDetail: coverImageAboutDetailPath, - Description: request.Description, - } - - if err := s.aboutRepo.CreateAboutDetail(&aboutDetail); err != nil { - return nil, fmt.Errorf("failed to create AboutDetail: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.UpdatedAt) - - response := &dto.ResponseAboutDetailDTO{ - ID: aboutDetail.ID, - AboutID: aboutDetail.AboutID, - ImageDetail: aboutDetail.ImageDetail, - Description: aboutDetail.Description, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return response, nil -} - -func (s *aboutService) UpdateAboutDetail(id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { - - errors, valid := request.ValidateAboutDetail() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - aboutDetail, err := s.aboutRepo.GetAboutDetailByID(id) - if err != nil { - return nil, fmt.Errorf("about detail tidakck ditemukan: %v", err) - } - - if aboutDetail.ImageDetail != "" { - err := deleteCoverImageAbout(aboutDetail.ImageDetail) - if err != nil { - return nil, fmt.Errorf("gagal menghapus gambar lama: %v", err) - } - } - - var coverImageAboutDeatilPath string - if imageDetail != nil { - coverImageAboutDeatilPath, err = s.saveCoverImageAbout(imageDetail) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err) - } - } - - aboutDetail.Description = request.Description - if coverImageAboutDeatilPath != "" { - aboutDetail.ImageDetail = coverImageAboutDeatilPath - } - - aboutDetail, err = s.aboutRepo.UpdateAboutDetail(id, aboutDetail) - if err != nil { - log.Printf("Error updating about detail: %v", err) - return nil, fmt.Errorf("failed to update about detail: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(aboutDetail.UpdatedAt) - - response := &dto.ResponseAboutDetailDTO{ - ID: aboutDetail.ID, - AboutID: aboutDetail.AboutID, - ImageDetail: aboutDetail.ImageDetail, - Description: aboutDetail.Description, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return response, nil -} - -func (s *aboutService) DeleteAboutDetail(id string) error { - aboutDetail, err := s.aboutRepo.GetAboutDetailByID(id) - if err != nil { - return fmt.Errorf("about detail tidakck ditemukan: %v", err) - } - - if aboutDetail.ImageDetail != "" { - err := deleteCoverImageAbout(aboutDetail.ImageDetail) - if err != nil { - return fmt.Errorf("gagal menghapus gambar lama: %v", err) - } - } - - if err := s.aboutRepo.DeleteAboutDetail(id); err != nil { - return fmt.Errorf("failed to delete AboutDetail: %v", err) - } - return nil -} diff --git a/internal/services/address_service.go b/internal/services/address_service.go deleted file mode 100644 index b6b25c8..0000000 --- a/internal/services/address_service.go +++ /dev/null @@ -1,412 +0,0 @@ -package services - -import ( - "fmt" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type AddressService interface { - CreateAddress(userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) - GetAddressByUserID(userID string) ([]dto.AddressResponseDTO, error) - GetAddressByID(userID, id string) (*dto.AddressResponseDTO, error) - UpdateAddress(userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) - DeleteAddress(userID, id string) error -} - -type addressService struct { - AddressRepo repositories.AddressRepository - WilayahRepo repositories.WilayahIndonesiaRepository -} - -func NewAddressService(addressRepo repositories.AddressRepository, wilayahRepo repositories.WilayahIndonesiaRepository) AddressService { - return &addressService{ - AddressRepo: addressRepo, - WilayahRepo: wilayahRepo, - } -} - -func (s *addressService) CreateAddress(userID string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { - - province, _, err := s.WilayahRepo.FindProvinceByID(addressDTO.Province, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid province_id") - } - - regency, _, err := s.WilayahRepo.FindRegencyByID(addressDTO.Regency, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid regency_id") - } - - district, _, err := s.WilayahRepo.FindDistrictByID(addressDTO.District, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid district_id") - } - - village, err := s.WilayahRepo.FindVillageByID(addressDTO.Village) - if err != nil { - return nil, fmt.Errorf("invalid village_id") - } - - address := model.Address{ - UserID: userID, - Province: province.Name, - Regency: regency.Name, - District: district.Name, - Village: village.Name, - PostalCode: addressDTO.PostalCode, - Detail: addressDTO.Detail, - Latitude: addressDTO.Latitude, - Longitude: addressDTO.Longitude, - } - - err = s.AddressRepo.CreateAddress(&address) - if err != nil { - return nil, fmt.Errorf("failed to create address: %v", err) - } - - userCacheKey := fmt.Sprintf("user:%s:addresses", userID) - utils.DeleteData(userCacheKey) - - createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) - - addressResponseDTO := &dto.AddressResponseDTO{ - UserID: address.UserID, - ID: address.ID, - Province: address.Province, - Regency: address.Regency, - District: address.District, - Village: address.Village, - PostalCode: address.PostalCode, - Detail: address.Detail, - Latitude: address.Latitude, - Longitude: address.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("address:%s", address.ID) - cacheData := map[string]interface{}{ - "data": addressResponseDTO, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching new address to Redis: %v\n", err) - } - - addresses, err := s.AddressRepo.FindAddressByUserID(userID) - if err != nil { - return nil, fmt.Errorf("failed to fetch updated addresses for user: %v", err) - } - - var addressDTOs []dto.AddressResponseDTO - for _, addr := range addresses { - createdAt, _ := utils.FormatDateToIndonesianFormat(addr.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(addr.UpdatedAt) - - addressDTOs = append(addressDTOs, dto.AddressResponseDTO{ - UserID: addr.UserID, - ID: addr.ID, - Province: addr.Province, - Regency: addr.Regency, - District: addr.District, - Village: addr.Village, - PostalCode: addr.PostalCode, - Detail: addr.Detail, - Latitude: addr.Latitude, - Longitude: addr.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData = map[string]interface{}{ - "data": addressDTOs, - } - err = utils.SetJSONData(userCacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated user addresses to Redis: %v\n", err) - } - - return addressResponseDTO, nil -} - -func (s *addressService) GetAddressByUserID(userID string) ([]dto.AddressResponseDTO, error) { - - cacheKey := fmt.Sprintf("user:%s:addresses", userID) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var addresses []dto.AddressResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - addressData, ok := item.(map[string]interface{}) - if ok { - addresses = append(addresses, dto.AddressResponseDTO{ - UserID: addressData["user_id"].(string), - ID: addressData["address_id"].(string), - Province: addressData["province"].(string), - Regency: addressData["regency"].(string), - District: addressData["district"].(string), - Village: addressData["village"].(string), - PostalCode: addressData["postalCode"].(string), - Detail: addressData["detail"].(string), - Latitude: addressData["latitude"].(float64), - Longitude: addressData["longitude"].(float64), - CreatedAt: addressData["createdAt"].(string), - UpdatedAt: addressData["updatedAt"].(string), - }) - } - } - return addresses, nil - } - } - - addresses, err := s.AddressRepo.FindAddressByUserID(userID) - if err != nil { - return nil, fmt.Errorf("failed to fetch addresses: %v", err) - } - - var addressDTOs []dto.AddressResponseDTO - for _, address := range addresses { - createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) - - addressDTOs = append(addressDTOs, dto.AddressResponseDTO{ - UserID: address.UserID, - ID: address.ID, - Province: address.Province, - Regency: address.Regency, - District: address.District, - Village: address.Village, - PostalCode: address.PostalCode, - Detail: address.Detail, - Latitude: address.Latitude, - Longitude: address.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": addressDTOs, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching addresses to Redis: %v\n", err) - } - - return addressDTOs, nil -} - -func (s *addressService) GetAddressByID(userID, id string) (*dto.AddressResponseDTO, error) { - address, err := s.AddressRepo.FindAddressByID(id) - if err != nil { - return nil, fmt.Errorf("address not found: %v", err) - } - - if address.UserID != userID { - return nil, fmt.Errorf("you are not authorized to update this address") - } - - cacheKey := fmt.Sprintf("address:%s", id) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - addressData, ok := cachedData["data"].(map[string]interface{}) - if ok { - address := dto.AddressResponseDTO{ - UserID: addressData["user_id"].(string), - ID: addressData["address_id"].(string), - Province: addressData["province"].(string), - Regency: addressData["regency"].(string), - District: addressData["district"].(string), - Village: addressData["village"].(string), - PostalCode: addressData["postalCode"].(string), - Detail: addressData["detail"].(string), - Latitude: addressData["latitude"].(float64), - Longitude: addressData["longitude"].(float64), - CreatedAt: addressData["createdAt"].(string), - UpdatedAt: addressData["updatedAt"].(string), - } - return &address, nil - } - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) - - addressDTO := &dto.AddressResponseDTO{ - UserID: address.UserID, - ID: address.ID, - Province: address.Province, - Regency: address.Regency, - District: address.District, - Village: address.Village, - PostalCode: address.PostalCode, - Detail: address.Detail, - Latitude: address.Latitude, - Longitude: address.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": addressDTO, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching address to Redis: %v\n", err) - } - - return addressDTO, nil -} - -func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { - - address, err := s.AddressRepo.FindAddressByID(id) - if err != nil { - return nil, fmt.Errorf("address not found: %v", err) - } - - if address.UserID != userID { - return nil, fmt.Errorf("you are not authorized to update this address") - } - - province, _, err := s.WilayahRepo.FindProvinceByID(addressDTO.Province, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid province_id") - } - - regency, _, err := s.WilayahRepo.FindRegencyByID(addressDTO.Regency, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid regency_id") - } - - district, _, err := s.WilayahRepo.FindDistrictByID(addressDTO.District, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid district_id") - } - - village, err := s.WilayahRepo.FindVillageByID(addressDTO.Village) - if err != nil { - return nil, fmt.Errorf("invalid village_id") - } - - address.Province = province.Name - address.Regency = regency.Name - address.District = district.Name - address.Village = village.Name - address.PostalCode = addressDTO.PostalCode - address.Detail = addressDTO.Detail - address.Latitude = addressDTO.Latitude - address.Longitude = addressDTO.Longitude - // address.UpdatedAt = time.Now() - - err = s.AddressRepo.UpdateAddress(address) - if err != nil { - return nil, fmt.Errorf("failed to update address: %v", err) - } - - addressCacheKey := fmt.Sprintf("address:%s", id) - utils.DeleteData(addressCacheKey) - - userAddressesCacheKey := fmt.Sprintf("user:%s:addresses", userID) - utils.DeleteData(userAddressesCacheKey) - - createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) - - addressResponseDTO := &dto.AddressResponseDTO{ - UserID: address.UserID, - ID: address.ID, - Province: address.Province, - Regency: address.Regency, - District: address.District, - Village: address.Village, - PostalCode: address.PostalCode, - Detail: address.Detail, - Latitude: address.Latitude, - Longitude: address.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": addressResponseDTO, - } - err = utils.SetJSONData(addressCacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated address to Redis: %v\n", err) - } - - addresses, err := s.AddressRepo.FindAddressByUserID(address.UserID) - if err != nil { - return nil, fmt.Errorf("failed to fetch updated addresses for user: %v", err) - } - - var addressDTOs []dto.AddressResponseDTO - for _, addr := range addresses { - createdAt, _ := utils.FormatDateToIndonesianFormat(addr.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(addr.UpdatedAt) - - addressDTOs = append(addressDTOs, dto.AddressResponseDTO{ - UserID: addr.UserID, - ID: addr.ID, - Province: addr.Province, - Regency: addr.Regency, - District: addr.District, - Village: addr.Village, - PostalCode: addr.PostalCode, - Detail: addr.Detail, - Latitude: addr.Latitude, - Longitude: addr.Longitude, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData = map[string]interface{}{ - "data": addressDTOs, - } - err = utils.SetJSONData(userAddressesCacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated user addresses to Redis: %v\n", err) - } - - return addressResponseDTO, nil -} - -func (s *addressService) DeleteAddress(userID, addressID string) error { - - address, err := s.AddressRepo.FindAddressByID(addressID) - if err != nil { - return fmt.Errorf("address not found: %v", err) - } - - if address.UserID != userID { - return fmt.Errorf("you are not authorized to delete this address") - } - - err = s.AddressRepo.DeleteAddress(addressID) - if err != nil { - return fmt.Errorf("failed to delete address: %v", err) - } - - addressCacheKey := fmt.Sprintf("address:%s", addressID) - err = utils.DeleteData(addressCacheKey) - if err != nil { - fmt.Printf("Error deleting address cache: %v\n", err) - } - - userAddressesCacheKey := fmt.Sprintf("user:%s:addresses", address.UserID) - err = utils.DeleteData(userAddressesCacheKey) - if err != nil { - fmt.Printf("Error deleting user addresses cache: %v\n", err) - } - - return nil -} diff --git a/internal/services/article_service.go b/internal/services/article_service.go deleted file mode 100644 index a2e10ee..0000000 --- a/internal/services/article_service.go +++ /dev/null @@ -1,415 +0,0 @@ -package services - -import ( - "encoding/json" - "fmt" - "log" - "mime/multipart" - "os" - "path/filepath" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type ArticleService interface { - CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) - GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) - GetArticleByID(id string) (*dto.ArticleResponseDTO, error) - UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) - DeleteArticle(id string) error -} - -type articleService struct { - ArticleRepo repositories.ArticleRepository -} - -func NewArticleService(articleRepo repositories.ArticleRepository) ArticleService { - return &articleService{ArticleRepo: articleRepo} -} - -func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) { - pathImage := "/uploads/articles/" - coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage - if _, err := os.Stat(coverArticleDir); os.IsNotExist(err) { - - if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for cover article: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} - extension := filepath.Ext(coverArticle.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension) - coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName) - - src, err := coverArticle.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(coverArticlePath) - if err != nil { - return "", fmt.Errorf("failed to create cover article file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save cover article: %v", err) - } - - iconTrashUrl := fmt.Sprintf("%s%s", pathImage, coverArticleFileName) - - return iconTrashUrl, nil -} - -func deleteCoverArticle(imagePath string) error { - if imagePath == "" { - return nil - } - - baseDir := "./public/" + os.Getenv("BASE_URL") - absolutePath := baseDir + imagePath - - if _, err := os.Stat(absolutePath); os.IsNotExist(err) { - return fmt.Errorf("image file not found: %v", err) - } - - err := os.Remove(absolutePath) - if err != nil { - return fmt.Errorf("failed to delete image: %v", err) - } - - log.Printf("Image deleted successfully: %s", absolutePath) - return nil -} - -func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { - - coverArticlePath, err := s.saveCoverArticle(coverImage) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) - } - - article := model.Article{ - Title: request.Title, - CoverImage: coverArticlePath, - Author: request.Author, - Heading: request.Heading, - Content: request.Content, - } - - if err := s.ArticleRepo.CreateArticle(&article); err != nil { - return nil, fmt.Errorf("failed to create article: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) - - articleResponseDTO := &dto.ArticleResponseDTO{ - ID: article.ID, - Title: article.Title, - CoverImage: article.CoverImage, - Author: article.Author, - Heading: article.Heading, - Content: article.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("article:%s", article.ID) - cacheData := map[string]interface{}{ - "data": articleResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching article to Redis: %v\n", err) - } - - articles, total, err := s.ArticleRepo.FindAllArticles(0, 0) - if err != nil { - fmt.Printf("Error fetching all articles: %v\n", err) - } - - var articleDTOs []dto.ArticleResponseDTO - for _, a := range articles { - createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) - - articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ - ID: a.ID, - Title: a.Title, - CoverImage: a.CoverImage, - Author: a.Author, - Heading: a.Heading, - Content: a.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - articlesCacheKey := "articles:all" - cacheData = map[string]interface{}{ - "data": articleDTOs, - "total": total, - } - if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching all articles to Redis: %v\n", err) - } - - return articleResponseDTO, nil -} - -func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) { - var cacheKey string - - if page == 0 && limit == 0 { - cacheKey = "articles:all" - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - if data, ok := cachedData["data"].([]interface{}); ok { - var articles []dto.ArticleResponseDTO - for _, item := range data { - articleData, ok := item.(map[string]interface{}) - if ok { - articles = append(articles, dto.ArticleResponseDTO{ - ID: articleData["article_id"].(string), - Title: articleData["title"].(string), - CoverImage: articleData["coverImage"].(string), - Author: articleData["author"].(string), - Heading: articleData["heading"].(string), - Content: articleData["content"].(string), - PublishedAt: articleData["publishedAt"].(string), - UpdatedAt: articleData["updatedAt"].(string), - }) - } - } - - if total, ok := cachedData["total"].(float64); ok { - fmt.Printf("Cached Total Articles: %f\n", total) - return articles, int(total), nil - } else { - fmt.Println("Total articles not found in cache, using 0 as fallback.") - return articles, 0, nil - } - } - } - } - - articles, total, err := s.ArticleRepo.FindAllArticles(page, limit) - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch articles: %v", err) - } - - fmt.Printf("Total Articles from Database: %d\n", total) - - var articleDTOs []dto.ArticleResponseDTO - for _, article := range articles { - publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) - - articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ - ID: article.ID, - Title: article.Title, - CoverImage: article.CoverImage, - Author: article.Author, - Heading: article.Heading, - Content: article.Content, - PublishedAt: publishedAt, - UpdatedAt: updatedAt, - }) - } - - cacheKey = fmt.Sprintf("articles_page:%d_limit:%d", page, limit) - cacheData := map[string]interface{}{ - "data": articleDTOs, - "total": total, - } - - fmt.Printf("Setting cache with total: %d\n", total) - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching articles to Redis: %v\n", err) - } - - return articleDTOs, total, nil -} - -func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) { - - cacheKey := fmt.Sprintf("article:%s", id) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - articleResponse := &dto.ArticleResponseDTO{} - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), articleResponse); err == nil { - return articleResponse, nil - } - } - } - - article, err := s.ArticleRepo.FindArticleByID(id) - if err != nil { - return nil, fmt.Errorf("failed to fetch article by ID: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt) - - articleResponseDTO := &dto.ArticleResponseDTO{ - ID: article.ID, - Title: article.Title, - CoverImage: article.CoverImage, - Author: article.Author, - Heading: article.Heading, - Content: article.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": articleResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching article to Redis: %v\n", err) - } - - return articleResponseDTO, nil -} - -func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) { - article, err := s.ArticleRepo.FindArticleByID(id) - if err != nil { - return nil, fmt.Errorf("article not found: %v", id) - } - - if article.CoverImage != "" { - err := deleteCoverArticle(article.CoverImage) - if err != nil { - return nil, fmt.Errorf("failed to delete old image: %v", err) - } - } - - var coverArticlePath string - if coverImage != nil { - coverArticlePath, err = s.saveCoverArticle(coverImage) - if err != nil { - return nil, fmt.Errorf("failed to save card photo: %v", err) - } - } - - if coverArticlePath != "" { - article.CoverImage = coverArticlePath - } - - article.Title = request.Title - article.Heading = request.Heading - article.Content = request.Content - article.Author = request.Author - - err = s.ArticleRepo.UpdateArticle(id, article) - if err != nil { - return nil, fmt.Errorf("failed to update article: %v", err) - } - - updatedArticle, err := s.ArticleRepo.FindArticleByID(id) - if err != nil { - return nil, fmt.Errorf("failed to fetch updated article: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.UpdatedAt) - - articleResponseDTO := &dto.ArticleResponseDTO{ - ID: updatedArticle.ID, - Title: updatedArticle.Title, - CoverImage: updatedArticle.CoverImage, - Author: updatedArticle.Author, - Heading: updatedArticle.Heading, - Content: updatedArticle.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - } - - articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID) - err = utils.SetJSONData(articleCacheKey, map[string]interface{}{"data": articleResponseDTO}, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated article to Redis: %v\n", err) - } - - articlesCacheKey := "articles:all" - err = utils.DeleteData(articlesCacheKey) - if err != nil { - fmt.Printf("Error deleting articles cache: %v\n", err) - } - - articles, _, err := s.ArticleRepo.FindAllArticles(0, 0) - if err != nil { - fmt.Printf("Error fetching all articles: %v\n", err) - } else { - var articleDTOs []dto.ArticleResponseDTO - for _, a := range articles { - createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt) - - articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{ - ID: a.ID, - Title: a.Title, - CoverImage: a.CoverImage, - Author: a.Author, - Heading: a.Heading, - Content: a.Content, - PublishedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": articleDTOs, - } - err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated articles to Redis: %v\n", err) - } - } - - return articleResponseDTO, nil -} - -func (s *articleService) DeleteArticle(id string) error { - article, err := s.ArticleRepo.FindArticleByID(id) - if err != nil { - return fmt.Errorf("failed to find article: %v", id) - } - - if err := deleteCoverArticle(article.CoverImage); err != nil { - return fmt.Errorf("error waktu menghapus cover image article %s: %v", id, err) - } - - err = s.ArticleRepo.DeleteArticle(id) - if err != nil { - return fmt.Errorf("failed to delete article: %v", err) - } - - articleCacheKey := fmt.Sprintf("article:%s", id) - err = utils.DeleteData(articleCacheKey) - if err != nil { - fmt.Printf("Error deleting cache for article: %v\n", err) - } - - articlesCacheKey := "articles:all" - err = utils.DeleteData(articlesCacheKey) - if err != nil { - fmt.Printf("Error deleting cache for all articles: %v\n", err) - } - - return nil -} diff --git a/internal/services/auth/auth_admin_service.go b/internal/services/auth/auth_admin_service.go deleted file mode 100644 index 7adb5af..0000000 --- a/internal/services/auth/auth_admin_service.go +++ /dev/null @@ -1,192 +0,0 @@ -package service -/* -import ( - "errors" - "fmt" - "log" - "rijig/config" - dto "rijig/dto/auth" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - "rijig/model" - "rijig/utils" - "time" - - "github.com/golang-jwt/jwt/v5" - "golang.org/x/crypto/bcrypt" -) - -const ( - ErrEmailTaken = "email is already used" - ErrPhoneTaken = "phone number is already used" - ErrInvalidPassword = "password does not match" - ErrRoleNotFound = "role not found" - ErrFailedToGenerateToken = "failed to generate token" - ErrFailedToHashPassword = "failed to hash password" - ErrFailedToCreateUser = "failed to create user" - ErrIncorrectPassword = "incorrect password" - ErrAccountNotFound = "account not found" -) - -type AuthAdminService interface { - RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) - - LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) - LogoutAdmin(userID, deviceID string) error -} - -type authAdminService struct { - UserRepo repository.AuthAdminRepository - RoleRepo repositories.RoleRepository - SecretKey string -} - -func NewAuthAdminService(userRepo repository.AuthAdminRepository, roleRepo repositories.RoleRepository, secretKey string) AuthAdminService { - return &authAdminService{UserRepo: userRepo, RoleRepo: roleRepo, SecretKey: secretKey} -} - -func (s *authAdminService) RegisterAdmin(request *dto.RegisterAdminRequest) (*model.User, error) { - - if existingUser, _ := s.UserRepo.FindByEmail(request.Email); existingUser != nil { - return nil, errors.New(ErrEmailTaken) - } - - if existingUser, _ := s.UserRepo.FindByPhone(request.Phone); existingUser != nil { - return nil, errors.New(ErrPhoneTaken) - } - - role, err := s.UserRepo.FindRoleByName("administrator") - if err != nil { - return nil, errors.New(ErrRoleNotFound) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost) - if err != nil { - log.Println("Error hashing password:", err) - return nil, errors.New(ErrFailedToHashPassword) - } - - user := &model.User{ - Name: request.Name, - Email: request.Email, - Phone: request.Phone, - Password: string(hashedPassword), - RoleID: role.ID, - Role: role, - Dateofbirth: request.Dateofbirth, - Placeofbirth: request.Placeofbirth, - Gender: request.Gender, - RegistrationStatus: "completed", - } - - createdUser, err := s.UserRepo.CreateUser(user) - if err != nil { - log.Println("Error creating user:", err) - return nil, fmt.Errorf("%s: %v", ErrFailedToCreateUser, err) - } - - return createdUser, nil -} - -func (s *authAdminService) LoginAdmin(req *dto.LoginAdminRequest) (*dto.LoginResponse, error) { - - user, err := s.UserRepo.FindByEmail(req.Email) - if err != nil { - log.Println("User not found:", err) - return nil, errors.New(ErrAccountNotFound) - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil { - log.Println("Incorrect password:", err) - return nil, errors.New(ErrIncorrectPassword) - } - - existingUser, err := s.UserRepo.FindAdminByEmailandRoleid(req.Email, "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7") - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } - - var adminUser *model.User - if existingUser != nil { - adminUser = existingUser - } else { - - adminUser = &model.User{ - Email: req.Email, - RoleID: "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7", - } - createdUser, err := s.UserRepo.CreateUser(adminUser) - if err != nil { - return nil, err - } - adminUser = createdUser - } - - token, err := s.generateJWTToken(adminUser.ID, req.Deviceid) - if err != nil { - return nil, err - } - - role, err := s.RoleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } - - deviceID := req.Deviceid - if err := s.saveSessionAdminData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } - - return &dto.LoginResponse{ - UserID: user.ID, - Role: user.Role.RoleName, - Token: token, - }, nil -} - -func (s *authAdminService) saveSessionAdminData(userID string, deviceID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } - - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } - - if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } - - return nil -} - -func (s *authAdminService) generateJWTToken(userID string, deviceID string) (string, error) { - - expirationTime := time.Now().Add(24 * time.Hour) - - claims := jwt.MapClaims{ - "sub": userID, - "exp": expirationTime.Unix(), - "device_id": deviceID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secretKey := config.GetSecretKey() - - return token.SignedString([]byte(secretKey)) -} - -func (s *authAdminService) LogoutAdmin(userID, deviceID string) error { - - err := utils.DeleteSessionData(userID, deviceID) - if err != nil { - return fmt.Errorf("failed to delete session from Redis: %w", err) - } - - return nil -} - */ \ No newline at end of file diff --git a/internal/services/auth/auth_masyarakat_service.go b/internal/services/auth/auth_masyarakat_service.go deleted file mode 100644 index 11070da..0000000 --- a/internal/services/auth/auth_masyarakat_service.go +++ /dev/null @@ -1,171 +0,0 @@ -package service -/* -import ( - "errors" - "fmt" - "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - "rijig/model" - "rijig/utils" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -type AuthMasyarakatService interface { - RegisterOrLogin(req *dto.RegisterRequest) error - VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) - Logout(userID, deviceID string) error -} - -type authMasyarakatService struct { - userRepo repository.AuthPengelolaRepository - roleRepo repositories.RoleRepository -} - -func NewAuthMasyarakatService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthMasyarakatService { - return &authMasyarakatService{userRepo, roleRepo} -} - -func (s *authMasyarakatService) generateJWTToken(userID string, deviceID string) (string, error) { - - expirationTime := time.Now().Add(672 * time.Hour) - - claims := jwt.MapClaims{ - "sub": userID, - "exp": expirationTime.Unix(), - "device_id": deviceID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secretKey := config.GetSecretKey() - - return token.SignedString([]byte(secretKey)) -} - -func (s *authMasyarakatService) RegisterOrLogin(req *dto.RegisterRequest) error { - if err := s.checkOTPRequestCooldown(req.Phone); err != nil { - return err - } - return s.sendOTP(req.Phone) -} - -func (s *authMasyarakatService) checkOTPRequestCooldown(phone string) error { - otpSentTime, err := utils.GetStringData("otp_sent:" + phone) - if err != nil || otpSentTime == "" { - return nil - } - lastSent, _ := time.Parse(time.RFC3339, otpSentTime) - if time.Since(lastSent) < otpCooldown { - return errors.New("please wait before requesting a new OTP") - } - return nil -} - -func (s *authMasyarakatService) sendOTP(phone string) error { - otp := generateOTP() - if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - return err - } - - if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { - return err - } - return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -} - -func (s *authMasyarakatService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil || storedOTP == "" { - return nil, errors.New("OTP expired or not found") - } - - if storedOTP != req.OTP { - return nil, errors.New("invalid OTP") - } - - if err := utils.DeleteData("otp:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) - } - if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) - } - - existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0e5684e4-b214-4bd0-972f-3be80c4649a0") - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } - - var user *model.User - if existingUser != nil { - user = existingUser - } else { - - user = &model.User{ - Phone: req.Phone, - RoleID: "0e5684e4-b214-4bd0-972f-3be80c4649a0", - PhoneVerified: true, - RegistrationStatus: "completed", - } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return nil, err - } - user = createdUser - } - - token, err := s.generateJWTToken(user.ID, req.DeviceID) - if err != nil { - return nil, err - } - - role, err := s.roleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } - - deviceID := req.DeviceID - if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } - - return &dto.UserDataResponse{ - UserID: user.ID, - UserRole: role.RoleName, - Token: token, - }, nil -} - -func (s *authMasyarakatService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } - - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } - - if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } - - return nil -} - -func (s *authMasyarakatService) Logout(userID, deviceID string) error { - - err := utils.DeleteSessionData(userID, deviceID) - if err != nil { - return fmt.Errorf("failed to delete session from Redis: %w", err) - } - - return nil -} - */ \ No newline at end of file diff --git a/internal/services/auth/auth_pengelola_service.go b/internal/services/auth/auth_pengelola_service.go deleted file mode 100644 index 0c2b33e..0000000 --- a/internal/services/auth/auth_pengelola_service.go +++ /dev/null @@ -1,171 +0,0 @@ -package service -/* -import ( - "errors" - "fmt" - "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - "rijig/model" - "rijig/utils" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -type AuthPengelolaService interface { - RegisterOrLogin(req *dto.RegisterRequest) error - VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) - Logout(userID, deviceID string) error -} - -type authPengelolaService struct { - userRepo repository.AuthPengelolaRepository - roleRepo repositories.RoleRepository -} - -func NewAuthPengelolaService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengelolaService { - return &authPengelolaService{userRepo, roleRepo} -} - -func (s *authPengelolaService) generateJWTToken(userID string, deviceID string) (string, error) { - - expirationTime := time.Now().Add(168 * time.Hour) - - claims := jwt.MapClaims{ - "sub": userID, - "exp": expirationTime.Unix(), - "device_id": deviceID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secretKey := config.GetSecretKey() - - return token.SignedString([]byte(secretKey)) -} - -func (s *authPengelolaService) RegisterOrLogin(req *dto.RegisterRequest) error { - if err := s.checkOTPRequestCooldown(req.Phone); err != nil { - return err - } - return s.sendOTP(req.Phone) -} - -func (s *authPengelolaService) checkOTPRequestCooldown(phone string) error { - otpSentTime, err := utils.GetStringData("otp_sent:" + phone) - if err != nil || otpSentTime == "" { - return nil - } - lastSent, _ := time.Parse(time.RFC3339, otpSentTime) - if time.Since(lastSent) < otpCooldown { - return errors.New("please wait before requesting a new OTP") - } - return nil -} - -func (s *authPengelolaService) sendOTP(phone string) error { - otp := generateOTP() - if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - return err - } - - if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { - return err - } - return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -} - -func (s *authPengelolaService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil || storedOTP == "" { - return nil, errors.New("OTP expired or not found") - } - - if storedOTP != req.OTP { - return nil, errors.New("invalid OTP") - } - - if err := utils.DeleteData("otp:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) - } - if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) - } - - existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "0bf86966-7042-410a-a88c-d01f70832348") - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } - - var user *model.User - if existingUser != nil { - user = existingUser - } else { - - user = &model.User{ - Phone: req.Phone, - RoleID: "0bf86966-7042-410a-a88c-d01f70832348", - PhoneVerified: true, - RegistrationStatus: "uncompleted", - } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return nil, err - } - user = createdUser - } - - token, err := s.generateJWTToken(user.ID, req.DeviceID) - if err != nil { - return nil, err - } - - role, err := s.roleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } - - deviceID := req.DeviceID - if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } - - return &dto.UserDataResponse{ - UserID: user.ID, - UserRole: role.RoleName, - Token: token, - }, nil -} - -func (s *authPengelolaService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } - - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } - - if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } - - return nil -} - -func (s *authPengelolaService) Logout(userID, deviceID string) error { - - err := utils.DeleteSessionData(userID, deviceID) - if err != nil { - return fmt.Errorf("failed to delete session from Redis: %w", err) - } - - return nil -} - */ \ No newline at end of file diff --git a/internal/services/auth/auth_pengepul_service.go b/internal/services/auth/auth_pengepul_service.go deleted file mode 100644 index fd91d95..0000000 --- a/internal/services/auth/auth_pengepul_service.go +++ /dev/null @@ -1,172 +0,0 @@ -package service -/* -import ( - "errors" - "fmt" - "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - "rijig/model" - "rijig/utils" - "time" - - "github.com/golang-jwt/jwt/v5" -) - -type AuthPengepulService interface { - RegisterOrLogin(req *dto.RegisterRequest) error - VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) - Logout(userID, deviceID string) error -} - -type authPengepulService struct { - userRepo repository.AuthPengelolaRepository - roleRepo repositories.RoleRepository -} - -func NewAuthPengepulService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthPengepulService { - return &authPengepulService{userRepo, roleRepo} -} - -func (s *authPengepulService) generateJWTToken(userID string, deviceID string) (string, error) { - - expirationTime := time.Now().Add(480 * time.Hour) - - claims := jwt.MapClaims{ - "sub": userID, - "exp": expirationTime.Unix(), - "device_id": deviceID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - secretKey := config.GetSecretKey() - - return token.SignedString([]byte(secretKey)) -} - -func (s *authPengepulService) RegisterOrLogin(req *dto.RegisterRequest) error { - if err := s.checkOTPRequestCooldown(req.Phone); err != nil { - return err - } - return s.sendOTP(req.Phone) -} - -func (s *authPengepulService) checkOTPRequestCooldown(phone string) error { - otpSentTime, err := utils.GetStringData("otp_sent:" + phone) - if err != nil || otpSentTime == "" { - return nil - } - lastSent, _ := time.Parse(time.RFC3339, otpSentTime) - if time.Since(lastSent) < otpCooldown { - return errors.New("please wait before requesting a new OTP") - } - return nil -} - -func (s *authPengepulService) sendOTP(phone string) error { - otp := generateOTP() - fmt.Printf("ur otp is:%s", otp) - // if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { - // return err - // } - - if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { - return err - } - return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -} - -func (s *authPengepulService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - - storedOTP, err := utils.GetStringData("otp:" + req.Phone) - if err != nil || storedOTP == "" { - return nil, errors.New("OTP expired or not found") - } - - if storedOTP != req.OTP { - return nil, errors.New("invalid OTP") - } - - if err := utils.DeleteData("otp:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) - } - if err := utils.DeleteData("otp_sent:" + req.Phone); err != nil { - return nil, fmt.Errorf("failed to remove otp_sent from Redis: %w", err) - } - - existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, "d7245535-0e9e-4d35-ab39-baece5c10b3c") - if err != nil { - return nil, fmt.Errorf("failed to check existing user: %w", err) - } - - var user *model.User - if existingUser != nil { - user = existingUser - } else { - - user = &model.User{ - Phone: req.Phone, - RoleID: "d7245535-0e9e-4d35-ab39-baece5c10b3c", - PhoneVerified: true, - RegistrationStatus: "uncompleted", - } - createdUser, err := s.userRepo.CreateUser(user) - if err != nil { - return nil, err - } - user = createdUser - } - - token, err := s.generateJWTToken(user.ID, req.DeviceID) - if err != nil { - return nil, err - } - - role, err := s.roleRepo.FindByID(user.RoleID) - if err != nil { - return nil, fmt.Errorf("failed to get role: %w", err) - } - - deviceID := req.DeviceID - if err := s.saveSessionData(user.ID, deviceID, user.RoleID, role.RoleName, token); err != nil { - return nil, err - } - - return &dto.UserDataResponse{ - UserID: user.ID, - UserRole: role.RoleName, - Token: token, - }, nil -} - -func (s *authPengepulService) saveSessionData(userID string, deviceID string, roleID string, roleName string, token string) error { - sessionKey := fmt.Sprintf("session:%s:%s", userID, deviceID) - sessionData := map[string]interface{}{ - "userID": userID, - "roleID": roleID, - "roleName": roleName, - } - - if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session data: %w", err) - } - - if err := utils.SetStringData("session_token:"+userID+":"+deviceID, token, 24*time.Hour); err != nil { - return fmt.Errorf("failed to set session token: %w", err) - } - - return nil -} - -func (s *authPengepulService) Logout(userID, deviceID string) error { - - err := utils.DeleteSessionData(userID, deviceID) - if err != nil { - return fmt.Errorf("failed to delete session from Redis: %w", err) - } - - return nil -} - */ \ No newline at end of file diff --git a/internal/services/auth/otp.go b/internal/services/auth/otp.go deleted file mode 100644 index d96c534..0000000 --- a/internal/services/auth/otp.go +++ /dev/null @@ -1,14 +0,0 @@ -package service - -import ( - "fmt" - "math/rand" - "time" -) - -func generateOTP() string { - randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) - return fmt.Sprintf("%04d", randGenerator.Intn(10000)) -} - -const otpCooldown = 50 * time.Second diff --git a/internal/services/auth_service.go b/internal/services/auth_service.go deleted file mode 100644 index 47a733d..0000000 --- a/internal/services/auth_service.go +++ /dev/null @@ -1,213 +0,0 @@ -package services - -// import ( -// "errors" -// "fmt" -// "math/rand" -// "rijig/config" -// "rijig/dto" -// "rijig/internal/repositories" -// "rijig/model" -// "rijig/utils" -// "time" - -// "github.com/golang-jwt/jwt/v5" -// ) - -// const otpCooldown = 30 * time.Second - -// type AuthService interface { -// RegisterOrLogin(req *dto.RegisterRequest) error -// VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) -// Logout(userID, phone string) error -// } - -// type authService struct { -// userRepo repositories.UserRepository -// roleRepo repositories.RoleRepository -// } - -// func NewAuthService(userRepo repositories.UserRepository, roleRepo repositories.RoleRepository) AuthService { -// return &authService{userRepo, roleRepo} -// } - -// func (s *authService) RegisterOrLogin(req *dto.RegisterRequest) error { - -// if err := s.checkOTPRequestCooldown(req.Phone); err != nil { -// return err -// } - -// user, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) -// if err != nil { -// return fmt.Errorf("failed to check existing user: %w", err) -// } - -// if user != nil { -// return s.sendOTP(req.Phone) -// } - -// user = &model.User{ -// Phone: req.Phone, -// RoleID: req.RoleID, -// } - -// createdUser, err := s.userRepo.CreateUser(user) -// if err != nil { -// return fmt.Errorf("failed to create new user: %w", err) -// } - -// if err := s.saveUserToRedis(createdUser.ID, createdUser, req.Phone); err != nil { -// return err -// } - -// return s.sendOTP(req.Phone) -// } - -// func (s *authService) checkOTPRequestCooldown(phone string) error { -// otpSentTime, err := utils.GetStringData("otp_sent:" + phone) -// if err != nil || otpSentTime == "" { -// return nil -// } -// lastSent, _ := time.Parse(time.RFC3339, otpSentTime) -// if time.Since(lastSent) < otpCooldown { -// return errors.New("please wait before requesting a new OTP") -// } -// return nil -// } - -// func (s *authService) sendOTP(phone string) error { -// otp := generateOTP() -// if err := config.SendWhatsAppMessage(phone, fmt.Sprintf("Your OTP is: %s", otp)); err != nil { -// return err -// } - -// if err := utils.SetStringData("otp:"+phone, otp, 10*time.Minute); err != nil { -// return err -// } -// return utils.SetStringData("otp_sent:"+phone, time.Now().Format(time.RFC3339), 10*time.Minute) -// } - -// func (s *authService) VerifyOTP(req *dto.VerifyOTPRequest) (*dto.UserDataResponse, error) { - -// storedOTP, err := utils.GetStringData("otp:" + req.Phone) -// if err != nil || storedOTP == "" { -// return nil, errors.New("OTP expired or not found") -// } - -// if storedOTP != req.OTP { -// return nil, errors.New("invalid OTP") -// } - -// if err := utils.DeleteData("otp:" + req.Phone); err != nil { -// return nil, fmt.Errorf("failed to remove OTP from Redis: %w", err) -// } - -// existingUser, err := s.userRepo.GetUserByPhoneAndRole(req.Phone, req.RoleID) -// if err != nil { -// return nil, fmt.Errorf("failed to check existing user: %w", err) -// } - -// var user *model.User -// if existingUser != nil { -// user = existingUser -// } else { - -// user = &model.User{ -// Phone: req.Phone, -// RoleID: req.RoleID, -// } -// createdUser, err := s.userRepo.CreateUser(user) -// if err != nil { -// return nil, err -// } -// user = createdUser -// } - -// token, err := s.generateJWTToken(user.ID) -// if err != nil { -// return nil, err -// } - -// role, err := s.roleRepo.FindByID(user.RoleID) -// if err != nil { -// return nil, fmt.Errorf("failed to get role: %w", err) -// } - -// if err := s.saveSessionData(user.ID, user.RoleID, role.RoleName, token); err != nil { -// return nil, err -// } - -// return &dto.UserDataResponse{ -// UserID: user.ID, -// UserRole: role.RoleName, -// Token: token, -// }, nil -// } - -// func (s *authService) saveUserToRedis(userID string, user *model.User, phone string) error { -// if err := utils.SetJSONData("user:"+userID, user, 10*time.Minute); err != nil { -// return fmt.Errorf("failed to store user data in Redis: %w", err) -// } - -// if err := utils.SetStringData("user_phone:"+userID, phone, 10*time.Minute); err != nil { -// return fmt.Errorf("failed to store user phone in Redis: %w", err) -// } - -// return nil -// } - -// func (s *authService) generateJWTToken(userID string) (string, error) { -// expirationTime := time.Now().Add(24 * time.Hour) -// claims := &jwt.RegisteredClaims{ -// Subject: userID, -// ExpiresAt: jwt.NewNumericDate(expirationTime), -// } - -// token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) -// secretKey := config.GetSecretKey() - -// return token.SignedString([]byte(secretKey)) -// } - -// func (s *authService) saveSessionData(userID string, roleID string, roleName string, token string) error { -// sessionKey := fmt.Sprintf("session:%s", userID) -// sessionData := map[string]interface{}{ -// "userID": userID, -// "roleID": roleID, -// "roleName": roleName, -// } - -// if err := utils.SetJSONData(sessionKey, sessionData, 24*time.Hour); err != nil { -// return fmt.Errorf("failed to set session data: %w", err) -// } - -// if err := utils.SetStringData("session_token:"+userID, token, 24*time.Hour); err != nil { -// return fmt.Errorf("failed to set session token: %w", err) -// } - -// return nil -// } - -// func (s *authService) Logout(userID, phone string) error { -// keys := []string{ -// "session:" + userID, -// "session_token:" + userID, -// "user_logged_in:" + userID, -// "user:" + userID, -// "user_phone:" + userID, -// "otp_sent:" + phone, -// } - -// for _, key := range keys { -// if err := utils.DeleteData(key); err != nil { -// return fmt.Errorf("failed to delete key %s from Redis: %w", key, err) -// } -// } - -// return nil -// } - -// func generateOTP() string { -// randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) -// return fmt.Sprintf("%04d", randGenerator.Intn(10000)) -// } diff --git a/internal/services/banner_service.go b/internal/services/banner_service.go deleted file mode 100644 index 1b1b734..0000000 --- a/internal/services/banner_service.go +++ /dev/null @@ -1,366 +0,0 @@ -package services - -import ( - "fmt" - "mime/multipart" - "os" - "path/filepath" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type BannerService interface { - CreateBanner(request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) - GetAllBanners() ([]dto.ResponseBannerDTO, error) - GetBannerByID(id string) (*dto.ResponseBannerDTO, error) - UpdateBanner(id string, request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) - DeleteBanner(id string) error -} - -type bannerService struct { - BannerRepo repositories.BannerRepository -} - -func NewBannerService(bannerRepo repositories.BannerRepository) BannerService { - return &bannerService{BannerRepo: bannerRepo} -} - -func (s *bannerService) saveBannerImage(bannerImage *multipart.FileHeader) (string, error) { - bannerImageDir := "./public" + os.Getenv("BASE_URL") + "/uploads/banners" - if _, err := os.Stat(bannerImageDir); os.IsNotExist(err) { - if err := os.MkdirAll(bannerImageDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for banner image: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} - extension := filepath.Ext(bannerImage.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - bannerImageFileName := fmt.Sprintf("%s_banner%s", uuid.New().String(), extension) - bannerImagePath := filepath.Join(bannerImageDir, bannerImageFileName) - - src, err := bannerImage.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(bannerImagePath) - if err != nil { - return "", fmt.Errorf("failed to create banner image file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save banner image: %v", err) - } - - return bannerImagePath, nil -} - -func (s *bannerService) CreateBanner(request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) { - - errors, valid := request.ValidateBannerInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - bannerImagePath, err := s.saveBannerImage(bannerImage) - if err != nil { - return nil, fmt.Errorf("failed to save banner image: %v", err) - } - - banner := model.Banner{ - BannerName: request.BannerName, - BannerImage: bannerImagePath, - } - - if err := s.BannerRepo.CreateBanner(&banner); err != nil { - return nil, fmt.Errorf("failed to create banner: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) - - bannerResponseDTO := &dto.ResponseBannerDTO{ - ID: banner.ID, - BannerName: banner.BannerName, - BannerImage: banner.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - articlesCacheKey := "banners:all" - err = utils.DeleteData(articlesCacheKey) - if err != nil { - fmt.Printf("Error deleting cache for all banners: %v\n", err) - } - - cacheKey := fmt.Sprintf("banner:%s", banner.ID) - cacheData := map[string]interface{}{ - "data": bannerResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching banner: %v\n", err) - } - - banners, err := s.BannerRepo.FindAllBanners() - if err == nil { - var bannersDTO []dto.ResponseBannerDTO - for _, b := range banners { - createdAt, _ := utils.FormatDateToIndonesianFormat(b.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(b.UpdatedAt) - - bannersDTO = append(bannersDTO, dto.ResponseBannerDTO{ - ID: b.ID, - BannerName: b.BannerName, - BannerImage: b.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData = map[string]interface{}{ - "data": bannersDTO, - } - if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching updated banners to Redis: %v\n", err) - } - } else { - fmt.Printf("Error fetching all banners: %v\n", err) - } - - return bannerResponseDTO, nil -} - -func (s *bannerService) GetAllBanners() ([]dto.ResponseBannerDTO, error) { - var banners []dto.ResponseBannerDTO - - cacheKey := "banners:all" - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - if bannerData, ok := item.(map[string]interface{}); ok { - banners = append(banners, dto.ResponseBannerDTO{ - ID: bannerData["id"].(string), - BannerName: bannerData["bannername"].(string), - BannerImage: bannerData["bannerimage"].(string), - CreatedAt: bannerData["createdAt"].(string), - UpdatedAt: bannerData["updatedAt"].(string), - }) - } - } - return banners, nil - } - } - - records, err := s.BannerRepo.FindAllBanners() - if err != nil { - return nil, fmt.Errorf("failed to fetch banners: %v", err) - } - - for _, record := range records { - createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) - - banners = append(banners, dto.ResponseBannerDTO{ - ID: record.ID, - BannerName: record.BannerName, - BannerImage: record.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": banners, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching banners: %v\n", err) - } - - return banners, nil -} - -func (s *bannerService) GetBannerByID(id string) (*dto.ResponseBannerDTO, error) { - - cacheKey := fmt.Sprintf("banner:%s", id) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - if data, ok := cachedData["data"].(map[string]interface{}); ok { - return &dto.ResponseBannerDTO{ - ID: data["id"].(string), - BannerName: data["bannername"].(string), - BannerImage: data["bannerimage"].(string), - CreatedAt: data["createdAt"].(string), - UpdatedAt: data["updatedAt"].(string), - }, nil - } - } - - banner, err := s.BannerRepo.FindBannerByID(id) - if err != nil { - - return nil, fmt.Errorf("banner with ID %s not found", id) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) - - bannerResponseDTO := &dto.ResponseBannerDTO{ - ID: banner.ID, - BannerName: banner.BannerName, - BannerImage: banner.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": bannerResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching banner: %v\n", err) - } - - return bannerResponseDTO, nil -} - -func (s *bannerService) UpdateBanner(id string, request dto.RequestBannerDTO, bannerImage *multipart.FileHeader) (*dto.ResponseBannerDTO, error) { - - banner, err := s.BannerRepo.FindBannerByID(id) - if err != nil { - - return nil, fmt.Errorf("banner with ID %s not found", id) - } - - var oldImagePath string - if bannerImage != nil { - - bannerImagePath, err := s.saveBannerImage(bannerImage) - if err != nil { - return nil, fmt.Errorf("failed to save banner image: %v", err) - } - - oldImagePath = banner.BannerImage - banner.BannerImage = bannerImagePath - } - - banner.BannerName = request.BannerName - - if err := s.BannerRepo.UpdateBanner(id, banner); err != nil { - return nil, fmt.Errorf("failed to update banner: %v", err) - } - - if oldImagePath != "" { - err := os.Remove(oldImagePath) - if err != nil { - fmt.Printf("Failed to delete old banner image: %v\n", err) - } - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(banner.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(banner.UpdatedAt) - - bannerResponseDTO := &dto.ResponseBannerDTO{ - ID: banner.ID, - BannerName: banner.BannerName, - BannerImage: banner.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("banner:%s", id) - err = utils.DeleteData(cacheKey) - if err != nil { - fmt.Printf("Error deleting cache for banner: %v\n", err) - } - - cacheData := map[string]interface{}{ - "data": bannerResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching updated banner: %v\n", err) - } - - articlesCacheKey := "banners:all" - err = utils.DeleteData(articlesCacheKey) - if err != nil { - fmt.Printf("Error deleting cache for all banners: %v\n", err) - } - - banners, err := s.BannerRepo.FindAllBanners() - if err == nil { - var bannersDTO []dto.ResponseBannerDTO - for _, b := range banners { - createdAt, _ := utils.FormatDateToIndonesianFormat(b.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(b.UpdatedAt) - - bannersDTO = append(bannersDTO, dto.ResponseBannerDTO{ - ID: b.ID, - BannerName: b.BannerName, - BannerImage: b.BannerImage, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData = map[string]interface{}{ - "data": bannersDTO, - } - if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching updated banners to Redis: %v\n", err) - } - } else { - fmt.Printf("Error fetching all banners: %v\n", err) - } - - return bannerResponseDTO, nil -} - -func (s *bannerService) DeleteBanner(id string) error { - - banner, err := s.BannerRepo.FindBannerByID(id) - if err != nil { - - return fmt.Errorf("banner with ID %s not found", id) - } - - if banner.BannerImage != "" { - err := os.Remove(banner.BannerImage) - if err != nil { - - fmt.Printf("Failed to delete banner image: %v\n", err) - } else { - fmt.Printf("Successfully deleted banner image: %s\n", banner.BannerImage) - } - } - - if err := s.BannerRepo.DeleteBanner(id); err != nil { - return fmt.Errorf("failed to delete banner from database: %v", err) - } - - cacheKey := fmt.Sprintf("banner:%s", banner.ID) - err = utils.DeleteData(cacheKey) - if err != nil { - fmt.Printf("Error deleting cache for banner: %v\n", err) - } - - articlesCacheKey := "banners:all" - err = utils.DeleteData(articlesCacheKey) - if err != nil { - fmt.Printf("Error deleting cache for all banners: %v\n", err) - } - - return nil -} diff --git a/internal/services/cart_redis.go b/internal/services/cart_redis.go deleted file mode 100644 index 80b0aae..0000000 --- a/internal/services/cart_redis.go +++ /dev/null @@ -1,73 +0,0 @@ -package services - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "rijig/config" - "rijig/dto" - - "github.com/go-redis/redis/v8" -) - -const CartTTL = 30 * time.Minute -const CartKeyPrefix = "cart:" - -func buildCartKey(userID string) string { - return fmt.Sprintf("%s%s", CartKeyPrefix, userID) -} - -func SetCartToRedis(ctx context.Context, userID string, cart dto.RequestCartDTO) error { - data, err := json.Marshal(cart) - if err != nil { - return err - } - - return config.RedisClient.Set(ctx, buildCartKey(userID), data, CartTTL).Err() -} - -func RefreshCartTTL(ctx context.Context, userID string) error { - return config.RedisClient.Expire(ctx, buildCartKey(userID), CartTTL).Err() -} - -func GetCartFromRedis(ctx context.Context, userID string) (*dto.RequestCartDTO, error) { - val, err := config.RedisClient.Get(ctx, buildCartKey(userID)).Result() - if err == redis.Nil { - return nil, nil - } else if err != nil { - return nil, err - } - - var cart dto.RequestCartDTO - if err := json.Unmarshal([]byte(val), &cart); err != nil { - return nil, err - } - return &cart, nil -} - -func DeleteCartFromRedis(ctx context.Context, userID string) error { - return config.RedisClient.Del(ctx, buildCartKey(userID)).Err() -} - -func GetExpiringCartKeys(ctx context.Context, threshold time.Duration) ([]string, error) { - keys, err := config.RedisClient.Keys(ctx, CartKeyPrefix+"*").Result() - if err != nil { - return nil, err - } - - var expiringKeys []string - for _, key := range keys { - ttl, err := config.RedisClient.TTL(ctx, key).Result() - if err != nil { - continue - } - - if ttl > 0 && ttl <= threshold { - expiringKeys = append(expiringKeys, key) - } - } - - return expiringKeys, nil -} diff --git a/internal/services/cart_service.go b/internal/services/cart_service.go deleted file mode 100644 index 3a905dc..0000000 --- a/internal/services/cart_service.go +++ /dev/null @@ -1,266 +0,0 @@ -package services - -import ( - "context" - "errors" - "log" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" -) - -type CartService interface { - AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error - GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error) - DeleteItem(ctx context.Context, userID string, trashID string) error - ClearCart(ctx context.Context, userID string) error - Checkout(ctx context.Context, userID string) error -} - -type cartService struct { - repo repositories.CartRepository - trashRepo repositories.TrashRepository -} - -func NewCartService(repo repositories.CartRepository, trashRepo repositories.TrashRepository) CartService { - return &cartService{repo: repo, trashRepo: trashRepo} -} - -func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req dto.RequestCartItemDTO) error { - if req.Amount <= 0 { - return errors.New("amount harus lebih dari 0") - } - - _, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID) - if err != nil { - return err - } - - existingCart, err := GetCartFromRedis(ctx, userID) - if err != nil { - return err - } - - if existingCart == nil { - existingCart = &dto.RequestCartDTO{ - CartItems: []dto.RequestCartItemDTO{}, - } - } - - updated := false - for i, item := range existingCart.CartItems { - if item.TrashID == req.TrashID { - existingCart.CartItems[i].Amount = req.Amount - updated = true - break - } - } - - if !updated { - existingCart.CartItems = append(existingCart.CartItems, dto.RequestCartItemDTO{ - TrashID: req.TrashID, - Amount: req.Amount, - }) - } - - return SetCartToRedis(ctx, userID, *existingCart) -} - -func (s *cartService) GetCart(ctx context.Context, userID string) (*dto.ResponseCartDTO, error) { - - cached, err := GetCartFromRedis(ctx, userID) - if err != nil { - return nil, err - } - - if cached != nil { - - if err := RefreshCartTTL(ctx, userID); err != nil { - log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err) - } - - return s.buildResponseFromCache(ctx, userID, cached) - } - - cart, err := s.repo.GetCartByUser(ctx, userID) - if err != nil { - - return &dto.ResponseCartDTO{ - ID: "", - UserID: userID, - TotalAmount: 0, - EstimatedTotalPrice: 0, - CartItems: []dto.ResponseCartItemDTO{}, - }, nil - - } - - response := s.buildResponseFromDB(cart) - - cacheData := dto.RequestCartDTO{CartItems: []dto.RequestCartItemDTO{}} - for _, item := range cart.CartItems { - cacheData.CartItems = append(cacheData.CartItems, dto.RequestCartItemDTO{ - TrashID: item.TrashCategoryID, - Amount: item.Amount, - }) - } - - if err := SetCartToRedis(ctx, userID, cacheData); err != nil { - log.Printf("Warning: Failed to cache cart for user %s: %v", userID, err) - } - - return response, nil -} - -func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error { - - existingCart, err := GetCartFromRedis(ctx, userID) - if err != nil { - return err - } - if existingCart == nil { - return errors.New("keranjang tidak ditemukan") - } - - filtered := []dto.RequestCartItemDTO{} - for _, item := range existingCart.CartItems { - if item.TrashID != trashID { - filtered = append(filtered, item) - } - } - existingCart.CartItems = filtered - - return SetCartToRedis(ctx, userID, *existingCart) -} - -func (s *cartService) ClearCart(ctx context.Context, userID string) error { - - if err := DeleteCartFromRedis(ctx, userID); err != nil { - return err - } - - return s.repo.DeleteCart(ctx, userID) -} - -func (s *cartService) Checkout(ctx context.Context, userID string) error { - - cachedCart, err := GetCartFromRedis(ctx, userID) - if err != nil { - return err - } - - if cachedCart != nil { - if err := s.commitCartFromRedis(ctx, userID, cachedCart); err != nil { - return err - } - } - - _, err = s.repo.GetCartByUser(ctx, userID) - if err != nil { - return err - } - - DeleteCartFromRedis(ctx, userID) - return s.repo.DeleteCart(ctx, userID) -} - -func (s *cartService) buildResponseFromCache(ctx context.Context, userID string, cached *dto.RequestCartDTO) (*dto.ResponseCartDTO, error) { - totalQty := 0.0 - totalPrice := 0.0 - items := []dto.ResponseCartItemDTO{} - - for _, item := range cached.CartItems { - trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) - if err != nil { - log.Printf("Warning: Trash category %s not found for cached cart item", item.TrashID) - continue - } - - subtotal := item.Amount * trash.EstimatedPrice - totalQty += item.Amount - totalPrice += subtotal - - items = append(items, dto.ResponseCartItemDTO{ - ID: "", - TrashID: item.TrashID, - TrashName: trash.Name, - TrashIcon: trash.Icon, - TrashPrice: trash.EstimatedPrice, - Amount: item.Amount, - SubTotalEstimatedPrice: subtotal, - }) - } - - return &dto.ResponseCartDTO{ - ID: "-", - UserID: userID, - TotalAmount: totalQty, - EstimatedTotalPrice: totalPrice, - CartItems: items, - }, nil -} - -func (s *cartService) buildResponseFromDB(cart *model.Cart) *dto.ResponseCartDTO { - var items []dto.ResponseCartItemDTO - for _, item := range cart.CartItems { - items = append(items, dto.ResponseCartItemDTO{ - ID: item.ID, - TrashID: item.TrashCategoryID, - TrashName: item.TrashCategory.Name, - TrashIcon: item.TrashCategory.Icon, - TrashPrice: item.TrashCategory.EstimatedPrice, - Amount: item.Amount, - SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, - }) - } - - return &dto.ResponseCartDTO{ - ID: cart.ID, - UserID: cart.UserID, - TotalAmount: cart.TotalAmount, - EstimatedTotalPrice: cart.EstimatedTotalPrice, - CartItems: items, - } -} - -func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *dto.RequestCartDTO) error { - if len(cachedCart.CartItems) == 0 { - return nil - } - - totalAmount := 0.0 - totalPrice := 0.0 - var cartItems []model.CartItem - - for _, item := range cachedCart.CartItems { - trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) - if err != nil { - log.Printf("Warning: Skipping invalid trash category %s during commit", item.TrashID) - continue - } - - subtotal := item.Amount * trash.EstimatedPrice - totalAmount += item.Amount - totalPrice += subtotal - - cartItems = append(cartItems, model.CartItem{ - TrashCategoryID: item.TrashID, - Amount: item.Amount, - SubTotalEstimatedPrice: subtotal, - }) - } - - if len(cartItems) == 0 { - return nil - } - - newCart := &model.Cart{ - UserID: userID, - TotalAmount: totalAmount, - EstimatedTotalPrice: totalPrice, - CartItems: cartItems, - } - - return s.repo.CreateCartWithItems(ctx, newCart) -} diff --git a/internal/services/collector_service.go b/internal/services/collector_service.go deleted file mode 100644 index 0984d31..0000000 --- a/internal/services/collector_service.go +++ /dev/null @@ -1,224 +0,0 @@ -package services - -import ( - "context" - "errors" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" -) - -type CollectorService interface { - CreateCollector(ctx context.Context, userID string, req dto.RequestCollectorDTO) error - AddTrashToCollector(ctx context.Context, collectorID string, req dto.RequestAddAvaibleTrash) error - GetCollectorByID(ctx context.Context, collectorID string) (*dto.ResponseCollectorDTO, error) - GetCollectorByUserID(ctx context.Context, userID string) (*dto.ResponseCollectorDTO, error) - UpdateCollector(ctx context.Context, collectorID string, jobStatus *string, rating float32, addressID string) error - UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []dto.RequestAvaibleTrashbyCollector) error - DeleteAvaibleTrash(ctx context.Context, trashID string) error -} - -type collectorService struct { - repo repositories.CollectorRepository - trashRepo repositories.TrashRepository -} - -func NewCollectorService(repo repositories.CollectorRepository, trashRepo repositories.TrashRepository, - -) CollectorService { - - return &collectorService{repo: repo, trashRepo: trashRepo} -} - -func (s *collectorService) CreateCollector(ctx context.Context, userID string, req dto.RequestCollectorDTO) error { - collector := &model.Collector{ - UserID: userID, - AddressID: req.AddressId, - JobStatus: "inactive", - Rating: 5, - } - - if err := s.repo.CreateCollector(ctx, collector); err != nil { - return err - } - - var trashItems []model.AvaibleTrashByCollector - for _, item := range req.AvaibleTrashbyCollector { - trashItems = append(trashItems, model.AvaibleTrashByCollector{ - CollectorID: collector.ID, - TrashCategoryID: item.TrashId, - Price: item.TrashPrice, - }) - } - - if err := s.repo.AddAvaibleTrash(ctx, trashItems); err != nil { - return err - } - - for _, t := range trashItems { - _ = s.trashRepo.UpdateEstimatedPrice(ctx, t.TrashCategoryID) - } - - return nil -} - -func (s *collectorService) AddTrashToCollector(ctx context.Context, collectorID string, req dto.RequestAddAvaibleTrash) error { - var trashItems []model.AvaibleTrashByCollector - for _, item := range req.AvaibleTrash { - trashItems = append(trashItems, model.AvaibleTrashByCollector{ - CollectorID: collectorID, - TrashCategoryID: item.TrashId, - Price: item.TrashPrice, - }) - } - if err := s.repo.AddAvaibleTrash(ctx, trashItems); err != nil { - return err - } - - for _, t := range trashItems { - _ = s.trashRepo.UpdateEstimatedPrice(ctx, t.TrashCategoryID) - } - - return nil -} - -func (s *collectorService) GetCollectorByID(ctx context.Context, collectorID string) (*dto.ResponseCollectorDTO, error) { - collector, err := s.repo.GetCollectorByID(ctx, collectorID) - if err != nil { - return nil, err - } - - response := &dto.ResponseCollectorDTO{ - ID: collector.ID, - UserId: collector.UserID, - AddressId: collector.AddressID, - JobStatus: &collector.JobStatus, - Rating: collector.Rating, - User: &dto.UserResponseDTO{ - ID: collector.User.ID, - Name: collector.User.Name, - Phone: collector.User.Phone, - }, - Address: &dto.AddressResponseDTO{ - Province: collector.Address.Province, - District: collector.Address.District, - Regency: collector.Address.Regency, - Village: collector.Address.Village, - PostalCode: collector.Address.PostalCode, - Latitude: collector.Address.Latitude, - Longitude: collector.Address.Longitude, - }, - } - - for _, item := range collector.AvaibleTrashByCollector { - response.AvaibleTrashbyCollector = append(response.AvaibleTrashbyCollector, dto.ResponseAvaibleTrashByCollector{ - ID: item.ID, - TrashId: item.TrashCategory.ID, - TrashName: item.TrashCategory.Name, - TrashIcon: item.TrashCategory.Icon, - TrashPrice: item.Price, - }) - } - - return response, nil -} - -func (s *collectorService) GetCollectorByUserID(ctx context.Context, userID string) (*dto.ResponseCollectorDTO, error) { - collector, err := s.repo.GetCollectorByUserID(ctx, userID) - if err != nil { - return nil, err - } - - response := &dto.ResponseCollectorDTO{ - ID: collector.ID, - UserId: collector.UserID, - AddressId: collector.AddressID, - JobStatus: &collector.JobStatus, - Rating: collector.Rating, - User: &dto.UserResponseDTO{ - ID: collector.User.ID, - Name: collector.User.Name, - Phone: collector.User.Phone, - }, - Address: &dto.AddressResponseDTO{ - Province: collector.Address.Province, - District: collector.Address.District, - Regency: collector.Address.Regency, - Village: collector.Address.Village, - PostalCode: collector.Address.PostalCode, - Latitude: collector.Address.Latitude, - Longitude: collector.Address.Longitude, - }, - } - - for _, item := range collector.AvaibleTrashByCollector { - response.AvaibleTrashbyCollector = append(response.AvaibleTrashbyCollector, dto.ResponseAvaibleTrashByCollector{ - ID: item.ID, - TrashId: item.TrashCategory.ID, - TrashName: item.TrashCategory.Name, - TrashIcon: item.TrashCategory.Icon, - TrashPrice: item.Price, - }) - } - - return response, nil -} - -func (s *collectorService) UpdateCollector(ctx context.Context, collectorID string, jobStatus *string, rating float32, addressID string) error { - updates := make(map[string]interface{}) - - if jobStatus != nil { - updates["job_status"] = *jobStatus - } - if rating > 0 { - updates["rating"] = rating - } - if addressID != "" { - updates["address_id"] = addressID - } - - if len(updates) == 0 { - return errors.New("tidak ada data yang diubah") - } - - return s.repo.UpdateCollector(ctx, &model.Collector{ID: collectorID}, updates) -} - -func (s *collectorService) UpdateAvaibleTrashByCollector(ctx context.Context, collectorID string, updatedTrash []dto.RequestAvaibleTrashbyCollector) error { - var updated []model.AvaibleTrashByCollector - for _, item := range updatedTrash { - updated = append(updated, model.AvaibleTrashByCollector{ - CollectorID: collectorID, - TrashCategoryID: item.TrashId, - Price: item.TrashPrice, - }) - } - - if err := s.repo.UpdateAvaibleTrashByCollector(ctx, collectorID, updated); err != nil { - return err - } - - for _, item := range updated { - _ = s.trashRepo.UpdateEstimatedPrice(ctx, item.TrashCategoryID) - } - - return nil -} - -func (s *collectorService) DeleteAvaibleTrash(ctx context.Context, trashID string) error { - if trashID == "" { - return errors.New("trash_id tidak boleh kosong") - } - - item, err := s.repo.GetTrashItemByID(ctx, trashID) - if err != nil { - return err - } - - if err := s.repo.DeleteAvaibleTrash(ctx, trashID); err != nil { - return err - } - - return s.trashRepo.UpdateEstimatedPrice(ctx, item.TrashCategoryID) -} diff --git a/internal/services/company_profile_service.go b/internal/services/company_profile_service.go deleted file mode 100644 index 6c2714b..0000000 --- a/internal/services/company_profile_service.go +++ /dev/null @@ -1,163 +0,0 @@ -package services - -import ( - "fmt" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type CompanyProfileService interface { - CreateCompanyProfile(userID string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) - GetCompanyProfileByID(id string) (*dto.ResponseCompanyProfileDTO, error) - GetCompanyProfilesByUserID(userID string) ([]dto.ResponseCompanyProfileDTO, error) - UpdateCompanyProfile(id string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) - DeleteCompanyProfile(id string) error -} - -type companyProfileService struct { - companyProfileRepo repositories.CompanyProfileRepository -} - -func NewCompanyProfileService(companyProfileRepo repositories.CompanyProfileRepository) CompanyProfileService { - return &companyProfileService{ - companyProfileRepo: companyProfileRepo, - } -} - -func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*dto.ResponseCompanyProfileDTO, error) { - - createdAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.UpdatedAt) - - responseDTO := &dto.ResponseCompanyProfileDTO{ - ID: companyProfile.ID, - UserID: companyProfile.UserID, - CompanyName: companyProfile.CompanyName, - CompanyAddress: companyProfile.CompanyAddress, - CompanyPhone: companyProfile.CompanyPhone, - CompanyEmail: companyProfile.CompanyEmail, - CompanyLogo: companyProfile.CompanyLogo, - CompanyWebsite: companyProfile.CompanyWebsite, - TaxID: companyProfile.TaxID, - FoundedDate: companyProfile.FoundedDate, - CompanyType: companyProfile.CompanyType, - CompanyDescription: companyProfile.CompanyDescription, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return responseDTO, nil -} - -func (s *companyProfileService) CreateCompanyProfile(userID string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) { - - errors, valid := request.ValidateCompanyProfileInput() - if !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } - - companyProfile := &model.CompanyProfile{ - UserID: userID, - CompanyName: request.CompanyName, - CompanyAddress: request.CompanyAddress, - CompanyPhone: request.CompanyPhone, - CompanyEmail: request.CompanyEmail, - CompanyLogo: request.CompanyLogo, - CompanyWebsite: request.CompanyWebsite, - TaxID: request.TaxID, - FoundedDate: request.FoundedDate, - CompanyType: request.CompanyType, - CompanyDescription: request.CompanyDescription, - } - - createdCompanyProfile, err := s.companyProfileRepo.CreateCompanyProfile(companyProfile) - if err != nil { - return nil, fmt.Errorf("failed to create company profile: %v", err) - } - - responseDTO, err := FormatResponseCompanyProfile(createdCompanyProfile) - if err != nil { - return nil, fmt.Errorf("failed to format company profile response: %v", err) - } - - return responseDTO, nil -} - -func (s *companyProfileService) GetCompanyProfileByID(id string) (*dto.ResponseCompanyProfileDTO, error) { - - companyProfile, err := s.companyProfileRepo.GetCompanyProfileByID(id) - if err != nil { - return nil, fmt.Errorf("error retrieving company profile by ID: %v", err) - } - - responseDTO, err := FormatResponseCompanyProfile(companyProfile) - if err != nil { - return nil, fmt.Errorf("error formatting company profile response: %v", err) - } - - return responseDTO, nil -} - -func (s *companyProfileService) GetCompanyProfilesByUserID(userID string) ([]dto.ResponseCompanyProfileDTO, error) { - - companyProfiles, err := s.companyProfileRepo.GetCompanyProfilesByUserID(userID) - if err != nil { - return nil, fmt.Errorf("error retrieving company profiles by userID: %v", err) - } - - var responseDTOs []dto.ResponseCompanyProfileDTO - for _, companyProfile := range companyProfiles { - responseDTO, err := FormatResponseCompanyProfile(&companyProfile) - if err != nil { - return nil, fmt.Errorf("error formatting company profile response: %v", err) - } - responseDTOs = append(responseDTOs, *responseDTO) - } - - return responseDTOs, nil -} - -func (s *companyProfileService) UpdateCompanyProfile(id string, request *dto.RequestCompanyProfileDTO) (*dto.ResponseCompanyProfileDTO, error) { - - errors, valid := request.ValidateCompanyProfileInput() - if !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } - - companyProfile := &model.CompanyProfile{ - CompanyName: request.CompanyName, - CompanyAddress: request.CompanyAddress, - CompanyPhone: request.CompanyPhone, - CompanyEmail: request.CompanyEmail, - CompanyLogo: request.CompanyLogo, - CompanyWebsite: request.CompanyWebsite, - TaxID: request.TaxID, - FoundedDate: request.FoundedDate, - CompanyType: request.CompanyType, - CompanyDescription: request.CompanyDescription, - } - - updatedCompanyProfile, err := s.companyProfileRepo.UpdateCompanyProfile(id, companyProfile) - if err != nil { - return nil, fmt.Errorf("failed to update company profile: %v", err) - } - - responseDTO, err := FormatResponseCompanyProfile(updatedCompanyProfile) - if err != nil { - return nil, fmt.Errorf("failed to format company profile response: %v", err) - } - - return responseDTO, nil -} - -func (s *companyProfileService) DeleteCompanyProfile(id string) error { - - err := s.companyProfileRepo.DeleteCompanyProfile(id) - if err != nil { - return fmt.Errorf("failed to delete company profile: %v", err) - } - - return nil -} diff --git a/internal/services/coveragearea_service.go b/internal/services/coveragearea_service.go deleted file mode 100644 index 1a0f522..0000000 --- a/internal/services/coveragearea_service.go +++ /dev/null @@ -1,155 +0,0 @@ -package services - -import ( - "fmt" - "log" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type CoverageAreaService interface { - CreateCoverageArea(request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) - GetCoverageAreaByID(id string) (*dto.ResponseCoverageArea, error) - GetAllCoverageAreas() ([]dto.ResponseCoverageArea, error) - UpdateCoverageArea(id string, request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) - DeleteCoverageArea(id string) error -} - -type coverageAreaService struct { - repo repositories.CoverageAreaRepository - WilayahRepo repositories.WilayahIndonesiaRepository -} - -func NewCoverageAreaService(repo repositories.CoverageAreaRepository, WilayahRepo repositories.WilayahIndonesiaRepository) CoverageAreaService { - return &coverageAreaService{repo: repo, WilayahRepo: WilayahRepo} -} - -func ConvertCoverageAreaToResponse(coverage *model.CoverageArea) *dto.ResponseCoverageArea { - createdAt, _ := utils.FormatDateToIndonesianFormat(coverage.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(coverage.UpdatedAt) - - return &dto.ResponseCoverageArea{ - ID: coverage.ID, - Province: coverage.Province, - Regency: coverage.Regency, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } -} - -func (s *coverageAreaService) CreateCoverageArea(request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) { - errors, valid := request.ValidateCoverageArea() - if !valid { - return nil, fmt.Errorf("validation errors: %v", errors) - } - - province, _, err := s.WilayahRepo.FindProvinceByID(request.Province, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid province_id") - } - - regency, _, err := s.WilayahRepo.FindRegencyByID(request.Regency, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid regency_id") - } - - existingCoverage, err := s.repo.FindCoverageByProvinceAndRegency(province.Name, regency.Name) - if err == nil && existingCoverage != nil { - return nil, fmt.Errorf("coverage area with province %s and regency %s already exists", province.Name, regency.Name) - } - - coverage := model.CoverageArea{ - Province: province.Name, - Regency: regency.Name, - } - - if err := s.repo.CreateCoverage(&coverage); err != nil { - return nil, fmt.Errorf("failed to create coverage area: %v", err) - } - - response := ConvertCoverageAreaToResponse(&coverage) - - return response, nil -} - -func (s *coverageAreaService) GetCoverageAreaByID(id string) (*dto.ResponseCoverageArea, error) { - coverage, err := s.repo.FindCoverageById(id) - if err != nil { - return nil, err - } - - response := ConvertCoverageAreaToResponse(coverage) - - return response, nil -} - -func (s *coverageAreaService) GetAllCoverageAreas() ([]dto.ResponseCoverageArea, error) { - coverageAreas, err := s.repo.FindAllCoverage() - if err != nil { - return nil, err - } - - var response []dto.ResponseCoverageArea - for _, coverage := range coverageAreas { - - response = append(response, *ConvertCoverageAreaToResponse(&coverage)) - } - - return response, nil -} - -func (s *coverageAreaService) UpdateCoverageArea(id string, request dto.RequestCoverageArea) (*dto.ResponseCoverageArea, error) { - - errors, valid := request.ValidateCoverageArea() - if !valid { - return nil, fmt.Errorf("validation errors: %v", errors) - } - - coverage, err := s.repo.FindCoverageById(id) - if err != nil { - return nil, fmt.Errorf("coverage area with ID %s not found: %v", id, err) - } - - province, _, err := s.WilayahRepo.FindProvinceByID(request.Province, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid province_id") - } - - regency, _, err := s.WilayahRepo.FindRegencyByID(request.Regency, 0, 0) - if err != nil { - return nil, fmt.Errorf("invalid regency_id") - } - - existingCoverage, err := s.repo.FindCoverageByProvinceAndRegency(province.Name, regency.Name) - if err == nil && existingCoverage != nil { - return nil, fmt.Errorf("coverage area with province %s and regency %s already exists", province.Name, regency.Name) - } - - coverage.Province = province.Name - coverage.Regency = regency.Name - - if err := s.repo.UpdateCoverage(id, coverage); err != nil { - return nil, fmt.Errorf("failed to update coverage area: %v", err) - } - - response := ConvertCoverageAreaToResponse(coverage) - - return response, nil -} - -func (s *coverageAreaService) DeleteCoverageArea(id string) error { - - coverage, err := s.repo.FindCoverageById(id) - if err != nil { - return fmt.Errorf("coverage area with ID %s not found: %v", id, err) - } - - if err := s.repo.DeleteCoverage(id); err != nil { - return fmt.Errorf("failed to delete coverage area: %v", err) - } - - log.Printf("Coverage area with ID %s successfully deleted", coverage.ID) - return nil -} diff --git a/internal/services/identitycard_service.go b/internal/services/identitycard_service.go deleted file mode 100644 index b2435cd..0000000 --- a/internal/services/identitycard_service.go +++ /dev/null @@ -1,289 +0,0 @@ -package services - -import ( - "fmt" - "log" - "mime/multipart" - "os" - "path/filepath" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type IdentityCardService interface { - CreateIdentityCard(userID string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) - GetIdentityCardByID(id string) (*dto.ResponseIdentityCardDTO, error) - GetIdentityCardsByUserID(userID string) ([]dto.ResponseIdentityCardDTO, error) - UpdateIdentityCard(userID string, id string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) - DeleteIdentityCard(id string) error -} - -type identityCardService struct { - identityCardRepo repositories.IdentityCardRepository - userRepo repositories.UserProfilRepository -} - -func NewIdentityCardService(identityCardRepo repositories.IdentityCardRepository, userRepo repositories.UserProfilRepository) IdentityCardService { - return &identityCardService{ - identityCardRepo: identityCardRepo, - userRepo: userRepo, - } -} - -func FormatResponseIdentityCars(identityCard *model.IdentityCard) (*dto.ResponseIdentityCardDTO, error) { - - createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt) - - idcardResponseDTO := &dto.ResponseIdentityCardDTO{ - ID: identityCard.ID, - UserID: identityCard.UserID, - Identificationumber: identityCard.Identificationumber, - Placeofbirth: identityCard.Placeofbirth, - Dateofbirth: identityCard.Dateofbirth, - Gender: identityCard.Gender, - BloodType: identityCard.BloodType, - District: identityCard.District, - Village: identityCard.Village, - Neighbourhood: identityCard.Neighbourhood, - Religion: identityCard.Religion, - Maritalstatus: identityCard.Maritalstatus, - Job: identityCard.Job, - Citizenship: identityCard.Citizenship, - Validuntil: identityCard.Validuntil, - Cardphoto: identityCard.Cardphoto, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return idcardResponseDTO, nil -} - -func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *multipart.FileHeader) (string, error) { - pathImage := "/uploads/identitycards/" - cardPhotoDir := "./public" + os.Getenv("BASE_URL") + pathImage - if _, err := os.Stat(cardPhotoDir); os.IsNotExist(err) { - - if err := os.MkdirAll(cardPhotoDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for identity card photo: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} - extension := filepath.Ext(cardPhoto.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - cardPhotoFileName := fmt.Sprintf("%s_cardphoto%s", userID, extension) - cardPhotoPath := filepath.Join(cardPhotoDir, cardPhotoFileName) - - src, err := cardPhoto.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(cardPhotoPath) - if err != nil { - return "", fmt.Errorf("failed to create card photo file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save card photo: %v", err) - } - - cardPhotoURL := fmt.Sprintf("%s%s", pathImage, cardPhotoFileName) - - return cardPhotoURL, nil -} - -func deleteIdentityCardImage(imagePath string) error { - if imagePath == "" { - return nil - } - - baseDir := "./public/" + os.Getenv("BASE_URL") - absolutePath := baseDir + imagePath - - if _, err := os.Stat(absolutePath); os.IsNotExist(err) { - return fmt.Errorf("image file not found: %v", err) - } - - err := os.Remove(absolutePath) - if err != nil { - return fmt.Errorf("failed to delete image: %v", err) - } - - log.Printf("Image deleted successfully: %s", absolutePath) - return nil -} - -func (s *identityCardService) CreateIdentityCard(userID string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) { - - errors, valid := request.ValidateIdentityCardInput() - if !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } - - cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto) - if err != nil { - return nil, fmt.Errorf("failed to save card photo: %v", err) - } - - identityCard := &model.IdentityCard{ - UserID: userID, - Identificationumber: request.Identificationumber, - Placeofbirth: request.Placeofbirth, - Dateofbirth: request.Dateofbirth, - Gender: request.Gender, - BloodType: request.BloodType, - District: request.District, - Village: request.Village, - Neighbourhood: request.Neighbourhood, - Religion: request.Religion, - Maritalstatus: request.Maritalstatus, - Job: request.Job, - Citizenship: request.Citizenship, - Validuntil: request.Validuntil, - Cardphoto: cardPhotoPath, - } - - identityCard, err = s.identityCardRepo.CreateIdentityCard(identityCard) - if err != nil { - log.Printf("Error creating identity card: %v", err) - return nil, fmt.Errorf("failed to create identity card: %v", err) - } - - user, err := s.userRepo.FindByID(userID) - if err != nil { - return nil, fmt.Errorf("failde to fint user: %v", err) - } - - user.RegistrationStatus = "onreview" - - err = s.userRepo.Update(user) - if err != nil { - return nil, fmt.Errorf("failed to update user: %v", err) - } - idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) - - return idcardResponseDTO, nil -} - -func (s *identityCardService) GetIdentityCardByID(id string) (*dto.ResponseIdentityCardDTO, error) { - - identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) - if err != nil { - log.Printf("Error fetching identity card: %v", err) - return nil, fmt.Errorf("failed to fetch identity card") - } - - idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) - - return idcardResponseDTO, nil - -} - -func (s *identityCardService) GetIdentityCardsByUserID(userID string) ([]dto.ResponseIdentityCardDTO, error) { - - identityCards, err := s.identityCardRepo.GetIdentityCardsByUserID(userID) - if err != nil { - log.Printf("Error fetching identity cards by userID: %v", err) - return nil, fmt.Errorf("failed to fetch identity cards by userID") - } - - var response []dto.ResponseIdentityCardDTO - for _, card := range identityCards { - - idcardResponseDTO, err := FormatResponseIdentityCars(&card) - if err != nil { - log.Printf("Error creating response DTO for identity card ID %v: %v", card.ID, err) - - continue - } - response = append(response, *idcardResponseDTO) - } - - return response, nil -} - -func (s *identityCardService) UpdateIdentityCard(userID string, id string, request *dto.RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*dto.ResponseIdentityCardDTO, error) { - - errors, valid := request.ValidateIdentityCardInput() - if !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } - - identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) - if err != nil { - return nil, fmt.Errorf("identity card not found: %v", err) - } - - if identityCard.Cardphoto != "" { - err := deleteIdentityCardImage(identityCard.Cardphoto) - if err != nil { - return nil, fmt.Errorf("failed to delete old image: %v", err) - } - } - - var cardPhotoPath string - if cardPhoto != nil { - cardPhotoPath, err = s.saveIdentityCardImage(userID, cardPhoto) - if err != nil { - return nil, fmt.Errorf("failed to save card photo: %v", err) - } - } - - identityCard.Identificationumber = request.Identificationumber - identityCard.Placeofbirth = request.Placeofbirth - identityCard.Dateofbirth = request.Dateofbirth - identityCard.Gender = request.Gender - identityCard.BloodType = request.BloodType - identityCard.District = request.District - identityCard.Village = request.Village - identityCard.Neighbourhood = request.Neighbourhood - identityCard.Religion = request.Religion - identityCard.Maritalstatus = request.Maritalstatus - identityCard.Job = request.Job - identityCard.Citizenship = request.Citizenship - identityCard.Validuntil = request.Validuntil - if cardPhotoPath != "" { - identityCard.Cardphoto = cardPhotoPath - } - - identityCard, err = s.identityCardRepo.UpdateIdentityCard(id, identityCard) - if err != nil { - log.Printf("Error updating identity card: %v", err) - return nil, fmt.Errorf("failed to update identity card: %v", err) - } - - idcardResponseDTO, _ := FormatResponseIdentityCars(identityCard) - - return idcardResponseDTO, nil -} - -func (s *identityCardService) DeleteIdentityCard(id string) error { - - identityCard, err := s.identityCardRepo.GetIdentityCardByID(id) - if err != nil { - return fmt.Errorf("identity card not found: %v", err) - } - - if identityCard.Cardphoto != "" { - err := deleteIdentityCardImage(identityCard.Cardphoto) - if err != nil { - return fmt.Errorf("failed to delete card photo: %v", err) - } - } - - err = s.identityCardRepo.DeleteIdentityCard(id) - if err != nil { - return fmt.Errorf("failed to delete identity card: %v", err) - } - - return nil -} diff --git a/internal/services/initialcoint_service.go b/internal/services/initialcoint_service.go deleted file mode 100644 index 2791335..0000000 --- a/internal/services/initialcoint_service.go +++ /dev/null @@ -1,299 +0,0 @@ -package services - -import ( - "fmt" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type InitialCointService interface { - CreateInitialCoint(request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) - GetAllInitialCoints() ([]dto.ReponseInitialCointDTO, error) - GetInitialCointByID(id string) (*dto.ReponseInitialCointDTO, error) - UpdateInitialCoint(id string, request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) - DeleteInitialCoint(id string) error -} - -type initialCointService struct { - InitialCointRepo repositories.InitialCointRepository -} - -func NewInitialCointService(repo repositories.InitialCointRepository) InitialCointService { - return &initialCointService{InitialCointRepo: repo} -} - -func (s *initialCointService) CreateInitialCoint(request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) { - - errors, valid := request.ValidateCointInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - coint := model.InitialCoint{ - CoinName: request.CoinName, - ValuePerUnit: request.ValuePerUnit, - } - if err := s.InitialCointRepo.CreateInitialCoint(&coint); err != nil { - return nil, fmt.Errorf("failed to create initial coint: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) - - responseDTO := &dto.ReponseInitialCointDTO{ - ID: coint.ID, - CoinName: coint.CoinName, - ValuePerUnit: coint.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("initialcoint:%s", coint.ID) - cacheData := map[string]interface{}{ - "data": responseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching new initial coint: %v\n", err) - } - - err := s.updateAllCointCache() - if err != nil { - return nil, fmt.Errorf("error updating all initial coint cache: %v", err) - } - - return responseDTO, nil -} - -func (s *initialCointService) GetAllInitialCoints() ([]dto.ReponseInitialCointDTO, error) { - var cointsDTO []dto.ReponseInitialCointDTO - cacheKey := "initialcoints:all" - - cachedData, err := utils.GetJSONData(cacheKey) - if err != nil { - fmt.Printf("Error fetching cache for initialcoints: %v\n", err) - } - - if cachedData != nil { - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - if cointData, ok := item.(map[string]interface{}); ok { - - if coinID, ok := cointData["coin_id"].(string); ok { - if coinName, ok := cointData["coin_name"].(string); ok { - if valuePerUnit, ok := cointData["value_perunit"].(float64); ok { - if createdAt, ok := cointData["createdAt"].(string); ok { - if updatedAt, ok := cointData["updatedAt"].(string); ok { - - cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ - ID: coinID, - CoinName: coinName, - ValuePerUnit: valuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - } - } - } - } - } - } - return cointsDTO, nil - } - } - - records, err := s.InitialCointRepo.FindAllInitialCoints() - if err != nil { - return nil, fmt.Errorf("failed to fetch initial coints from database: %v", err) - } - - if len(records) == 0 { - return cointsDTO, nil - } - - for _, record := range records { - createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) - - cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ - ID: record.ID, - CoinName: record.CoinName, - ValuePerUnit: record.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": cointsDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching all initial coints: %v\n", err) - } - - return cointsDTO, nil -} - -func (s *initialCointService) GetInitialCointByID(id string) (*dto.ReponseInitialCointDTO, error) { - cacheKey := fmt.Sprintf("initialcoint:%s", id) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - - if data, ok := cachedData["data"].(map[string]interface{}); ok { - - return &dto.ReponseInitialCointDTO{ - ID: data["coin_id"].(string), - CoinName: data["coin_name"].(string), - ValuePerUnit: data["value_perunit"].(float64), - CreatedAt: data["createdAt"].(string), - UpdatedAt: data["updatedAt"].(string), - }, nil - } else { - return nil, fmt.Errorf("error: cache data is not in the expected format for coin ID %s", id) - } - } - - coint, err := s.InitialCointRepo.FindInitialCointByID(id) - if err != nil { - return nil, fmt.Errorf("failed to fetch initial coint by ID %s: %v", id, err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) - - cointDTO := &dto.ReponseInitialCointDTO{ - ID: coint.ID, - CoinName: coint.CoinName, - ValuePerUnit: coint.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": cointDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching initial coint by ID: %v\n", err) - } - - return cointDTO, nil -} - -func (s *initialCointService) UpdateInitialCoint(id string, request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) { - - coint, err := s.InitialCointRepo.FindInitialCointByID(id) - if err != nil { - return nil, fmt.Errorf("initial coint with ID %s not found", id) - } - - coint.CoinName = request.CoinName - coint.ValuePerUnit = request.ValuePerUnit - - if err := s.InitialCointRepo.UpdateInitialCoint(id, coint); err != nil { - return nil, fmt.Errorf("failed to update initial coint: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) - - cointDTO := &dto.ReponseInitialCointDTO{ - ID: coint.ID, - CoinName: coint.CoinName, - ValuePerUnit: coint.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("initialcoint:%s", id) - cacheData := map[string]interface{}{ - "data": cointDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - fmt.Printf("Error caching updated initial coint: %v\n", err) - } - - allCoints, err := s.InitialCointRepo.FindAllInitialCoints() - if err != nil { - return nil, fmt.Errorf("failed to fetch all initial coints from database: %v", err) - } - - var cointsDTO []dto.ReponseInitialCointDTO - for _, record := range allCoints { - createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) - - cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ - ID: record.ID, - CoinName: record.CoinName, - ValuePerUnit: record.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheAllKey := "initialcoints:all" - cacheAllData := map[string]interface{}{ - "data": cointsDTO, - } - if err := utils.SetJSONData(cacheAllKey, cacheAllData, time.Hour*24); err != nil { - fmt.Printf("Error caching all initial coints: %v\n", err) - } - - return cointDTO, nil -} - -func (s *initialCointService) DeleteInitialCoint(id string) error { - - coint, err := s.InitialCointRepo.FindInitialCointByID(id) - if err != nil { - return fmt.Errorf("initial coint with ID %s not found", id) - } - - if err := s.InitialCointRepo.DeleteInitialCoint(id); err != nil { - return fmt.Errorf("failed to delete initial coint: %v", err) - } - - cacheKey := fmt.Sprintf("initialcoint:%s", coint.ID) - if err := utils.DeleteData(cacheKey); err != nil { - fmt.Printf("Error deleting cache for initial coint: %v\n", err) - } - - return s.updateAllCointCache() -} - -func (s *initialCointService) updateAllCointCache() error { - - records, err := s.InitialCointRepo.FindAllInitialCoints() - if err != nil { - return fmt.Errorf("failed to fetch all initial coints from database: %v", err) - } - - var cointsDTO []dto.ReponseInitialCointDTO - for _, record := range records { - createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) - - cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ - ID: record.ID, - CoinName: record.CoinName, - ValuePerUnit: record.ValuePerUnit, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheAllKey := "initialcoints:all" - cacheAllData := map[string]interface{}{ - "data": cointsDTO, - } - if err := utils.SetJSONData(cacheAllKey, cacheAllData, time.Hour*24); err != nil { - fmt.Printf("Error caching all initial coints: %v\n", err) - return err - } - - return nil -} diff --git a/internal/services/pickup_history_service.go b/internal/services/pickup_history_service.go deleted file mode 100644 index 5362664..0000000 --- a/internal/services/pickup_history_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package services - -import ( - "context" - "time" - "rijig/model" - "rijig/internal/repositories" -) - -type PickupStatusHistoryService interface { - LogStatusChange(ctx context.Context, requestID, status, changedByID, changedByRole string) error - GetStatusHistory(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) -} - -type pickupStatusHistoryService struct { - repo repositories.PickupStatusHistoryRepository -} - -func NewPickupStatusHistoryService(repo repositories.PickupStatusHistoryRepository) PickupStatusHistoryService { - return &pickupStatusHistoryService{repo: repo} -} - -func (s *pickupStatusHistoryService) LogStatusChange(ctx context.Context, requestID, status, changedByID, changedByRole string) error { - history := model.PickupStatusHistory{ - RequestID: requestID, - Status: status, - ChangedAt: time.Now(), - ChangedByID: changedByID, - ChangedByRole: changedByRole, - } - return s.repo.CreateStatusHistory(ctx, history) -} - -func (s *pickupStatusHistoryService) GetStatusHistory(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) { - return s.repo.GetStatusHistoryByRequestID(ctx, requestID) -} diff --git a/internal/services/pickup_maching_service.go b/internal/services/pickup_maching_service.go deleted file mode 100644 index ae36e1a..0000000 --- a/internal/services/pickup_maching_service.go +++ /dev/null @@ -1,146 +0,0 @@ -package services - -import ( - "context" - "fmt" - "rijig/dto" - "rijig/internal/repositories" - "rijig/utils" -) - -type PickupMatchingService interface { - FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]dto.NearbyCollectorDTO, error) - FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]dto.PickupRequestForCollectorDTO, error) -} - -type pickupMatchingService struct { - pickupRepo repositories.RequestPickupRepository - collectorRepo repositories.CollectorRepository -} - -func NewPickupMatchingService(pickupRepo repositories.RequestPickupRepository, collectorRepo repositories.CollectorRepository) PickupMatchingService { - return &pickupMatchingService{ - pickupRepo: pickupRepo, - collectorRepo: collectorRepo, - } -} - -func (s *pickupMatchingService) FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]dto.NearbyCollectorDTO, error) { - pickup, err := s.pickupRepo.GetPickupWithItemsAndAddress(ctx, pickupID) - if err != nil { - return nil, fmt.Errorf("pickup tidak ditemukan: %w", err) - } - - userCoord := utils.Coord{ - Lat: pickup.Address.Latitude, - Lon: pickup.Address.Longitude, - } - - requestedTrash := make(map[string]bool) - for _, item := range pickup.RequestItems { - requestedTrash[item.TrashCategoryId] = true - } - - collectors, err := s.collectorRepo.GetActiveCollectorsWithTrashAndAddress(ctx) - if err != nil { - return nil, fmt.Errorf("gagal mengambil data collector: %w", err) - } - - var result []dto.NearbyCollectorDTO - for _, col := range collectors { - coord := utils.Coord{ - Lat: col.Address.Latitude, - Lon: col.Address.Longitude, - } - - _, km := utils.Distance(userCoord, coord) - if km > 10 { - continue - } - - var matchedTrash []string - for _, item := range col.AvaibleTrashByCollector { - if requestedTrash[item.TrashCategoryID] { - matchedTrash = append(matchedTrash, item.TrashCategoryID) - } - } - - if len(matchedTrash) == 0 { - continue - } - - result = append(result, dto.NearbyCollectorDTO{ - CollectorID: col.ID, - Name: col.User.Name, - Phone: col.User.Phone, - Rating: col.Rating, - Latitude: col.Address.Latitude, - Longitude: col.Address.Longitude, - DistanceKm: km, - MatchedTrash: matchedTrash, - }) - } - - return result, nil -} - -// terdpaat error seperti ini: "undefined: dto.PickupRequestForCollectorDTO" dan seprti ini: s.collectorRepo.GetCollectorWithAddressAndTrash undefined (type repositories.CollectorRepository has no field or method GetCollectorWithAddressAndTrash) pada kode berikut: - -func (s *pickupMatchingService) FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]dto.PickupRequestForCollectorDTO, error) { - collector, err := s.collectorRepo.GetCollectorWithAddressAndTrash(ctx, collectorID) - if err != nil { - return nil, fmt.Errorf("collector tidak ditemukan: %w", err) - } - - pickupList, err := s.pickupRepo.GetAllAutomaticRequestsWithAddress(ctx) - if err != nil { - return nil, fmt.Errorf("gagal mengambil pickup otomatis: %w", err) - } - - collectorCoord := utils.Coord{ - Lat: collector.Address.Latitude, - Lon: collector.Address.Longitude, - } - - // map trash collector - collectorTrash := make(map[string]bool) - for _, t := range collector.AvaibleTrashByCollector { - collectorTrash[t.TrashCategoryID] = true - } - - var results []dto.PickupRequestForCollectorDTO - for _, p := range pickupList { - if p.StatusPickup != "waiting_collector" { - continue - } - coord := utils.Coord{ - Lat: p.Address.Latitude, - Lon: p.Address.Longitude, - } - _, km := utils.Distance(collectorCoord, coord) - if km > 10 { - continue - } - - match := false - var matchedTrash []string - for _, item := range p.RequestItems { - if collectorTrash[item.TrashCategoryId] { - match = true - matchedTrash = append(matchedTrash, item.TrashCategoryId) - } - } - if match { - results = append(results, dto.PickupRequestForCollectorDTO{ - PickupID: p.ID, - UserID: p.UserId, - Latitude: p.Address.Latitude, - Longitude: p.Address.Longitude, - DistanceKm: km, - MatchedTrash: matchedTrash, - }) - } - } - - return results, nil -} diff --git a/internal/services/product_service.go b/internal/services/product_service.go deleted file mode 100644 index f448396..0000000 --- a/internal/services/product_service.go +++ /dev/null @@ -1,404 +0,0 @@ -package services - -import ( - "fmt" - "mime/multipart" - "os" - "path/filepath" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type ProductService interface { - SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) - CreateProduct(userID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) - - GetAllProductsByStoreID(userID string, page, limit int) ([]dto.ResponseProductDTO, int64, error) - GetProductByID(productID string) (*dto.ResponseProductDTO, error) - - UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, error) - DeleteProduct(productID string) error - DeleteProducts(productIDs []string) error - DeleteProductImage(imageID string) error - DeleteProductImages(imageIDs []string) error - deleteImageFile(imageID string) error -} - -type productService struct { - productRepo repositories.ProductRepository - storeRepo repositories.StoreRepository -} - -func NewProductService(productRepo repositories.ProductRepository, storeRepo repositories.StoreRepository) ProductService { - return &productService{productRepo, storeRepo} -} - -func (s *productService) CreateProduct(userID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, 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 %s", userID) - } - - var imagePaths []string - var productImages []model.ProductImage - for _, file := range productDTO.ProductImages { - imagePath, err := s.SaveProductImage(file, "product") - if err != nil { - return nil, fmt.Errorf("failed to save product image: %w", err) - } - imagePaths = append(imagePaths, imagePath) - - productImages = append(productImages, model.ProductImage{ - ImageURL: imagePath, - }) - } - - if len(imagePaths) == 0 { - return nil, fmt.Errorf("at least one image is required for the product") - } - - product := model.Product{ - StoreID: store.ID, - ProductName: productDTO.ProductName, - Quantity: productDTO.Quantity, - } - - product.ProductImages = productImages - - if err := s.productRepo.CreateProduct(&product); err != nil { - return nil, fmt.Errorf("failed to create product: %w", err) - } - - createdAt, err := utils.FormatDateToIndonesianFormat(product.CreatedAt) - if err != nil { - return nil, fmt.Errorf("failed to format createdAt: %w", err) - } - updatedAt, err := utils.FormatDateToIndonesianFormat(product.UpdatedAt) - if err != nil { - return nil, fmt.Errorf("failed to format updatedAt: %w", err) - } - - var productImagesDTO []dto.ResponseProductImageDTO - for _, img := range product.ProductImages { - productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ - ID: img.ID, - ProductID: img.ProductID, - ImageURL: img.ImageURL, - }) - } - - productDTOResponse := &dto.ResponseProductDTO{ - ID: product.ID, - StoreID: product.StoreID, - ProductName: product.ProductName, - Quantity: product.Quantity, - ProductImages: productImagesDTO, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return productDTOResponse, nil -} - -func (s *productService) GetAllProductsByStoreID(userID string, page, limit int) ([]dto.ResponseProductDTO, int64, error) { - - store, err := s.storeRepo.FindStoreByUserID(userID) - if err != nil { - return nil, 0, fmt.Errorf("error retrieving store by user ID: %w", err) - } - if store == nil { - return nil, 0, fmt.Errorf("store not found for user %s", userID) - } - - total, err := s.productRepo.CountProductsByStoreID(store.ID) - if err != nil { - return nil, 0, fmt.Errorf("error counting products: %w", err) - } - - products, err := s.productRepo.FindProductsByStoreID(store.ID, page, limit) - if err != nil { - return nil, 0, fmt.Errorf("error fetching products: %w", err) - } - - var productDTOs []dto.ResponseProductDTO - for _, product := range products { - productImages, err := s.productRepo.FindProductImagesByProductID(product.ID) - if err != nil { - return nil, 0, fmt.Errorf("error fetching product images: %w", err) - } - - var productImagesDTO []dto.ResponseProductImageDTO - for _, img := range productImages { - productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ - ID: img.ID, - ProductID: img.ProductID, - ImageURL: img.ImageURL, - }) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) - - productDTOs = append(productDTOs, dto.ResponseProductDTO{ - ID: product.ID, - StoreID: product.StoreID, - ProductName: product.ProductName, - Quantity: product.Quantity, - Saled: product.Saled, - ProductImages: productImagesDTO, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - return productDTOs, total, nil -} - -func (s *productService) GetProductByID(productID string) (*dto.ResponseProductDTO, error) { - - product, err := s.productRepo.GetProductByID(productID) - if err != nil { - return nil, fmt.Errorf("failed to retrieve product: %w", err) - } - if product == nil { - return nil, fmt.Errorf("product not found") - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) - - productDTO := &dto.ResponseProductDTO{ - ID: product.ID, - StoreID: product.StoreID, - ProductName: product.ProductName, - Quantity: product.Quantity, - Saled: product.Saled, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - var productImagesDTO []dto.ResponseProductImageDTO - for _, image := range product.ProductImages { - productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ - ID: image.ID, - ProductID: image.ProductID, - ImageURL: image.ImageURL, - }) - } - - productDTO.ProductImages = productImagesDTO - - return productDTO, nil -} - -func (s *productService) UpdateProduct(userID, productID string, productDTO *dto.RequestProductDTO) (*dto.ResponseProductDTO, 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 %s", userID) - } - - product, err := s.productRepo.GetProductByID(productID) - if err != nil { - return nil, fmt.Errorf("failed to retrieve product: %v", err) - } - if product == nil { - return nil, fmt.Errorf("product not found") - } - - if product.StoreID != store.ID { - return nil, fmt.Errorf("user does not own the store for this product") - } - - if err := s.deleteProductImages(productID); err != nil { - return nil, fmt.Errorf("failed to delete old product images: %v", err) - } - - var productImages []model.ProductImage - for _, file := range productDTO.ProductImages { - imagePath, err := s.SaveProductImage(file, "product") - if err != nil { - return nil, fmt.Errorf("failed to save product image: %w", err) - } - - productImages = append(productImages, model.ProductImage{ - ImageURL: imagePath, - }) - } - - product.ProductName = productDTO.ProductName - product.Quantity = productDTO.Quantity - product.ProductImages = productImages - - if err := s.productRepo.UpdateProduct(product); err != nil { - return nil, fmt.Errorf("failed to update product: %w", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(product.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(product.UpdatedAt) - - var productImagesDTO []dto.ResponseProductImageDTO - for _, img := range product.ProductImages { - productImagesDTO = append(productImagesDTO, dto.ResponseProductImageDTO{ - ID: img.ID, - ProductID: img.ProductID, - ImageURL: img.ImageURL, - }) - } - - productDTOResponse := &dto.ResponseProductDTO{ - ID: product.ID, - StoreID: product.StoreID, - ProductName: product.ProductName, - Quantity: product.Quantity, - ProductImages: productImagesDTO, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return productDTOResponse, nil -} - -func (s *productService) SaveProductImage(file *multipart.FileHeader, imageType string) (string, error) { - - imageDir := fmt.Sprintf("./public%s/uploads/store/%s", os.Getenv("BASE_URL"), 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 *productService) DeleteProduct(productID string) error { - - if err := s.deleteProductImages(productID); err != nil { - return fmt.Errorf("failed to delete associated product images: %w", err) - } - - if err := s.productRepo.DeleteProduct(productID); err != nil { - return fmt.Errorf("failed to delete product: %w", err) - } - return nil -} - -func (s *productService) DeleteProducts(productIDs []string) error { - - for _, productID := range productIDs { - if err := s.deleteProductImages(productID); err != nil { - return fmt.Errorf("failed to delete associated images for product %s: %w", productID, err) - } - } - - if err := s.productRepo.DeleteProductsByID(productIDs); err != nil { - return fmt.Errorf("failed to delete products: %w", err) - } - - return nil -} - -func (s *productService) DeleteProductImage(imageID string) error { - - if err := s.deleteImageFile(imageID); err != nil { - return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) - } - - if err := s.productRepo.DeleteProductImageByID(imageID); err != nil { - return fmt.Errorf("failed to delete product image from database: %w", err) - } - - return nil -} - -func (s *productService) DeleteProductImages(imageIDs []string) error { - - for _, imageID := range imageIDs { - if err := s.deleteImageFile(imageID); err != nil { - return fmt.Errorf("failed to delete image file with ID %s: %w", imageID, err) - } - } - - if err := s.productRepo.DeleteProductImagesByID(imageIDs); err != nil { - return fmt.Errorf("failed to delete product images from database: %w", err) - } - - return nil -} - -func (s *productService) deleteProductImages(productID string) error { - productImages, err := s.productRepo.FindProductImagesByProductID(productID) - if err != nil { - return fmt.Errorf("failed to fetch product images: %w", err) - } - - for _, img := range productImages { - if err := s.deleteImageFile(img.ID); err != nil { - return fmt.Errorf("failed to delete image file: %w", err) - } - } - - if err := s.productRepo.DeleteProductImagesByProductID(productID); err != nil { - return fmt.Errorf("failed to delete product images from database: %w", err) - } - - return nil -} - -func (s *productService) deleteImageFile(imageID string) error { - productImage, err := s.productRepo.GetProductImageByID(imageID) - if err != nil { - return fmt.Errorf("failed to fetch product image: %w", err) - } - - if productImage == nil { - return fmt.Errorf("product image with ID %s not found", imageID) - } - - baseURL := os.Getenv("BASE_URL") - - imagePath := fmt.Sprintf("./public%s%s", baseURL, productImage.ImageURL) - - if err := os.Remove(imagePath); err != nil { - return fmt.Errorf("failed to delete image file: %w", err) - } - - return nil -} diff --git a/internal/services/rating_service.go b/internal/services/rating_service.go deleted file mode 100644 index e5d7c55..0000000 --- a/internal/services/rating_service.go +++ /dev/null @@ -1,43 +0,0 @@ -package services - -import ( - "context" - "time" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" -) - -type PickupRatingService interface { - CreateRating(ctx context.Context, userID, pickupID, collectorID string, input dto.CreatePickupRatingDTO) error - GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) - GetAverageRating(ctx context.Context, collectorID string) (float32, error) -} - -type pickupRatingService struct { - repo repositories.PickupRatingRepository -} - -func NewPickupRatingService(repo repositories.PickupRatingRepository) PickupRatingService { - return &pickupRatingService{repo: repo} -} - -func (s *pickupRatingService) CreateRating(ctx context.Context, userID, pickupID, collectorID string, input dto.CreatePickupRatingDTO) error { - rating := model.PickupRating{ - RequestID: pickupID, - UserID: userID, - CollectorID: collectorID, - Rating: input.Rating, - Feedback: input.Feedback, - CreatedAt: time.Now(), - } - return s.repo.CreateRating(ctx, rating) -} - -func (s *pickupRatingService) GetRatingsByCollector(ctx context.Context, collectorID string) ([]model.PickupRating, error) { - return s.repo.GetRatingsByCollector(ctx, collectorID) -} - -func (s *pickupRatingService) GetAverageRating(ctx context.Context, collectorID string) (float32, error) { - return s.repo.CalculateAverageRating(ctx, collectorID) -} diff --git a/internal/services/request_pickup_service.go b/internal/services/request_pickup_service.go deleted file mode 100644 index 363d6a4..0000000 --- a/internal/services/request_pickup_service.go +++ /dev/null @@ -1,137 +0,0 @@ -package services - -import ( - "context" - "fmt" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "time" -) - -type RequestPickupService interface { - ConvertCartToRequestPickup(ctx context.Context, userID string, req dto.RequestPickupDTO) error - AssignCollectorToRequest(ctx context.Context, pickupID string, req dto.SelectCollectorDTO) error - FindRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]dto.AssignedPickupDTO, error) - ConfirmPickupByCollector(ctx context.Context, pickupID string, confirmedAt time.Time) error - UpdatePickupStatusToPickingUp(ctx context.Context, pickupID string) error - UpdateActualPickupItems(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error -} - -type requestPickupService struct { - trashRepo repositories.TrashRepository - pickupRepo repositories.RequestPickupRepository - cartService CartService - historyRepo repositories.PickupStatusHistoryRepository -} - -func NewRequestPickupService(trashRepo repositories.TrashRepository, pickupRepo repositories.RequestPickupRepository, cartService CartService, historyRepo repositories.PickupStatusHistoryRepository) RequestPickupService { - return &requestPickupService{ - trashRepo: trashRepo, - pickupRepo: pickupRepo, - cartService: cartService, - historyRepo: historyRepo, - } -} - -func (s *requestPickupService) ConvertCartToRequestPickup(ctx context.Context, userID string, req dto.RequestPickupDTO) error { - cart, err := s.cartService.GetCart(ctx, userID) - if err != nil || len(cart.CartItems) == 0 { - return fmt.Errorf("cart kosong atau tidak ditemukan") - } - - var requestItems []model.RequestPickupItem - for _, item := range cart.CartItems { - trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) - if err != nil { - continue - } - subtotal := float64(item.Amount) * trash.EstimatedPrice - - requestItems = append(requestItems, model.RequestPickupItem{ - TrashCategoryId: item.TrashID, - EstimatedAmount: float64(item.Amount), - EstimatedPricePerKg: trash.EstimatedPrice, - EstimatedSubtotalPrice: subtotal, - }) - } - - if len(requestItems) == 0 { - return fmt.Errorf("tidak ada item valid dalam cart") - } - - pickup := model.RequestPickup{ - UserId: userID, - AddressId: req.AddressID, - RequestMethod: req.RequestMethod, - Notes: req.Notes, - StatusPickup: "waiting_collector", - RequestItems: requestItems, - } - - if err := s.pickupRepo.CreateRequestPickup(ctx, &pickup); err != nil { - return fmt.Errorf("gagal menyimpan request pickup: %w", err) - } - - if err := s.cartService.ClearCart(ctx, userID); err != nil { - return fmt.Errorf("request berhasil, tapi gagal hapus cart: %w", err) - } - - return nil -} - -func (s *requestPickupService) AssignCollectorToRequest(ctx context.Context, pickupID string, req dto.SelectCollectorDTO) error { - if req.CollectorID == "" { - return fmt.Errorf("collector_id tidak boleh kosong") - } - return s.pickupRepo.UpdateCollectorID(ctx, pickupID, req.CollectorID) -} - -func (s *requestPickupService) FindRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]dto.AssignedPickupDTO, error) { - pickups, err := s.pickupRepo.GetRequestsAssignedToCollector(ctx, collectorID) - if err != nil { - return nil, err - } - - var result []dto.AssignedPickupDTO - for _, p := range pickups { - var matchedTrash []string - for _, item := range p.RequestItems { - matchedTrash = append(matchedTrash, item.TrashCategoryId) - } - - result = append(result, dto.AssignedPickupDTO{ - PickupID: p.ID, - UserID: p.UserId, - UserName: p.User.Name, - Latitude: p.Address.Latitude, - Longitude: p.Address.Longitude, - Notes: p.Notes, - MatchedTrash: matchedTrash, - }) - } - - return result, nil -} - -func (s *requestPickupService) ConfirmPickupByCollector(ctx context.Context, pickupID string, confirmedAt time.Time) error { - return s.pickupRepo.UpdatePickupStatusAndConfirmationTime(ctx, pickupID, "confirmed_by_collector", confirmedAt) -} - -func (s *requestPickupService) UpdatePickupStatusToPickingUp(ctx context.Context, pickupID string) error { - err := s.pickupRepo.UpdatePickupStatus(ctx, pickupID, "collector_are_picking_up") - if err != nil { - return err - } - return s.historyRepo.CreateStatusHistory(ctx, model.PickupStatusHistory{ - RequestID: pickupID, - Status: "collector_are_picking_up", - ChangedAt: time.Now(), - ChangedByID: "collector", - ChangedByRole: "collector", - }) -} - -func (s *requestPickupService) UpdateActualPickupItems(ctx context.Context, pickupID string, items []dto.UpdateRequestPickupItemDTO) error { - return s.pickupRepo.UpdateRequestPickupItemsAmountAndPrice(ctx, pickupID, items) -} diff --git a/internal/services/role_service.go b/internal/services/role_service.go deleted file mode 100644 index 5d938bc..0000000 --- a/internal/services/role_service.go +++ /dev/null @@ -1,103 +0,0 @@ -package services - -import ( - "context" - "fmt" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/utils" -) - -type RoleService interface { - GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) - GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error) -} - -type roleService struct { - RoleRepo repositories.RoleRepository -} - -func NewRoleService(roleRepo repositories.RoleRepository) RoleService { - return &roleService{RoleRepo: roleRepo} -} - -func (s *roleService) GetRoles(ctx context.Context) ([]dto.RoleResponseDTO, error) { - cacheKey := "roles_list" - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var roles []dto.RoleResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - role, ok := item.(map[string]interface{}) - if ok { - roles = append(roles, dto.RoleResponseDTO{ - ID: role["role_id"].(string), - RoleName: role["role_name"].(string), - CreatedAt: role["createdAt"].(string), - UpdatedAt: role["updatedAt"].(string), - }) - } - } - return roles, nil - } - } - - roles, err := s.RoleRepo.FindAll(ctx) - if err != nil { - return nil, fmt.Errorf("failed to fetch roles: %v", err) - } - - var roleDTOs []dto.RoleResponseDTO - for _, role := range roles { - createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt) - - roleDTOs = append(roleDTOs, dto.RoleResponseDTO{ - ID: role.ID, - RoleName: role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": roleDTOs, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching roles data to Redis: %v\n", err) - } - - return roleDTOs, nil -} - -func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*dto.RoleResponseDTO, error) { - - role, err := s.RoleRepo.FindByID(ctx, roleID) - if err != nil { - return nil, fmt.Errorf("role not found: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt) - - roleDTO := &dto.RoleResponseDTO{ - ID: role.ID, - RoleName: role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("role:%s", roleID) - cacheData := map[string]interface{}{ - "data": roleDTO, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching role data to Redis: %v\n", err) - } - - return roleDTO, nil -} diff --git a/internal/services/store_service.go b/internal/services/store_service.go deleted file mode 100644 index ef855c7..0000000 --- a/internal/services/store_service.go +++ /dev/null @@ -1,294 +0,0 @@ -package services - -import ( - "fmt" - "mime/multipart" - "os" - "path/filepath" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type StoreService interface { - 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 { - storeRepo repositories.StoreRepository -} - -func NewStoreService(storeRepo repositories.StoreRepository) StoreService { - return &storeService{storeRepo} -} - -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 { - 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") - } - - 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: storeLogoPath, - StoreBanner: storeBannerPath, - 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 -} - -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%s/uploads/store/%s", os.Getenv("BASE_URL"), 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/internal/services/trash_service.go b/internal/services/trash_service.go deleted file mode 100644 index c4601e9..0000000 --- a/internal/services/trash_service.go +++ /dev/null @@ -1,631 +0,0 @@ -package services - -import ( - "fmt" - "log" - "mime/multipart" - "os" - "path/filepath" - "strconv" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "github.com/google/uuid" -) - -type TrashService interface { - CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) - AddDetailToCategory(request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) - - GetCategories() ([]dto.ResponseTrashCategoryDTO, error) - GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO, error) - GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDTO, error) - - UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) - UpdateDetail(id string, request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) - - DeleteCategory(id string) error - DeleteDetail(id string) error -} - -type trashService struct { - TrashRepo repositories.TrashRepository -} - -func NewTrashService(trashRepo repositories.TrashRepository) TrashService { - return &trashService{TrashRepo: trashRepo} -} - -func (s *trashService) saveIconOfTrash(iconTrash *multipart.FileHeader) (string, error) { - pathImage := "/uploads/icontrash/" - iconTrashDir := "./public" + os.Getenv("BASE_URL") + pathImage - if _, err := os.Stat(iconTrashDir); os.IsNotExist(err) { - - if err := os.MkdirAll(iconTrashDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for icon trash: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true} - extension := filepath.Ext(iconTrash.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - iconTrashFIleName := fmt.Sprintf("%s_icontrash%s", uuid.New().String(), extension) - iconTrashPath := filepath.Join(iconTrashDir, iconTrashFIleName) - - src, err := iconTrash.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(iconTrashPath) - if err != nil { - return "", fmt.Errorf("failed to create icon trash file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save icon trash: %v", err) - } - - iconTrashUrl := fmt.Sprintf("%s%s", pathImage, iconTrashFIleName) - - return iconTrashUrl, nil -} - -func deleteIconTrashFIle(imagePath string) error { - if imagePath == "" { - return nil - } - - baseDir := "./public/" + os.Getenv("BASE_URL") - absolutePath := baseDir + imagePath - - if _, err := os.Stat(absolutePath); os.IsNotExist(err) { - return fmt.Errorf("image file not found: %v", err) - } - - err := os.Remove(absolutePath) - if err != nil { - return fmt.Errorf("failed to delete image: %v", err) - } - - log.Printf("Image deleted successfully: %s", absolutePath) - return nil -} - -func (s *trashService) CreateCategory(request dto.RequestTrashCategoryDTO, iconTrash *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { - - parsedPrice, err := strconv.ParseFloat(request.EstimatedPrice, 64) - fmt.Println("Received estimatedprice:", request.EstimatedPrice) - if err != nil { - return nil, fmt.Errorf("gagal memvalidasi harga: %v", err) - } else { - fmt.Printf("hasil parsing%v", parsedPrice) - } - - icontrashPath, err := s.saveIconOfTrash(iconTrash) - if err != nil { - return nil, fmt.Errorf("gagal menyimpan ikon sampah: %v", err) - } - - category := model.TrashCategory{ - Name: request.Name, - - EstimatedPrice: parsedPrice, - Icon: icontrashPath, - } - - if err := s.TrashRepo.CreateCategory(&category); err != nil { - return nil, fmt.Errorf("failed to create category: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - - categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - EstimatedPrice: float64(category.EstimatedPrice), - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - if err := s.CacheCategoryAndDetails(category.ID, categoryResponseDTO, nil, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching category: %v", err) - } - - categories, err := s.TrashRepo.GetCategories() - if err == nil { - var categoriesDTO []dto.ResponseTrashCategoryDTO - for _, c := range categories { - ccreatedAt, _ := utils.FormatDateToIndonesianFormat(c.CreatedAt) - cupdatedAt, _ := utils.FormatDateToIndonesianFormat(c.UpdatedAt) - categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: c.ID, - Name: c.Name, - EstimatedPrice: float64(c.EstimatedPrice), - Icon: c.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, - }) - } - - if err := s.CacheCategoryList(categoriesDTO, time.Hour*6); err != nil { - fmt.Printf("Error caching all categories: %v\n", err) - } - } - - return categoryResponseDTO, nil -} - -func (s *trashService) AddDetailToCategory(request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) { - errors, valid := request.ValidateTrashDetailInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - detail := model.TrashDetail{ - CategoryID: request.CategoryID, - Description: request.Description, - Price: request.Price, - } - - if err := s.TrashRepo.AddDetailToCategory(&detail); err != nil { - return nil, fmt.Errorf("failed to add detail to category: %v", err) - } - - dcreatedAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) - dupdatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) - - detailResponseDTO := &dto.ResponseTrashDetailDTO{ - ID: detail.ID, - CategoryID: detail.CategoryID, - Description: detail.Description, - Price: detail.Price, - CreatedAt: dcreatedAt, - UpdatedAt: dupdatedAt, - } - - cacheKey := fmt.Sprintf("detail:%s", detail.ID) - cacheData := map[string]interface{}{ - "data": detailResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching detail: %v", err) - } - - category, err := s.TrashRepo.GetCategoryByID(detail.CategoryID) - - if err == nil { - - ccreatedAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - cupdatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - - categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, - } - - if err := s.CacheCategoryAndDetails(detail.CategoryID, categoryResponseDTO, category.Details, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching updated category: %v", err) - } - } else { - return nil, fmt.Errorf("error fetching category for cache update: %v", err) - } - - return detailResponseDTO, nil -} - -func (s *trashService) GetCategories() ([]dto.ResponseTrashCategoryDTO, error) { - cacheKey := "categories:all" - cachedCategories, err := utils.GetJSONData(cacheKey) - if err == nil && cachedCategories != nil { - var categoriesDTO []dto.ResponseTrashCategoryDTO - for _, category := range cachedCategories["data"].([]interface{}) { - categoryData := category.(map[string]interface{}) - categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: categoryData["id"].(string), - Name: categoryData["name"].(string), - EstimatedPrice: categoryData["estimatedprice"].(float64), - Icon: categoryData["icon"].(string), - CreatedAt: categoryData["createdAt"].(string), - UpdatedAt: categoryData["updatedAt"].(string), - }) - } - return categoriesDTO, nil - } - - categories, err := s.TrashRepo.GetCategories() - if err != nil { - return nil, fmt.Errorf("failed to fetch categories: %v", err) - } - - var categoriesDTO []dto.ResponseTrashCategoryDTO - for _, category := range categories { - - createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - EstimatedPrice: category.EstimatedPrice, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - - cacheData := map[string]interface{}{ - "data": categoriesDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*6); err != nil { - fmt.Printf("Error caching categories: %v\n", err) - } - - return categoriesDTO, nil -} - -func (s *trashService) GetCategoryByID(id string) (*dto.ResponseTrashCategoryDTO, error) { - cacheKey := fmt.Sprintf("category:%s", id) - cachedCategory, err := utils.GetJSONData(cacheKey) - if err == nil && cachedCategory != nil { - categoryData := cachedCategory["data"].(map[string]interface{}) - details := mapDetails(cachedCategory["details"]) - return &dto.ResponseTrashCategoryDTO{ - ID: categoryData["id"].(string), - Name: categoryData["name"].(string), - EstimatedPrice: categoryData["estimatedprice"].(float64), - Icon: categoryData["icon"].(string), - CreatedAt: categoryData["createdAt"].(string), - UpdatedAt: categoryData["updatedAt"].(string), - Details: details, - }, nil - } - - category, err := s.TrashRepo.GetCategoryByID(id) - if err != nil { - return nil, fmt.Errorf("category not found: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - - categoryDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - EstimatedPrice: category.EstimatedPrice, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - if category.Details != nil { - var detailsDTO []dto.ResponseTrashDetailDTO - for _, detail := range category.Details { - createdAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) - detailsDTO = append(detailsDTO, dto.ResponseTrashDetailDTO{ - ID: detail.ID, - CategoryID: detail.CategoryID, - Description: detail.Description, - Price: detail.Price, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - }) - } - categoryDTO.Details = detailsDTO - } - - if err := s.CacheCategoryAndDetails(category.ID, categoryDTO, categoryDTO.Details, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching category and details: %v", err) - } - - return categoryDTO, nil -} - -func (s *trashService) GetTrashDetailByID(id string) (*dto.ResponseTrashDetailDTO, error) { - cacheKey := fmt.Sprintf("detail:%s", id) - cachedDetail, err := utils.GetJSONData(cacheKey) - if err == nil && cachedDetail != nil { - detailData := cachedDetail["data"].(map[string]interface{}) - return &dto.ResponseTrashDetailDTO{ - ID: detailData["id"].(string), - CategoryID: detailData["category_id"].(string), - Description: detailData["description"].(string), - Price: detailData["price"].(float64), - CreatedAt: detailData["createdAt"].(string), - UpdatedAt: detailData["updatedAt"].(string), - }, nil - } - - detail, err := s.TrashRepo.GetTrashDetailByID(id) - if err != nil { - return nil, fmt.Errorf("trash detail not found: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) - - detailDTO := &dto.ResponseTrashDetailDTO{ - ID: detail.ID, - CategoryID: detail.CategoryID, - Description: detail.Description, - Price: detail.Price, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheData := map[string]interface{}{ - "data": detailDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { - return nil, fmt.Errorf("error caching detail: %v", err) - } - - return detailDTO, nil -} - -func (s *trashService) UpdateCategory(id string, request dto.RequestTrashCategoryDTO, iconPath *multipart.FileHeader) (*dto.ResponseTrashCategoryDTO, error) { - errors, valid := request.ValidateTrashCategoryInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - category, err := s.TrashRepo.GetCategoryByID(id) - if err != nil { - return nil, fmt.Errorf("category not found: %v", err) - } - - if category.Icon != "" { - err := deleteIconTrashFIle(category.Icon) - if err != nil { - return nil, fmt.Errorf("failed to delete old image: %v", err) - } - } - - var iconTrashPath string - if iconPath != nil { - iconTrashPath, err = s.saveIconOfTrash(iconPath) - if err != nil { - return nil, fmt.Errorf("failed to save card photo: %v", err) - } - } - - if iconTrashPath != "" { - category.Icon = iconTrashPath - } - - category, err = s.TrashRepo.UpdateCategory(id, category) - if err != nil { - log.Printf("Error updating trash category: %v", err) - return nil, fmt.Errorf("failed to update category: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - - categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - EstimatedPrice: category.EstimatedPrice, - Icon: category.Icon, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - if err := s.CacheCategoryAndDetails(category.ID, categoryResponseDTO, category.Details, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching updated category: %v", err) - } - - allCategories, err := s.TrashRepo.GetCategories() - if err == nil { - var categoriesDTO []dto.ResponseTrashCategoryDTO - for _, c := range allCategories { - ccreatedAt, _ := utils.FormatDateToIndonesianFormat(c.CreatedAt) - cupdatedAt, _ := utils.FormatDateToIndonesianFormat(c.UpdatedAt) - categoriesDTO = append(categoriesDTO, dto.ResponseTrashCategoryDTO{ - ID: c.ID, - Name: c.Name, - EstimatedPrice: c.EstimatedPrice, - Icon: c.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, - }) - } - - if err := s.CacheCategoryList(categoriesDTO, time.Hour*6); err != nil { - fmt.Printf("Error caching all categories: %v\n", err) - } - } - - return categoryResponseDTO, nil -} - -func (s *trashService) UpdateDetail(id string, request dto.RequestTrashDetailDTO) (*dto.ResponseTrashDetailDTO, error) { - errors, valid := request.ValidateTrashDetailInput() - if !valid { - return nil, fmt.Errorf("validation error: %v", errors) - } - - if err := s.TrashRepo.UpdateTrashDetail(id, request.Description, request.Price); err != nil { - return nil, fmt.Errorf("failed to update detail: %v", err) - } - - detail, err := s.TrashRepo.GetTrashDetailByID(id) - if err != nil { - return nil, fmt.Errorf("trash detail not found: %v", err) - } - - createdAt, _ := utils.FormatDateToIndonesianFormat(detail.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(detail.UpdatedAt) - - detailResponseDTO := &dto.ResponseTrashDetailDTO{ - ID: detail.ID, - CategoryID: detail.CategoryID, - Description: detail.Description, - Price: detail.Price, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - cacheKey := fmt.Sprintf("detail:%s", detail.ID) - cacheData := map[string]interface{}{ - "data": detailResponseDTO, - } - if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching updated detail: %v", err) - } - - category, err := s.TrashRepo.GetCategoryByID(detail.CategoryID) - if err == nil { - - ccreatedAt, _ := utils.FormatDateToIndonesianFormat(category.CreatedAt) - cupdatedAt, _ := utils.FormatDateToIndonesianFormat(category.UpdatedAt) - - categoryResponseDTO := &dto.ResponseTrashCategoryDTO{ - ID: category.ID, - Name: category.Name, - Icon: category.Icon, - CreatedAt: ccreatedAt, - UpdatedAt: cupdatedAt, - } - - if err := s.CacheCategoryAndDetails(detail.CategoryID, categoryResponseDTO, category.Details, time.Hour*6); err != nil { - return nil, fmt.Errorf("error caching updated category: %v", err) - } - } else { - fmt.Printf("Error fetching category for cache update: %v\n", err) - } - - return detailResponseDTO, nil -} - -func (s *trashService) DeleteCategory(id string) error { - detailsCacheKeyPrefix := "detail:" - details, err := s.TrashRepo.GetDetailsByCategoryID(id) - if err != nil { - return fmt.Errorf("failed to fetch details for category %s: %v", id, err) - } - - for _, detail := range details { - detailCacheKey := detailsCacheKeyPrefix + detail.ID - if err := s.deleteCache(detailCacheKey); err != nil { - return fmt.Errorf("error clearing cache for deleted detail: %v", err) - } - } - - category, err := s.TrashRepo.GetCategoryByID(id) - if err != nil { - return fmt.Errorf("failed to fetch category for deletion: %v", err) - } - - if err := deleteIconTrashFIle(category.Icon); err != nil { - return fmt.Errorf("error deleting icon for category %s: %v", id, err) - } - - if err := s.TrashRepo.DeleteCategory(id); err != nil { - return fmt.Errorf("failed to delete category: %v", err) - } - - if err := s.deleteCache("category:" + id); err != nil { - return fmt.Errorf("error clearing cache for deleted category: %v", err) - } - - if err := s.deleteCache("categories:all"); err != nil { - return fmt.Errorf("error clearing categories list cache: %v", err) - } - - return nil -} - -func (s *trashService) DeleteDetail(id string) error { - - detail, err := s.TrashRepo.GetTrashDetailByID(id) - if err != nil { - return fmt.Errorf("trash detail not found: %v", err) - } - - if err := s.TrashRepo.DeleteTrashDetail(id); err != nil { - return fmt.Errorf("failed to delete detail: %v", err) - } - - detailCacheKey := fmt.Sprintf("detail:%s", id) - if err := s.deleteCache(detailCacheKey); err != nil { - return fmt.Errorf("error clearing cache for deleted detail: %v", err) - } - - categoryCacheKey := fmt.Sprintf("category:%s", detail.CategoryID) - if err := s.deleteCache(categoryCacheKey); err != nil { - return fmt.Errorf("error clearing cache for category after detail deletion: %v", err) - } - - return nil -} - -func mapDetails(details interface{}) []dto.ResponseTrashDetailDTO { - var detailsDTO []dto.ResponseTrashDetailDTO - if details != nil { - for _, detail := range details.([]interface{}) { - detailData := detail.(map[string]interface{}) - detailsDTO = append(detailsDTO, dto.ResponseTrashDetailDTO{ - ID: detailData["id"].(string), - CategoryID: detailData["category_id"].(string), - Description: detailData["description"].(string), - Price: detailData["price"].(float64), - CreatedAt: detailData["createdAt"].(string), - UpdatedAt: detailData["updatedAt"].(string), - }) - } - } - return detailsDTO -} - -func (s *trashService) CacheCategoryAndDetails(categoryID string, categoryData interface{}, detailsData interface{}, expiration time.Duration) error { - cacheKey := fmt.Sprintf("category:%s", categoryID) - cacheData := map[string]interface{}{ - "data": categoryData, - "details": detailsData, - } - - err := utils.SetJSONData(cacheKey, cacheData, expiration) - if err != nil { - return fmt.Errorf("error caching category and details: %v", err) - } - - return nil -} - -func (s *trashService) CacheCategoryList(categoriesData interface{}, expiration time.Duration) error { - cacheKey := "categories:all" - cacheData := map[string]interface{}{ - "data": categoriesData, - } - - err := utils.SetJSONData(cacheKey, cacheData, expiration) - if err != nil { - return fmt.Errorf("error caching categories list: %v", err) - } - - return nil -} - -func (s *trashService) deleteCache(cacheKey string) error { - if err := utils.DeleteData(cacheKey); err != nil { - fmt.Printf("Error clearing cache for key: %v\n", cacheKey) - return fmt.Errorf("error clearing cache for key %s: %v", cacheKey, err) - } - fmt.Printf("Deleted cache for key: %s\n", cacheKey) - return nil -} diff --git a/internal/services/trashcart_service.go b/internal/services/trashcart_service.go deleted file mode 100644 index a231c47..0000000 --- a/internal/services/trashcart_service.go +++ /dev/null @@ -1,154 +0,0 @@ -package services - -// import ( -// "log" -// "time" - -// "rijig/dto" -// "rijig/internal/repositories" -// "rijig/model" - -// "github.com/google/uuid" -// ) - -// type CartService struct { -// Repo repositories.CartRepository -// } - -// func NewCartService(repo repositories.CartRepository) *CartService { -// return &CartService{Repo: repo} -// } - -// func (s *CartService) CommitCartToDatabase(userID string) error { -// items, err := GetCartItems(userID) -// if err != nil || len(items) == 0 { -// log.Printf("No items to commit for user: %s", userID) -// return err -// } - -// var cartItems []model.CartItem -// var totalAmount float32 -// var estimatedTotal float32 - -// for _, item := range items { -// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) -// if err != nil { -// log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) -// continue -// } - -// subTotal := float32(trash.EstimatedPrice) * item.Amount -// totalAmount += item.Amount -// estimatedTotal += subTotal - -// cartItems = append(cartItems, model.CartItem{ -// ID: uuid.NewString(), -// TrashCategoryID: item.TrashCategoryID, -// Amount: item.Amount, -// SubTotalEstimatedPrice: subTotal, -// }) -// } - -// cart := &model.Cart{ -// ID: uuid.NewString(), -// UserID: userID, -// CartItems: cartItems, -// TotalAmount: totalAmount, -// EstimatedTotalPrice: estimatedTotal, -// CreatedAt: time.Now(), -// UpdatedAt: time.Now(), -// } - -// if err := s.Repo.DeleteCartByUserID(userID); err != nil { -// log.Printf("Failed to delete old cart: %v", err) -// } - -// if err := s.Repo.CreateCart(cart); err != nil { -// log.Printf("Failed to create cart: %v", err) -// return err -// } - -// if err := ClearCart(userID); err != nil { -// log.Printf("Failed to clear Redis cart: %v", err) -// } - -// log.Printf("Cart committed successfully for user: %s", userID) -// return nil -// } - -// func (s *CartService) GetCartFromRedis(userID string) (*dto.CartResponse, error) { -// items, err := GetCartItems(userID) -// if err != nil || len(items) == 0 { -// return nil, err -// } - -// var totalAmount float32 -// var estimatedTotal float32 -// var cartItemDTOs []dto.CartItemResponse - -// for _, item := range items { -// trash, err := s.Repo.GetTrashCategoryByID(item.TrashCategoryID) -// if err != nil { -// continue -// } - -// subtotal := float32(trash.EstimatedPrice) * item.Amount -// totalAmount += item.Amount -// estimatedTotal += subtotal - -// cartItemDTOs = append(cartItemDTOs, dto.CartItemResponse{ -// TrashId: trash.ID, -// TrashIcon: trash.Icon, -// TrashName: trash.Name, -// Amount: item.Amount, -// EstimatedSubTotalPrice: subtotal, -// }) -// } - -// resp := &dto.CartResponse{ -// ID: "N/A", -// UserID: userID, -// TotalAmount: totalAmount, -// EstimatedTotalPrice: estimatedTotal, -// CreatedAt: time.Now().Format(time.RFC3339), -// UpdatedAt: time.Now().Format(time.RFC3339), -// CartItems: cartItemDTOs, -// } -// return resp, nil -// } - -// func (s *CartService) GetCart(userID string) (*dto.CartResponse, error) { - -// cartRedis, err := s.GetCartFromRedis(userID) -// if err == nil && len(cartRedis.CartItems) > 0 { -// return cartRedis, nil -// } - -// cartDB, err := s.Repo.GetCartByUserID(userID) -// if err != nil { -// return nil, err -// } - -// var items []dto.CartItemResponse -// for _, item := range cartDB.CartItems { -// items = append(items, dto.CartItemResponse{ -// ItemId: item.ID, -// TrashId: item.TrashCategoryID, -// TrashIcon: item.TrashCategory.Icon, -// TrashName: item.TrashCategory.Name, -// Amount: item.Amount, -// EstimatedSubTotalPrice: item.SubTotalEstimatedPrice, -// }) -// } - -// resp := &dto.CartResponse{ -// ID: cartDB.ID, -// UserID: cartDB.UserID, -// TotalAmount: cartDB.TotalAmount, -// EstimatedTotalPrice: cartDB.EstimatedTotalPrice, -// CreatedAt: cartDB.CreatedAt.Format(time.RFC3339), -// UpdatedAt: cartDB.UpdatedAt.Format(time.RFC3339), -// CartItems: items, -// } -// return resp, nil -// } diff --git a/internal/services/user_service.go b/internal/services/user_service.go deleted file mode 100644 index 05c2774..0000000 --- a/internal/services/user_service.go +++ /dev/null @@ -1,231 +0,0 @@ -package services - -import ( - "fmt" - "log" - "mime/multipart" - "os" - "path/filepath" - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type UserService interface { - GetUserByID(userID string) (*dto.UserResponseDTO, error) - GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) - UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) - UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) - UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error -} - -type userService struct { - userRepo repositories.UserProfilRepository -} - -func NewUserService(userRepo repositories.UserProfilRepository) UserService { - return &userService{userRepo: userRepo} -} - -func (s *userService) GetUserByID(userID string) (*dto.UserResponseDTO, error) { - user, err := s.userRepo.FindByID(userID) - if err != nil { - return nil, fmt.Errorf("error retrieving user by ID: %v", err) - } - - userDTO, err := s.formatUserResponse(user) - if err != nil { - return nil, fmt.Errorf("error formatting user response: %v", err) - } - - return userDTO, nil -} - -func (s *userService) GetAllUsers(page, limit int) ([]dto.UserResponseDTO, error) { - users, err := s.userRepo.FindAll(page, limit) - if err != nil { - return nil, fmt.Errorf("error retrieving all users: %v", err) - } - - var userDTOs []dto.UserResponseDTO - for _, user := range users { - userDTO, err := s.formatUserResponse(&user) - if err != nil { - log.Printf("Error formatting user response for userID %s: %v", user.ID, err) - continue - } - userDTOs = append(userDTOs, *userDTO) - } - - return userDTOs, nil -} - -func (s *userService) UpdateUser(userID string, request *dto.RequestUserDTO) (*dto.UserResponseDTO, error) { - - errors, valid := request.Validate() - if !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } - - user, err := s.userRepo.FindByID(userID) - if err != nil { - return nil, fmt.Errorf("user not found: %v", err) - } - - user.Name = request.Name - user.Phone = request.Phone - user.Email = request.Email - - err = s.userRepo.Update(user) - if err != nil { - return nil, fmt.Errorf("error updating user: %v", err) - } - - userDTO, err := s.formatUserResponse(user) - if err != nil { - return nil, fmt.Errorf("error formatting updated user response: %v", err) - } - - return userDTO, nil -} - -func (s *userService) UpdateUserAvatar(userID string, avatar *multipart.FileHeader) (*dto.UserResponseDTO, error) { - - user, err := s.userRepo.FindByID(userID) - if err != nil { - return nil, fmt.Errorf("user not found: %v", err) - } - - if *user.Avatar != "" { - err := s.deleteAvatarImage(*user.Avatar) - if err != nil { - return nil, fmt.Errorf("failed to delete old image: %v", err) - } - } - - avatarURL, err := s.saveAvatarImage(userID, avatar) - if err != nil { - return nil, fmt.Errorf("failed to save avatar image: %v", err) - } - - err = s.userRepo.UpdateAvatar(userID, avatarURL) - if err != nil { - return nil, fmt.Errorf("failed to update avatar in the database: %v", err) - } - - userDTO, err := s.formatUserResponse(user) - if err != nil { - return nil, fmt.Errorf("failed to format user response: %v", err) - } - - return userDTO, nil -} - -func (s *userService) UpdateUserPassword(userID, oldPassword, newPassword, confirmNewPassword string) error { - - // errors, valid := utils.ValidatePasswordUpdate(oldPassword, newPassword, confirmNewPassword) - // if !valid { - // return fmt.Errorf("password validation error: %v", errors) - // } - - user, err := s.userRepo.FindByID(userID) - if err != nil { - return fmt.Errorf("user not found: %v", err) - } - - if user.Password != oldPassword { - return fmt.Errorf("old password is incorrect") - } - - err = s.userRepo.UpdatePassword(userID, newPassword) - if err != nil { - return fmt.Errorf("error updating password: %v", err) - } - - return nil -} - -func (s *userService) formatUserResponse(user *model.User) (*dto.UserResponseDTO, error) { - - createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) - updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) - - userDTO := &dto.UserResponseDTO{ - ID: user.ID, - Username: user.Name, - Avatar: user.Avatar, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - EmailVerified: user.PhoneVerified, - RoleName: user.Role.RoleName, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - } - - return userDTO, nil -} - -func (s *userService) saveAvatarImage(userID string, avatar *multipart.FileHeader) (string, error) { - - pathImage := "/uploads/avatars/" - avatarDir := "./public" + os.Getenv("BASE_URL") + pathImage - - if _, err := os.Stat(avatarDir); os.IsNotExist(err) { - if err := os.MkdirAll(avatarDir, os.ModePerm); err != nil { - return "", fmt.Errorf("failed to create directory for avatar: %v", err) - } - } - - allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} - extension := filepath.Ext(avatar.Filename) - if !allowedExtensions[extension] { - return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") - } - - avatarFileName := fmt.Sprintf("%s_avatar%s", userID, extension) - avatarPath := filepath.Join(avatarDir, avatarFileName) - - src, err := avatar.Open() - if err != nil { - return "", fmt.Errorf("failed to open uploaded file: %v", err) - } - defer src.Close() - - dst, err := os.Create(avatarPath) - if err != nil { - return "", fmt.Errorf("failed to create avatar file: %v", err) - } - defer dst.Close() - - if _, err := dst.ReadFrom(src); err != nil { - return "", fmt.Errorf("failed to save avatar: %v", err) - } - - avatarURL := fmt.Sprintf("%s%s", pathImage, avatarFileName) - - return avatarURL, nil -} - -func (s *userService) deleteAvatarImage(avatarPath string) error { - - if avatarPath == "" { - return nil - } - - baseDir := "./public/" + os.Getenv("BASE_URL") - absolutePath := baseDir + avatarPath - - if _, err := os.Stat(absolutePath); os.IsNotExist(err) { - return fmt.Errorf("image file not found: %v", err) - } - - err := os.Remove(absolutePath) - if err != nil { - return fmt.Errorf("failed to delete avatar image: %v", err) - } - - log.Printf("Avatar image deleted successfully: %s", absolutePath) - return nil -} diff --git a/internal/services/userpin_service.go b/internal/services/userpin_service.go deleted file mode 100644 index 9a2ba45..0000000 --- a/internal/services/userpin_service.go +++ /dev/null @@ -1,133 +0,0 @@ -package services - -import ( - "fmt" - "time" - - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" - - "golang.org/x/crypto/bcrypt" -) - -type UserPinService interface { - CreateUserPin(userID, pin string) (string, error) - VerifyUserPin(userID, pin string) (string, error) - CheckPinStatus(userID string) (string, error) - UpdateUserPin(userID, oldPin, newPin string) (string, error) -} - -type userPinService struct { - UserPinRepo repositories.UserPinRepository -} - -func NewUserPinService(userPinRepo repositories.UserPinRepository) UserPinService { - return &userPinService{UserPinRepo: userPinRepo} -} - -func (s *userPinService) VerifyUserPin(userID, pin string) (string, error) { - if pin == "" { - return "", fmt.Errorf("pin tidak boleh kosong") - } - - userPin, err := s.UserPinRepo.FindByUserID(userID) - if err != nil { - return "", fmt.Errorf("error fetching user pin: %v", err) - } - if userPin == nil { - return "", fmt.Errorf("user pin not found") - } - - err = bcrypt.CompareHashAndPassword([]byte(userPin.Pin), []byte(pin)) - if err != nil { - return "", fmt.Errorf("incorrect pin") - } - - return "Pin yang anda masukkan benar", nil -} - -func (s *userPinService) CheckPinStatus(userID string) (string, error) { - userPin, err := s.UserPinRepo.FindByUserID(userID) - if err != nil { - return "", fmt.Errorf("error checking pin status: %v", err) - } - if userPin == nil { - return "Pin not created", nil - } - - return "Pin already created", nil -} - -func (s *userPinService) CreateUserPin(userID, pin string) (string, error) { - - existingPin, err := s.UserPinRepo.FindByUserID(userID) - if err != nil { - return "", fmt.Errorf("error checking existing pin: %v", err) - } - - if existingPin != nil { - return "", fmt.Errorf("you have already created a pin, you don't need to create another one") - } - - hashedPin, err := bcrypt.GenerateFromPassword([]byte(pin), bcrypt.DefaultCost) - if err != nil { - return "", fmt.Errorf("error hashing the pin: %v", err) - } - - newPin := model.UserPin{ - UserID: userID, - Pin: string(hashedPin), - } - - err = s.UserPinRepo.Create(&newPin) - if err != nil { - return "", fmt.Errorf("error creating user pin: %v", err) - } - - cacheKey := fmt.Sprintf("userpin:%s", userID) - cacheData := map[string]interface{}{"data": newPin} - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching new user pin to Redis: %v\n", err) - } - - return "Pin berhasil dibuat", nil -} - -func (s *userPinService) UpdateUserPin(userID, oldPin, newPin string) (string, error) { - - userPin, err := s.UserPinRepo.FindByUserID(userID) - if err != nil { - return "", fmt.Errorf("error fetching user pin: %v", err) - } - - if userPin == nil { - return "", fmt.Errorf("user pin not found") - } - - err = bcrypt.CompareHashAndPassword([]byte(userPin.Pin), []byte(oldPin)) - if err != nil { - return "", fmt.Errorf("incorrect old pin") - } - - hashedPin, err := bcrypt.GenerateFromPassword([]byte(newPin), bcrypt.DefaultCost) - if err != nil { - return "", fmt.Errorf("error hashing the new pin: %v", err) - } - - userPin.Pin = string(hashedPin) - err = s.UserPinRepo.Update(userPin) - if err != nil { - return "", fmt.Errorf("error updating user pin: %v", err) - } - - cacheKey := fmt.Sprintf("userpin:%s", userID) - cacheData := map[string]interface{}{"data": userPin} - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching updated user pin to Redis: %v\n", err) - } - - return "Pin berhasil diperbarui", nil -} diff --git a/internal/services/wilayah_indonesia_service.go b/internal/services/wilayah_indonesia_service.go deleted file mode 100644 index 519522a..0000000 --- a/internal/services/wilayah_indonesia_service.go +++ /dev/null @@ -1,494 +0,0 @@ -package services - -import ( - "encoding/json" - "fmt" - "strconv" - "time" - - "rijig/dto" - "rijig/internal/repositories" - "rijig/model" - "rijig/utils" -) - -type WilayahIndonesiaService interface { - ImportDataFromCSV() error - - GetAllProvinces(page, limit int) ([]dto.ProvinceResponseDTO, int, error) - GetProvinceByID(id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) - - GetAllRegencies(page, limit int) ([]dto.RegencyResponseDTO, int, error) - GetRegencyByID(id string, page, limit int) (*dto.RegencyResponseDTO, int, error) - - GetAllDistricts(page, limit int) ([]dto.DistrictResponseDTO, int, error) - GetDistrictByID(id string, page, limit int) (*dto.DistrictResponseDTO, int, error) - - GetAllVillages(page, limit int) ([]dto.VillageResponseDTO, int, error) - GetVillageByID(id string) (*dto.VillageResponseDTO, error) -} - -type wilayahIndonesiaService struct { - WilayahRepo repositories.WilayahIndonesiaRepository -} - -func NewWilayahIndonesiaService(wilayahRepo repositories.WilayahIndonesiaRepository) WilayahIndonesiaService { - return &wilayahIndonesiaService{WilayahRepo: wilayahRepo} -} - -func (s *wilayahIndonesiaService) ImportDataFromCSV() error { - - provinces, err := utils.ReadCSV("public/document/provinces.csv") - if err != nil { - return fmt.Errorf("failed to read provinces CSV: %v", err) - } - - var provinceList []model.Province - for _, record := range provinces[1:] { - province := model.Province{ - ID: record[0], - Name: record[1], - } - provinceList = append(provinceList, province) - } - - if err := s.WilayahRepo.ImportProvinces(provinceList); err != nil { - return fmt.Errorf("failed to import provinces: %v", err) - } - - regencies, err := utils.ReadCSV("public/document/regencies.csv") - if err != nil { - return fmt.Errorf("failed to read regencies CSV: %v", err) - } - - var regencyList []model.Regency - for _, record := range regencies[1:] { - regency := model.Regency{ - ID: record[0], - ProvinceID: record[1], - Name: record[2], - } - regencyList = append(regencyList, regency) - } - - if err := s.WilayahRepo.ImportRegencies(regencyList); err != nil { - return fmt.Errorf("failed to import regencies: %v", err) - } - - districts, err := utils.ReadCSV("public/document/districts.csv") - if err != nil { - return fmt.Errorf("failed to read districts CSV: %v", err) - } - - var districtList []model.District - for _, record := range districts[1:] { - district := model.District{ - ID: record[0], - RegencyID: record[1], - Name: record[2], - } - districtList = append(districtList, district) - } - - if err := s.WilayahRepo.ImportDistricts(districtList); err != nil { - return fmt.Errorf("failed to import districts: %v", err) - } - - villages, err := utils.ReadCSV("public/document/villages.csv") - if err != nil { - return fmt.Errorf("failed to read villages CSV: %v", err) - } - - var villageList []model.Village - for _, record := range villages[1:] { - village := model.Village{ - ID: record[0], - DistrictID: record[1], - Name: record[2], - } - villageList = append(villageList, village) - } - - if err := s.WilayahRepo.ImportVillages(villageList); err != nil { - return fmt.Errorf("failed to import villages: %v", err) - } - - return nil -} - -func (s *wilayahIndonesiaService) GetAllProvinces(page, limit int) ([]dto.ProvinceResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("provinces_page:%d_limit:%d", page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var provinces []dto.ProvinceResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - province, ok := item.(map[string]interface{}) - if ok { - provinces = append(provinces, dto.ProvinceResponseDTO{ - ID: province["id"].(string), - Name: province["name"].(string), - }) - } - } - total := int(cachedData["total"].(float64)) - return provinces, total, nil - } - } - - provinces, total, err := s.WilayahRepo.FindAllProvinces(page, limit) - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch provinces: %v", err) - } - - var provinceDTOs []dto.ProvinceResponseDTO - for _, province := range provinces { - provinceDTOs = append(provinceDTOs, dto.ProvinceResponseDTO{ - ID: province.ID, - Name: province.Name, - }) - } - - cacheData := map[string]interface{}{ - "data": provinceDTOs, - "total": total, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching provinces data: %v\n", err) - } - - return provinceDTOs, total, nil -} - -func (s *wilayahIndonesiaService) GetProvinceByID(id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("province:%s_page:%d_limit:%d", id, page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - - var provinceDTO dto.ProvinceResponseDTO - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), &provinceDTO); err == nil { - - totalRegencies, _ := strconv.Atoi(cachedData["total_regencies"].(string)) - return &provinceDTO, totalRegencies, nil - } - } - } - - province, totalRegencies, err := s.WilayahRepo.FindProvinceByID(id, page, limit) - if err != nil { - return nil, 0, err - } - - provinceDTO := dto.ProvinceResponseDTO{ - ID: province.ID, - Name: province.Name, - } - - var regencyDTOs []dto.RegencyResponseDTO - for _, regency := range province.Regencies { - regencyDTO := dto.RegencyResponseDTO{ - ID: regency.ID, - ProvinceID: regency.ProvinceID, - Name: regency.Name, - } - regencyDTOs = append(regencyDTOs, regencyDTO) - } - - provinceDTO.Regencies = regencyDTOs - - cacheData := map[string]interface{}{ - "data": provinceDTO, - "total_regencies": strconv.Itoa(totalRegencies), - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching province data: %v\n", err) - } - - return &provinceDTO, totalRegencies, nil -} - -func (s *wilayahIndonesiaService) GetAllRegencies(page, limit int) ([]dto.RegencyResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("regencies_page:%d_limit:%d", page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var regencies []dto.RegencyResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - regency, ok := item.(map[string]interface{}) - if ok { - regencies = append(regencies, dto.RegencyResponseDTO{ - ID: regency["id"].(string), - ProvinceID: regency["province_id"].(string), - Name: regency["name"].(string), - }) - } - } - total := int(cachedData["total"].(float64)) - return regencies, total, nil - } - } - - regencies, total, err := s.WilayahRepo.FindAllRegencies(page, limit) - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch provinces: %v", err) - } - - var regencyDTOs []dto.RegencyResponseDTO - for _, regency := range regencies { - regencyDTOs = append(regencyDTOs, dto.RegencyResponseDTO{ - ID: regency.ID, - ProvinceID: regency.ProvinceID, - Name: regency.Name, - }) - } - - cacheData := map[string]interface{}{ - "data": regencyDTOs, - "total": total, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching regencies data: %v\n", err) - } - - return regencyDTOs, total, nil -} - -func (s *wilayahIndonesiaService) GetRegencyByID(id string, page, limit int) (*dto.RegencyResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("regency:%s_page:%d_limit:%d", id, page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - - var regencyDTO dto.RegencyResponseDTO - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), ®encyDTO); err == nil { - - totalDistrict, _ := strconv.Atoi(cachedData["total_regencies"].(string)) - return ®encyDTO, totalDistrict, nil - } - } - } - - regency, totalDistrict, err := s.WilayahRepo.FindRegencyByID(id, page, limit) - if err != nil { - return nil, 0, err - } - - regencyDTO := dto.RegencyResponseDTO{ - ID: regency.ID, - ProvinceID: regency.ProvinceID, - Name: regency.Name, - } - - var districtDTOs []dto.DistrictResponseDTO - for _, regency := range regency.Districts { - districtDTO := dto.DistrictResponseDTO{ - ID: regency.ID, - RegencyID: regency.RegencyID, - Name: regency.Name, - } - districtDTOs = append(districtDTOs, districtDTO) - } - - regencyDTO.Districts = districtDTOs - - cacheData := map[string]interface{}{ - "data": regencyDTO, - "total_regencies": strconv.Itoa(totalDistrict), - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching province data: %v\n", err) - } - - return ®encyDTO, totalDistrict, nil -} - -func (s *wilayahIndonesiaService) GetAllDistricts(page, limit int) ([]dto.DistrictResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("district_page:%d_limit:%d", page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var districts []dto.DistrictResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - district, ok := item.(map[string]interface{}) - if ok { - districts = append(districts, dto.DistrictResponseDTO{ - ID: district["id"].(string), - RegencyID: district["regency_id"].(string), - Name: district["name"].(string), - }) - } - } - total := int(cachedData["total"].(float64)) - return districts, total, nil - } - } - - districts, total, err := s.WilayahRepo.FindAllDistricts(page, limit) - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch districts: %v", err) - } - - var districtsDTOs []dto.DistrictResponseDTO - for _, district := range districts { - districtsDTOs = append(districtsDTOs, dto.DistrictResponseDTO{ - ID: district.ID, - RegencyID: district.RegencyID, - Name: district.Name, - }) - } - - cacheData := map[string]interface{}{ - "data": districtsDTOs, - "total": total, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching districts data: %v\n", err) - } - - return districtsDTOs, total, nil -} - -func (s *wilayahIndonesiaService) GetDistrictByID(id string, page, limit int) (*dto.DistrictResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("district:%s_page:%d_limit:%d", id, page, limit) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - - var districtDTO dto.DistrictResponseDTO - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), &districtDTO); err == nil { - - totalVillage, _ := strconv.Atoi(cachedData["total_village"].(string)) - return &districtDTO, totalVillage, nil - } - } - } - - district, totalVillages, err := s.WilayahRepo.FindDistrictByID(id, page, limit) - if err != nil { - return nil, 0, err - } - - districtDTO := dto.DistrictResponseDTO{ - ID: district.ID, - RegencyID: district.RegencyID, - Name: district.Name, - } - - var villageDTOs []dto.VillageResponseDTO - for _, village := range district.Villages { - regencyDTO := dto.VillageResponseDTO{ - ID: village.ID, - DistrictID: village.DistrictID, - Name: village.Name, - } - villageDTOs = append(villageDTOs, regencyDTO) - } - - districtDTO.Villages = villageDTOs - - cacheData := map[string]interface{}{ - "data": districtDTO, - "total_villages": strconv.Itoa(totalVillages), - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching province data: %v\n", err) - } - - return &districtDTO, totalVillages, nil -} - -func (s *wilayahIndonesiaService) GetAllVillages(page, limit int) ([]dto.VillageResponseDTO, int, error) { - - cacheKey := fmt.Sprintf("villages_page:%d_limit:%d", page, limit) - - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - var villages []dto.VillageResponseDTO - if data, ok := cachedData["data"].([]interface{}); ok { - for _, item := range data { - villageData, ok := item.(map[string]interface{}) - if ok { - villages = append(villages, dto.VillageResponseDTO{ - ID: villageData["id"].(string), - DistrictID: villageData["district_id"].(string), - Name: villageData["name"].(string), - }) - } - } - return villages, int(cachedData["total"].(float64)), nil - } - } - - villages, total, err := s.WilayahRepo.FindAllVillages(page, limit) - if err != nil { - return nil, 0, fmt.Errorf("failed to fetch villages: %v", err) - } - - var villageDTOs []dto.VillageResponseDTO - for _, village := range villages { - villageDTOs = append(villageDTOs, dto.VillageResponseDTO{ - ID: village.ID, - DistrictID: village.DistrictID, - Name: village.Name, - }) - } - - cacheData := map[string]interface{}{ - "data": villageDTOs, - "total": total, - } - err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24) - if err != nil { - fmt.Printf("Error caching villages data to Redis: %v\n", err) - } - - return villageDTOs, total, nil -} - -func (s *wilayahIndonesiaService) GetVillageByID(id string) (*dto.VillageResponseDTO, error) { - - cacheKey := fmt.Sprintf("village:%s", id) - cachedData, err := utils.GetJSONData(cacheKey) - if err == nil && cachedData != nil { - villageResponse := &dto.VillageResponseDTO{} - if data, ok := cachedData["data"].(string); ok { - if err := json.Unmarshal([]byte(data), villageResponse); err == nil { - return villageResponse, nil - } - } - } - - village, err := s.WilayahRepo.FindVillageByID(id) - if err != nil { - return nil, fmt.Errorf("village not found: %v", err) - } - - villageResponse := &dto.VillageResponseDTO{ - ID: village.ID, - DistrictID: village.DistrictID, - Name: village.Name, - } - - cacheData := map[string]interface{}{ - "data": villageResponse, - } - err = utils.SetJSONData(cacheKey, cacheData, 24*time.Hour) - if err != nil { - fmt.Printf("Error caching village data to Redis: %v\n", err) - } - - return villageResponse, nil -} diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go index 5c292da..e6bd367 100644 --- a/internal/worker/cart_worker.go +++ b/internal/worker/cart_worker.go @@ -1,5 +1,5 @@ package worker - +/* import ( "context" "encoding/json" @@ -156,3 +156,4 @@ func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData return w.cartRepo.CreateCartWithItems(ctx, newCart) } + */ \ No newline at end of file diff --git a/presentation/about_route.go b/presentation/about_route.go deleted file mode 100644 index 78871ed..0000000 --- a/presentation/about_route.go +++ /dev/null @@ -1,35 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/about" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func AboutRouter(api fiber.Router) { - aboutRepo := repositories.NewAboutRepository(config.DB) - aboutService := services.NewAboutService(aboutRepo) - aboutHandler := about.NewAboutHandler(aboutService) - - aboutRoutes := api.Group("/about") - aboutRoutes.Use(middleware.AuthMiddleware()) - - aboutRoutes.Get("/", aboutHandler.GetAllAbout) - aboutRoutes.Get("/:id", aboutHandler.GetAboutByID) - aboutRoutes.Post("/", aboutHandler.CreateAbout) // admin - aboutRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAbout) - aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout) // admin - - aboutDetailRoutes := api.Group("/about-detail") - aboutDetailRoutes.Use(middleware.AuthMiddleware()) - aboutDetailRoute := api.Group("/about-detail") - aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById) - aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail) // admin - aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail) - aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail) -} diff --git a/presentation/address_route.go b/presentation/address_route.go deleted file mode 100644 index 6b563a8..0000000 --- a/presentation/address_route.go +++ /dev/null @@ -1,26 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func AddressRouter(api fiber.Router) { - addressRepo := repositories.NewAddressRepository(config.DB) - wilayahRepo := repositories.NewWilayahIndonesiaRepository(config.DB) - addressService := services.NewAddressService(addressRepo, wilayahRepo) - addressHandler := handler.NewAddressHandler(addressService) - - adddressAPI := api.Group("/user/address") - - adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress) - adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID) - adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID) - adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress) - adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware(), addressHandler.DeleteAddress) -} diff --git a/presentation/article_route.go b/presentation/article_route.go deleted file mode 100644 index 1a741f2..0000000 --- a/presentation/article_route.go +++ /dev/null @@ -1,26 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func ArticleRouter(api fiber.Router) { - articleRepo := repositories.NewArticleRepository(config.DB) - articleService := services.NewArticleService(articleRepo) - articleHandler := handler.NewArticleHandler(articleService) - - articleAPI := api.Group("/article-rijik") - - articleAPI.Post("/create-article", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.CreateArticle) - articleAPI.Get("/view-article", articleHandler.GetAllArticles) - articleAPI.Get("/view-article/:article_id", articleHandler.GetArticleByID) - articleAPI.Put("/update-article/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.UpdateArticle) - articleAPI.Delete("/delete-article/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.DeleteArticle) -} diff --git a/presentation/auth/auth_admin_route.go b/presentation/auth/auth_admin_route.go deleted file mode 100644 index 3d20de9..0000000 --- a/presentation/auth/auth_admin_route.go +++ /dev/null @@ -1,36 +0,0 @@ -package presentation -/* -import ( - "log" - "os" - "rijig/config" - handler "rijig/internal/handler/auth" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - services "rijig/internal/services/auth" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func AuthAdminRouter(api fiber.Router) { - secretKey := os.Getenv("SECRET_KEY") - if secretKey == "" { - log.Fatal("SECRET_KEY is not set in the environment variables") - os.Exit(1) - } - - adminAuthRepo := repository.NewAuthAdminRepository(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - - adminAuthService := services.NewAuthAdminService(adminAuthRepo, roleRepo, secretKey) - - adminAuthHandler := handler.NewAuthAdminHandler(adminAuthService) - - adminAuthAPI := api.Group("/admin-auth") - - adminAuthAPI.Post("/register", adminAuthHandler.RegisterAdmin) - adminAuthAPI.Post("/login", adminAuthHandler.LoginAdmin) - adminAuthAPI.Post("/logout", middleware.AuthMiddleware, adminAuthHandler.LogoutAdmin) -} - */ \ No newline at end of file diff --git a/presentation/auth/auth_masyarakat_route.go b/presentation/auth/auth_masyarakat_route.go deleted file mode 100644 index c5f8af7..0000000 --- a/presentation/auth/auth_masyarakat_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation -/* -import ( - "rijig/config" - handler "rijig/internal/handler/auth" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - services "rijig/internal/services/auth" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func AuthMasyarakatRouter(api fiber.Router) { - authMasyarakatRepo := repository.NewAuthMasyarakatRepositories(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - authMasyarakatService := services.NewAuthMasyarakatService(authMasyarakatRepo, roleRepo) - - authHandler := handler.NewAuthMasyarakatHandler(authMasyarakatService) - - authMasyarakat := api.Group("/authmasyarakat") - - authMasyarakat.Post("/auth", authHandler.RegisterOrLoginHandler) - authMasyarakat.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) - authMasyarakat.Post("/verify-otp", authHandler.VerifyOTPHandler) -} - */ \ No newline at end of file diff --git a/presentation/auth/auth_pengelola_route.go b/presentation/auth/auth_pengelola_route.go deleted file mode 100644 index efcd9e1..0000000 --- a/presentation/auth/auth_pengelola_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation -/* -import ( - "rijig/config" - handler "rijig/internal/handler/auth" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - services "rijig/internal/services/auth" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func AuthPengelolaRouter(api fiber.Router) { - authPengelolaRepo := repository.NewAuthPengelolaRepositories(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - authPengelolaService := services.NewAuthPengelolaService(authPengelolaRepo, roleRepo) - - authHandler := handler.NewAuthPengelolaHandler(authPengelolaService) - - authPengelola := api.Group("/authpengelola") - - authPengelola.Post("/auth", authHandler.RegisterOrLoginHandler) - authPengelola.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) - authPengelola.Post("/verify-otp", authHandler.VerifyOTPHandler) -} - */ \ No newline at end of file diff --git a/presentation/auth/auth_pengepul_route.go b/presentation/auth/auth_pengepul_route.go deleted file mode 100644 index d796da4..0000000 --- a/presentation/auth/auth_pengepul_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation -/* -import ( - "rijig/config" - handler "rijig/internal/handler/auth" - "rijig/internal/repositories" - repository "rijig/internal/repositories/auth" - services "rijig/internal/services/auth" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func AuthPengepulRouter(api fiber.Router) { - authPengepulRepo := repository.NewAuthPengepulRepositories(config.DB) - roleRepo := repositories.NewRoleRepository(config.DB) - authPengepulService := services.NewAuthPengepulService(authPengepulRepo, roleRepo) - - authHandler := handler.NewAuthPengepulHandler(authPengepulService) - - authPengepul := api.Group("/authpengepul") - - authPengepul.Post("/auth", authHandler.RegisterOrLoginHandler) - authPengepul.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) - authPengepul.Post("/verify-otp", authHandler.VerifyOTPHandler) -} - */ \ No newline at end of file diff --git a/presentation/auth_route.go b/presentation/auth_route.go deleted file mode 100644 index ee733c1..0000000 --- a/presentation/auth_route.go +++ /dev/null @@ -1,23 +0,0 @@ -package presentation - -// import ( -// "rijig/config" -// "rijig/internal/handler" -// "rijig/internal/repositories" -// "rijig/internal/services" -// "rijig/middleware" - -// "github.com/gofiber/fiber/v2" -// ) - -// func AuthRouter(api fiber.Router) { -// userRepo := repositories.NewUserRepository(config.DB) -// roleRepo := repositories.NewRoleRepository(config.DB) -// authService := services.NewAuthService(userRepo, roleRepo) - -// authHandler := handler.NewAuthHandler(authService) - -// api.Post("/auth", authHandler.RegisterOrLoginHandler) -// api.Post("/logout", middleware.AuthMiddleware, authHandler.LogoutHandler) -// api.Post("/verify-otp", authHandler.VerifyOTPHandler) -// } diff --git a/presentation/cart_router.go b/presentation/cart_router.go deleted file mode 100644 index 05d66fb..0000000 --- a/presentation/cart_router.go +++ /dev/null @@ -1,28 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func TrashCartRouter(api fiber.Router) { - repo := repositories.NewCartRepository() - trashRepo := repositories.NewTrashRepository(config.DB) - cartService := services.NewCartService(repo, trashRepo) - cartHandler := handler.NewCartHandler(cartService) - - cart := api.Group("/cart") - cart.Use(middleware.AuthMiddleware()) - - cart.Get("/", cartHandler.GetCart) - cart.Post("/item", cartHandler.AddOrUpdateItem) - cart.Delete("/item/:trash_id", cartHandler.DeleteItem) - cart.Delete("/clear", cartHandler.ClearCart) -} - -// cart.Post("/items", cartHandler.AddMultipleCartItems) diff --git a/presentation/collector_route.go b/presentation/collector_route.go deleted file mode 100644 index a5f9b17..0000000 --- a/presentation/collector_route.go +++ /dev/null @@ -1,42 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func CollectorRouter(api fiber.Router) { - cartRepo := repositories.NewCartRepository() - // trashRepo repositories.TrashRepository - - pickupRepo := repositories.NewRequestPickupRepository() - trashRepo := repositories.NewTrashRepository(config.DB) - historyRepo := repositories.NewPickupStatusHistoryRepository() - cartService := services.NewCartService(cartRepo, trashRepo) - - pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) - pickupHandler := handler.NewRequestPickupHandler(pickupService) - collectorRepo := repositories.NewCollectorRepository() - - collectorService := services.NewCollectorService(collectorRepo, trashRepo) - collectorHandler := handler.NewCollectorHandler(collectorService) - - collectors := api.Group("/collectors") - collectors.Use(middleware.AuthMiddleware()) - - collectors.Post("/", collectorHandler.CreateCollector) - collectors.Post("/:id/trash", collectorHandler.AddTrashToCollector) - collectors.Get("/:id", collectorHandler.GetCollectorByID) - collectors.Get("/", collectorHandler.GetCollectorByUserID) - collectors.Get("/pickup/assigned-to-me", pickupHandler.GetAssignedPickup) - - collectors.Patch("/:id", collectorHandler.UpdateCollector) - collectors.Patch("/:id/trash", collectorHandler.UpdateTrash) - collectors.Patch("/:id/job-status", collectorHandler.UpdateJobStatus) - collectors.Delete("/trash/:id", collectorHandler.DeleteTrash) -} diff --git a/presentation/company_profile_route.go b/presentation/company_profile_route.go deleted file mode 100644 index 709ab0b..0000000 --- a/presentation/company_profile_route.go +++ /dev/null @@ -1,27 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func CompanyProfileRouter(api fiber.Router) { - - companyProfileRepo := repositories.NewCompanyProfileRepository(config.DB) - companyProfileService := services.NewCompanyProfileService(companyProfileRepo) - companyProfileHandler := handler.NewCompanyProfileHandler(companyProfileService) - - companyProfileAPI := api.Group("/company-profile") - companyProfileAPI.Use(middleware.AuthMiddleware()) - - companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile) - companyProfileAPI.Get("/get/:company_id", companyProfileHandler.GetCompanyProfileByID) - companyProfileAPI.Get("/get", companyProfileHandler.GetCompanyProfilesByUserID) - companyProfileAPI.Put("/update/:company_id", companyProfileHandler.UpdateCompanyProfile) - companyProfileAPI.Delete("/delete/:company_id", companyProfileHandler.DeleteCompanyProfile) -} diff --git a/presentation/coveragearea_route.go b/presentation/coveragearea_route.go deleted file mode 100644 index a1603ed..0000000 --- a/presentation/coveragearea_route.go +++ /dev/null @@ -1,25 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - - "github.com/gofiber/fiber/v2" -) - -func CoverageAreaRouter(api fiber.Router) { - coverageAreaRepo := repositories.NewCoverageAreaRepository(config.DB) - wilayahRepo := repositories.NewWilayahIndonesiaRepository(config.DB) - coverageAreaService := services.NewCoverageAreaService(coverageAreaRepo, wilayahRepo) - coverageAreaHandler := handler.NewCoverageAreaHandler(coverageAreaService) - - coverage := api.Group("/coveragearea") - - coverage.Post("/", coverageAreaHandler.CreateCoverageArea) - coverage.Get("/", coverageAreaHandler.GetAllCoverageAreas) - coverage.Get("/:id", coverageAreaHandler.GetCoverageAreaByID) - coverage.Put("/:id", coverageAreaHandler.UpdateCoverageArea) - coverage.Delete("/:id", coverageAreaHandler.DeleteCoverageArea) -} diff --git a/presentation/identitycard_route.go b/presentation/identitycard_route.go deleted file mode 100644 index 39301b2..0000000 --- a/presentation/identitycard_route.go +++ /dev/null @@ -1,29 +0,0 @@ -package presentation -/* -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func IdentityCardRouter(api fiber.Router) { - identityCardRepo := repositories.NewIdentityCardRepository(config.DB) - userRepo := repositories.NewUserProfilRepository(config.DB) - identityCardService := services.NewIdentityCardService(identityCardRepo, userRepo) - identityCardHandler := handler.NewIdentityCardHandler(identityCardService) - - identityCardApi := api.Group("/identitycard") - identityCardApi.Use(middleware.AuthMiddleware) - - identityCardApi.Post("/create", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.CreateIdentityCard) - identityCardApi.Get("/get/:identity_id", identityCardHandler.GetIdentityCardById) - identityCardApi.Get("/get", identityCardHandler.GetIdentityCard) - identityCardApi.Put("/update/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.UpdateIdentityCard) - identityCardApi.Delete("/delete/:identity_id", middleware.RoleMiddleware(utils.RolePengelola, utils.RolePengepul), identityCardHandler.DeleteIdentityCard) -} - */ \ No newline at end of file diff --git a/presentation/pickup_matching_route.go b/presentation/pickup_matching_route.go deleted file mode 100644 index a389640..0000000 --- a/presentation/pickup_matching_route.go +++ /dev/null @@ -1,25 +0,0 @@ -package presentation - -import ( - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func PickupMatchingRouter(api fiber.Router) { - pickupRepo := repositories.NewRequestPickupRepository() - collectorRepo := repositories.NewCollectorRepository() - service := services.NewPickupMatchingService(pickupRepo, collectorRepo) - handler := handler.NewPickupMatchingHandler(service) - - manual := api.Group("/pickup/manual") - manual.Use(middleware.AuthMiddleware()) - manual.Get("/:pickupID/nearby-collectors", handler.GetNearbyCollectorsForPickup) - - auto := api.Group("/pickup/otomatis") - auto.Use(middleware.AuthMiddleware()) - auto.Get("/available-requests", handler.GetAvailablePickupForCollector) -} diff --git a/presentation/rating_route.go b/presentation/rating_route.go deleted file mode 100644 index 6d801aa..0000000 --- a/presentation/rating_route.go +++ /dev/null @@ -1,24 +0,0 @@ -package presentation - -import ( - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func PickupRatingRouter(api fiber.Router) { - ratingRepo := repositories.NewPickupRatingRepository() - ratingService := services.NewPickupRatingService(ratingRepo) - ratingHandler := handler.NewPickupRatingHandler(ratingService) - - rating := api.Group("/pickup") - rating.Use(middleware.AuthMiddleware()) - rating.Post("/:id/rating", ratingHandler.CreateRating) - - collector := api.Group("/collector") - collector.Get("/:id/ratings", ratingHandler.GetRatingsByCollector) - collector.Get("/:id/ratings/average", ratingHandler.GetAverageRating) -} diff --git a/presentation/request_pickup_route.go b/presentation/request_pickup_route.go deleted file mode 100644 index 81c6681..0000000 --- a/presentation/request_pickup_route.go +++ /dev/null @@ -1,35 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func RequestPickupRouter(api fiber.Router) { - cartRepo := repositories.NewCartRepository() - pickupRepo := repositories.NewRequestPickupRepository() - historyRepo := repositories.NewPickupStatusHistoryRepository() - trashRepo := repositories.NewTrashRepository(config.DB) - - cartService := services.NewCartService(cartRepo, trashRepo) - historyService := services.NewPickupStatusHistoryService(historyRepo) - - pickupService := services.NewRequestPickupService(trashRepo, pickupRepo, cartService, historyRepo) - pickupHandler := handler.NewRequestPickupHandler(pickupService) - statuspickupHandler := handler.NewPickupStatusHistoryHandler(historyService) - - reqpickup := api.Group("/reqpickup") - reqpickup.Use(middleware.AuthMiddleware()) - - reqpickup.Post("/manual", pickupHandler.CreateRequestPickup) - reqpickup.Get("/pickup/:id/history", statuspickupHandler.GetStatusHistory) - reqpickup.Post("/otomatis", pickupHandler.CreateRequestPickup) - reqpickup.Put("/:id/select-collector", pickupHandler.SelectCollector) - reqpickup.Put("/pickup/:id/status", pickupHandler.UpdatePickupStatus) - reqpickup.Put("/pickup/:id/item/update-actual", pickupHandler.UpdatePickupItemActualAmount) -} diff --git a/presentation/role_route.go b/presentation/role_route.go deleted file mode 100644 index 6ec230a..0000000 --- a/presentation/role_route.go +++ /dev/null @@ -1,19 +0,0 @@ -package presentation - -// import ( -// "rijig/config" -// "rijig/internal/handler" -// "rijig/internal/repositories" -// "rijig/internal/services" - -// "github.com/gofiber/fiber/v2" -// ) - -// func RoleRouter(api fiber.Router) { -// roleRepo := repositories.NewRoleRepository(config.DB) -// roleService := services.NewRoleService(roleRepo) -// roleHandler := handler.NewRoleHandler(roleService) - -// api.Get("/roles", roleHandler.GetRoles) -// api.Get("/role/:role_id", roleHandler.GetRoleByID) -// } diff --git a/presentation/trash_route.go b/presentation/trash_route.go deleted file mode 100644 index bef7b83..0000000 --- a/presentation/trash_route.go +++ /dev/null @@ -1,33 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func TrashRouter(api fiber.Router) { - trashRepo := repositories.NewTrashRepository(config.DB) - trashService := services.NewTrashService(trashRepo) - trashHandler := handler.NewTrashHandler(trashService) - - trashAPI := api.Group("/trash") - trashAPI.Use(middleware.AuthMiddleware()) - - trashAPI.Post("/category", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.CreateCategory) - trashAPI.Post("/category/detail", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.AddDetailToCategory) - trashAPI.Get("/categories", trashHandler.GetCategories) - trashAPI.Get("/category/:category_id", trashHandler.GetCategoryByID) - trashAPI.Get("/detail/:detail_id", trashHandler.GetTrashDetailByID) - - trashAPI.Patch("/category/:category_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.UpdateCategory) - trashAPI.Put("/detail/:detail_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.UpdateDetail) - - trashAPI.Delete("/category/:category_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.DeleteCategory) - trashAPI.Delete("/detail/:detail_id", middleware.RequireRoles(utils.RoleAdministrator), trashHandler.DeleteDetail) -} diff --git a/presentation/user_route.go b/presentation/user_route.go deleted file mode 100644 index c008ace..0000000 --- a/presentation/user_route.go +++ /dev/null @@ -1,29 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - - "github.com/gofiber/fiber/v2" -) - -func UserProfileRouter(api fiber.Router) { - userProfileRepo := repositories.NewUserProfilRepository(config.DB) - userProfileService := services.NewUserService(userProfileRepo) - userProfileHandler := handler.NewUserHandler(userProfileService) - - userProfilRoute := api.Group("/user") - - userProfilRoute.Get("/info", middleware.AuthMiddleware(), userProfileHandler.GetUserByIDHandler) - - userProfilRoute.Get("/show-all", middleware.AuthMiddleware(), userProfileHandler.GetAllUsersHandler) - // userProfilRoute.Get("/:userid", middleware.AuthMiddleware, userProfileHandler.GetUserProfileById) - // userProfilRoute.Get("/:roleid", middleware.AuthMiddleware, userProfileHandler.GetUsersByRoleID) - - userProfilRoute.Put("/update-user", middleware.AuthMiddleware(), userProfileHandler.UpdateUserHandler) - userProfilRoute.Patch("/update-user-password", middleware.AuthMiddleware(), userProfileHandler.UpdateUserPasswordHandler) - userProfilRoute.Patch("/upload-photoprofile", middleware.AuthMiddleware(), userProfileHandler.UpdateUserAvatarHandler) -} diff --git a/presentation/userpin_route.go b/presentation/userpin_route.go deleted file mode 100644 index 9d6dbbb..0000000 --- a/presentation/userpin_route.go +++ /dev/null @@ -1,24 +0,0 @@ -package presentation - -import ( - /* "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" */ - - "github.com/gofiber/fiber/v2" -) - -func UserPinRouter(api fiber.Router) { - // userPinRepo := repositories.NewUserPinRepository(config.DB) - - // userPinService := services.NewUserPinService(userPinRepo) - - // userPinHandler := handler.NewUserPinHandler(userPinService) - - // api.Post("/set-pin", middleware.AuthMiddleware, userPinHandler.CreateUserPin) - // api.Post("/verif-pin", middleware.AuthMiddleware, userPinHandler.VerifyUserPin) - // api.Get("/cek-pin-status", middleware.AuthMiddleware, userPinHandler.CheckPinStatus) - // api.Patch("/update-pin", middleware.AuthMiddleware, userPinHandler.UpdateUserPin) -} diff --git a/presentation/wilayahindonesia_route.go b/presentation/wilayahindonesia_route.go deleted file mode 100644 index 13bfa44..0000000 --- a/presentation/wilayahindonesia_route.go +++ /dev/null @@ -1,36 +0,0 @@ -package presentation - -import ( - "rijig/config" - "rijig/internal/handler" - "rijig/internal/repositories" - "rijig/internal/services" - "rijig/middleware" - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func WilayahRouter(api fiber.Router) { - - wilayahRepo := repositories.NewWilayahIndonesiaRepository(config.DB) - wilayahService := services.NewWilayahIndonesiaService(wilayahRepo) - wilayahHandler := handler.NewWilayahImportHandler(wilayahService) - - api.Post("/import/data-wilayah-indonesia", middleware.RequireRoles(utils.RoleAdministrator), wilayahHandler.ImportWilayahData) - - wilayahAPI := api.Group("/wilayah-indonesia") - - wilayahAPI.Get("/provinces", wilayahHandler.GetProvinces) - wilayahAPI.Get("/provinces/:provinceid", wilayahHandler.GetProvinceByID) - - wilayahAPI.Get("/regencies", wilayahHandler.GetAllRegencies) - wilayahAPI.Get("/regencies/:regencyid", wilayahHandler.GetRegencyByID) - - wilayahAPI.Get("/districts", wilayahHandler.GetAllDistricts) - wilayahAPI.Get("/districts/:districtid", wilayahHandler.GetDistrictByID) - - wilayahAPI.Get("/villages", wilayahHandler.GetAllVillages) - wilayahAPI.Get("/villages/:villageid", wilayahHandler.GetVillageByID) - -} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index cdcb51f..d06911a 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,9 +11,8 @@ import ( "rijig/internal/userpin" "rijig/internal/whatsapp" "rijig/middleware" - "rijig/presentation" + // "rijig/presentation" - // presentationn "rijig/presentation/auth" "github.com/gofiber/fiber/v2" ) @@ -41,22 +40,22 @@ func SetupRoutes(app *fiber.App) { // presentationn.AuthMasyarakatRouter(api) // || auth router || // // presentation.IdentityCardRouter(api) - presentation.CompanyProfileRouter(api) - presentation.RequestPickupRouter(api) - presentation.PickupMatchingRouter(api) - presentation.PickupRatingRouter(api) + // presentation.CompanyProfileRouter(api) + // presentation.RequestPickupRouter(api) + // presentation.PickupMatchingRouter(api) + // presentation.PickupRatingRouter(api) - presentation.CollectorRouter(api) - presentation.TrashCartRouter(api) + // presentation.CollectorRouter(api) + // presentation.TrashCartRouter(api) - presentation.UserProfileRouter(api) - presentation.UserPinRouter(api) - // presentation.RoleRouter(api) - presentation.WilayahRouter(api) - presentation.AddressRouter(api) - // presentation.ArticleRouter(api) - presentation.AboutRouter(api) - presentation.TrashRouter(api) - presentation.CoverageAreaRouter(api) + // presentation.UserProfileRouter(api) + // presentation.UserPinRouter(api) + // // presentation.RoleRouter(api) + // presentation.WilayahRouter(api) + // presentation.AddressRouter(api) + // // presentation.ArticleRouter(api) + // // presentation.AboutRouter(api) + // presentation.TrashRouter(api) + // presentation.CoverageAreaRouter(api) whatsapp.WhatsAppRouter(api) } From d7633d3c7f6440fd754ffc98f7bec09abe0389ee Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Mon, 9 Jun 2025 06:19:59 +0700 Subject: [PATCH 43/48] fix: fixing command redis-cli and psql in makefile --- Makefile | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 6179595..44d98ca 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,8 @@ help: @echo " logs - Show logs for all services" @echo " clean - Remove all containers and volumes" @echo " prod - Start production environment" + @echo " psql-prod - Execute psql in production postgres" + @echo " redis-prod - Execute redis-cli in production redis" @echo "" @echo "$(YELLOW)Development (dengan Air hot reload):$(NC)" @echo " dev-build - Build development images" @@ -27,14 +29,14 @@ help: @echo " dev-logs - Show development logs" @echo " dev-clean - Clean development environment" @echo " dev-restart- Restart development environment" + @echo " psql - Execute psql in development postgres" + @echo " redis-cli - Execute redis-cli in development redis" @echo "" @echo "$(YELLOW)Utilities:$(NC)" @echo " app-logs - Show only app logs" @echo " db-logs - Show only database logs" @echo " status - Check service status" @echo " shell - Execute bash in app container" - @echo " psql - Execute psql in postgres container" - @echo " redis-cli - Execute redis-cli in redis container" # Production Commands build: @@ -67,6 +69,15 @@ prod: @echo "$(GREEN)Starting production environment...$(NC)" docker compose up -d +# Production utilities +psql-prod: + @echo "$(GREEN)Connecting to production PostgreSQL...$(NC)" + docker compose exec postgres psql -U postgres -d apirijig_v2 + +redis-prod: + @echo "$(GREEN)Connecting to production Redis...$(NC)" + docker compose exec redis redis-cli + # Development Commands (dengan Air hot reload) dev-build: @echo "$(GREEN)Building development images dengan Air...$(NC)" @@ -101,7 +112,7 @@ dev-restart: @echo "$(YELLOW)Restarting development services...$(NC)" docker compose -f docker-compose.dev.yml restart -# Development utilities +# Development utilities (FIXED - menggunakan -f docker-compose.dev.yml) dev-app-logs: @echo "$(GREEN)Showing development app logs...$(NC)" docker compose -f docker-compose.dev.yml logs -f app @@ -118,7 +129,16 @@ dev-status: @echo "$(GREEN)Development service status:$(NC)" docker compose -f docker-compose.dev.yml ps -# Shared utilities +# FIXED: Development database access (default untuk development) +psql: + @echo "$(GREEN)Connecting to development PostgreSQL...$(NC)" + docker compose -f docker-compose.dev.yml exec postgres psql -U postgres -d apirijig_v2 + +redis-cli: + @echo "$(GREEN)Connecting to development Redis...$(NC)" + docker compose -f docker-compose.dev.yml exec redis redis-cli + +# Shared utilities (default ke production) app-logs: docker compose logs -f app @@ -131,12 +151,6 @@ status: shell: docker compose exec app sh -psql: - docker compose exec postgres psql -U postgres -d apirijig_v2 - -redis-cli: - docker compose exec redis redis-cli - # Rebuild and restart app only app-rebuild: docker compose build app From e06b6033b5efcd46fbe0808277805b2e8f769040 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Tue, 10 Jun 2025 02:34:10 +0700 Subject: [PATCH 44/48] refact&fix: fixing whatsmeow and fixing air and docker setup --- Dockerfile.dev | 45 +- Makefile | 284 ++++++------ config/setup_config.go | 3 - config/whatsapp.go | 411 +++++------------- docker-compose.dev.yml | 41 +- go.mod | 10 +- go.sum | 2 + .../authentication/authentication_handler.go | 5 - .../authentication/authentication_service.go | 77 ++-- internal/whatsapp/scanner.html | 184 ++++++++ internal/whatsapp/success_scan.html | 411 ++++++++++++++++++ internal/whatsapp/whatsapp_handler.go | 162 ++++++- internal/whatsapp/whatsapp_route.go | 6 +- model/user_model.go | 32 +- router/setup_routes.go.go | 3 +- 15 files changed, 1102 insertions(+), 574 deletions(-) create mode 100644 internal/whatsapp/scanner.html create mode 100644 internal/whatsapp/success_scan.html diff --git a/Dockerfile.dev b/Dockerfile.dev index c08cd9d..3c66d10 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,28 +1,49 @@ -# Dockerfile untuk development environment dengan Air hot reload -FROM golang:1.23-alpine +# Multi-stage Dockerfile untuk development dengan Air hot reload +FROM golang:1.23-alpine AS base -# Install dependencies dan Air -RUN apk add --no-cache git ca-certificates curl && \ - go install github.com/cosmtrek/air@latest +# Install dependencies yang diperlukan +RUN apk add --no-cache \ + git \ + ca-certificates \ + curl \ + tzdata \ + make \ + gcc \ + musl-dev + +# Install Air untuk hot reload dengan versi yang stabil +RUN go install github.com/cosmtrek/air@v1.49.0 + +# Set timezone ke Asia/Jakarta +ENV TZ=Asia/Jakarta +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Set working directory WORKDIR /app -# Copy go mod files dan download dependencies +# Create user untuk security (non-root) +RUN addgroup -g 1001 -S golang && \ + adduser -S golang -u 1001 -G golang + +# Copy go.mod dan go.sum terlebih dahulu untuk better caching COPY go.mod go.sum ./ -RUN go mod download + +# Download dependencies +RUN go mod download && go mod verify # Copy source code COPY . . -# Create tmp directory untuk Air -RUN mkdir -p tmp +# Create tmp directory dengan permissions yang tepat +RUN mkdir -p tmp && \ + chown -R golang:golang /app && \ + chmod -R 755 /app -# Set timezone (optional) -RUN cp /usr/share/zoneinfo/Asia/Jakarta /etc/localtime +# Switch to non-root user +USER golang # Expose port EXPOSE 7000 -# Run Air untuk hot reload +# Command untuk menjalankan Air CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/Makefile b/Makefile index 44d98ca..9c7d8d3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Makefile untuk mengelola Docker commands +# Makefile untuk mengelola Docker commands - Optimized Version .PHONY: help build up down restart logs clean dev prod dev-build dev-up dev-down dev-logs @@ -6,162 +6,146 @@ GREEN := \033[0;32m YELLOW := \033[1;33m RED := \033[0;31m +BLUE := \033[0;34m +PURPLE := \033[0;35m +CYAN := \033[0;36m NC := \033[0m # No Color +# Project variables +PROJECT_NAME := rijig_backend +DEV_COMPOSE_FILE := docker-compose.dev.yml + # Default target help: - @echo "$(GREEN)Available commands:$(NC)" - @echo "$(YELLOW)Production:$(NC)" - @echo " build - Build all Docker images" - @echo " up - Start all services" - @echo " down - Stop all services" - @echo " restart - Restart all services" - @echo " logs - Show logs for all services" - @echo " clean - Remove all containers and volumes" - @echo " prod - Start production environment" - @echo " psql-prod - Execute psql in production postgres" - @echo " redis-prod - Execute redis-cli in production redis" + @echo "$(GREEN)🚀 $(PROJECT_NAME) - Available Commands:$(NC)" @echo "" - @echo "$(YELLOW)Development (dengan Air hot reload):$(NC)" - @echo " dev-build - Build development images" - @echo " dev-up - Start development environment dengan hot reload" - @echo " dev-down - Stop development environment" - @echo " dev-logs - Show development logs" - @echo " dev-clean - Clean development environment" - @echo " dev-restart- Restart development environment" - @echo " psql - Execute psql in development postgres" - @echo " redis-cli - Execute redis-cli in development redis" + @echo "$(YELLOW)📦 Development Commands (Hot Reload):$(NC)" + @echo " $(CYAN)dev$(NC) - Complete development setup (build + up)" + @echo " $(CYAN)dev-build$(NC) - Build development images" + @echo " $(CYAN)dev-up$(NC) - Start development environment" + @echo " $(CYAN)dev-down$(NC) - Stop development environment" + @echo " $(CYAN)dev-restart$(NC) - Restart development services" + @echo " $(CYAN)dev-logs$(NC) - Show development logs (all services)" + @echo " $(CYAN)dev-clean$(NC) - Clean development environment" @echo "" - @echo "$(YELLOW)Utilities:$(NC)" - @echo " app-logs - Show only app logs" - @echo " db-logs - Show only database logs" - @echo " status - Check service status" - @echo " shell - Execute bash in app container" - -# Production Commands -build: - @echo "$(GREEN)Building production images...$(NC)" - docker compose build --no-cache - -up: - @echo "$(GREEN)Starting production services...$(NC)" - docker compose up -d - -down: - @echo "$(RED)Stopping production services...$(NC)" - docker compose down - -restart: - @echo "$(YELLOW)Restarting production services...$(NC)" - docker compose restart - -logs: - @echo "$(GREEN)Showing production logs...$(NC)" - docker compose logs -f - -clean: - @echo "$(RED)Cleaning production environment...$(NC)" - docker compose down -v --remove-orphans - docker system prune -f - docker volume prune -f - -prod: - @echo "$(GREEN)Starting production environment...$(NC)" - docker compose up -d - -# Production utilities -psql-prod: - @echo "$(GREEN)Connecting to production PostgreSQL...$(NC)" - docker compose exec postgres psql -U postgres -d apirijig_v2 - -redis-prod: - @echo "$(GREEN)Connecting to production Redis...$(NC)" - docker compose exec redis redis-cli - -# Development Commands (dengan Air hot reload) -dev-build: - @echo "$(GREEN)Building development images dengan Air...$(NC)" - docker compose -f docker-compose.dev.yml build --no-cache - -dev-up: - @echo "$(GREEN)Starting development environment dengan Air hot reload...$(NC)" - docker compose -f docker-compose.dev.yml up -d - @echo "$(GREEN)Development services started!$(NC)" - @echo "$(YELLOW)API Server: http://localhost:7000$(NC)" - @echo "$(YELLOW)PostgreSQL: localhost:5433$(NC)" - @echo "$(YELLOW)Redis: localhost:6378$(NC)" - @echo "$(YELLOW)pgAdmin: http://localhost:8080 (admin@rijig.com / admin123)$(NC)" - @echo "$(YELLOW)Redis Commander: http://localhost:8081$(NC)" + @echo "$(YELLOW)🛠️ Development Utilities:$(NC)" + @echo " $(CYAN)dev-app-logs$(NC) - Show only app logs" + @echo " $(CYAN)dev-db-logs$(NC) - Show only database logs" + @echo " $(CYAN)dev-shell$(NC) - Access app container shell" + @echo " $(CYAN)dev-status$(NC) - Check development services status" + @echo " $(CYAN)psql$(NC) - Connect to development PostgreSQL" + @echo " $(CYAN)redis-cli$(NC) - Connect to development Redis" @echo "" - @echo "$(GREEN)✨ Hot reload is active! Edit your Go files and see changes automatically ✨$(NC)" + @echo "$(YELLOW)🧹 Maintenance:$(NC)" + @echo " $(RED)clean-all$(NC) - Clean everything (containers, volumes, images)" + @echo " $(RED)system-prune$(NC) - Clean Docker system" + @echo " $(CYAN)stats$(NC) - Show container resource usage" -dev-down: - @echo "$(RED)Stopping development services...$(NC)" - docker compose -f docker-compose.dev.yml down - -dev-logs: - @echo "$(GREEN)Showing development logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f - -dev-clean: - @echo "$(RED)Cleaning development environment...$(NC)" - docker compose -f docker-compose.dev.yml down -v --remove-orphans - docker system prune -f - -dev-restart: - @echo "$(YELLOW)Restarting development services...$(NC)" - docker compose -f docker-compose.dev.yml restart - -# Development utilities (FIXED - menggunakan -f docker-compose.dev.yml) -dev-app-logs: - @echo "$(GREEN)Showing development app logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f app - -dev-db-logs: - @echo "$(GREEN)Showing development database logs...$(NC)" - docker compose -f docker-compose.dev.yml logs -f postgres - -dev-shell: - @echo "$(GREEN)Accessing development app container...$(NC)" - docker compose -f docker-compose.dev.yml exec app sh - -dev-status: - @echo "$(GREEN)Development service status:$(NC)" - docker compose -f docker-compose.dev.yml ps - -# FIXED: Development database access (default untuk development) -psql: - @echo "$(GREEN)Connecting to development PostgreSQL...$(NC)" - docker compose -f docker-compose.dev.yml exec postgres psql -U postgres -d apirijig_v2 - -redis-cli: - @echo "$(GREEN)Connecting to development Redis...$(NC)" - docker compose -f docker-compose.dev.yml exec redis redis-cli - -# Shared utilities (default ke production) -app-logs: - docker compose logs -f app - -db-logs: - docker compose logs -f postgres - -status: - docker compose ps - -shell: - docker compose exec app sh - -# Rebuild and restart app only -app-rebuild: - docker compose build app - docker compose up -d app - -# View real-time resource usage -stats: - docker stats +# ====================== +# DEVELOPMENT COMMANDS +# ====================== # Quick development setup (recommended) -dev: - @echo "$(GREEN)Setting up complete development environment...$(NC)" - make dev-build - make dev-up \ No newline at end of file +dev: dev-build dev-up + @echo "$(GREEN)✨ Development environment ready!$(NC)" + @echo "$(BLUE)🌐 Services:$(NC)" + @echo " • API Server: $(CYAN)http://localhost:7000$(NC)" + @echo " • PostgreSQL: $(CYAN)localhost:5433$(NC)" + @echo " • Redis: $(CYAN)localhost:6378$(NC)" + @echo " • pgAdmin: $(CYAN)http://localhost:8080$(NC) (admin@rijig.com / admin123)" + @echo " • Redis Commander: $(CYAN)http://localhost:8081$(NC)" + @echo "" + @echo "$(GREEN)🔥 Hot reload is active! Edit your Go files and see changes automatically$(NC)" + +dev-build: + @echo "$(YELLOW)🔨 Building development images...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) build --no-cache + @echo "$(GREEN)✅ Development images built successfully!$(NC)" + +dev-up: + @echo "$(YELLOW)🚀 Starting development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) up -d + @echo "$(GREEN)✅ Development services started!$(NC)" + @make dev-status + +dev-down: + @echo "$(RED)🛑 Stopping development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down + @echo "$(GREEN)✅ Development services stopped!$(NC)" + +dev-restart: + @echo "$(YELLOW)🔄 Restarting development services...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) restart + @echo "$(GREEN)✅ Development services restarted!$(NC)" + +dev-logs: + @echo "$(CYAN)📋 Showing development logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=100 + +dev-clean: + @echo "$(RED)🧹 Cleaning development environment...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans + @echo "$(GREEN)✅ Development environment cleaned!$(NC)" + +# ====================== +# DEVELOPMENT UTILITIES +# ====================== + +dev-app-logs: + @echo "$(CYAN)📋 Showing app logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 app + +dev-db-logs: + @echo "$(CYAN)📋 Showing database logs (Ctrl+C to exit)...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 postgres + +dev-shell: + @echo "$(CYAN)🐚 Accessing app container shell...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec app sh + +dev-status: + @echo "$(BLUE)📊 Development services status:$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) ps + +psql: + @echo "$(CYAN)🐘 Connecting to development PostgreSQL...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec postgres psql -U postgres -d apirijig_v2 + +redis-cli: + @echo "$(CYAN)⚡ Connecting to development Redis...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) exec redis redis-cli + +# ====================== +# MAINTENANCE COMMANDS +# ====================== + +clean-all: + @echo "$(RED)🧹 Performing complete cleanup...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans 2>/dev/null || true + @echo "$(YELLOW)🗑️ Removing unused containers, networks, and images...$(NC)" + @docker system prune -a -f --volumes + @echo "$(GREEN)✅ Complete cleanup finished!$(NC)" + +system-prune: + @echo "$(YELLOW)🗑️ Cleaning Docker system...$(NC)" + @docker system prune -f + @echo "$(GREEN)✅ Docker system cleaned!$(NC)" + +stats: + @echo "$(BLUE)📈 Container resource usage:$(NC)" + @docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" + +# ====================== +# QUICK COMMANDS +# ====================== + +# App only restart (faster for development) +app-restart: + @echo "$(YELLOW)🔄 Restarting app container only...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) restart app + @echo "$(GREEN)✅ App container restarted!$(NC)" + +# Check if containers are healthy +health-check: + @echo "$(BLUE)🏥 Checking container health...$(NC)" + @docker compose -f $(DEV_COMPOSE_FILE) ps --format "table {{.Name}}\t{{.Status}}" \ No newline at end of file diff --git a/config/setup_config.go b/config/setup_config.go index 0702601..fc7b531 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -8,12 +8,9 @@ import ( ) func SetupConfig() { - if _, exists := os.LookupEnv("DOCKER_ENV"); exists { - log.Println("Running in Docker container, using environment variables") } else { - err := godotenv.Load(".env.dev") if err != nil { log.Printf("Warning: Error loading .env file: %v", err) diff --git a/config/whatsapp.go b/config/whatsapp.go index aef3559..9f1372d 100644 --- a/config/whatsapp.go +++ b/config/whatsapp.go @@ -2,16 +2,16 @@ package config import ( "context" + "encoding/base64" "fmt" "log" "os" "os/signal" - "sync" "syscall" - "time" _ "github.com/lib/pq" - "github.com/mdp/qrterminal/v3" + + "github.com/skip2/go-qrcode" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/store/sqlstore" @@ -21,355 +21,156 @@ import ( "google.golang.org/protobuf/proto" ) -type WhatsAppManager struct { - Client *whatsmeow.Client - container *sqlstore.Container - isConnected bool - mu sync.RWMutex - ctx context.Context - cancel context.CancelFunc - shutdownCh chan struct{} +type WhatsAppService struct { + Client *whatsmeow.Client + Container *sqlstore.Container } -var ( - waManager *WhatsAppManager - once sync.Once -) - -func GetWhatsAppManager() *WhatsAppManager { - once.Do(func() { - ctx, cancel := context.WithCancel(context.Background()) - waManager = &WhatsAppManager{ - ctx: ctx, - cancel: cancel, - shutdownCh: make(chan struct{}), - } - }) - return waManager -} +var whatsappService *WhatsAppService func InitWhatsApp() { - manager := GetWhatsAppManager() + var err error - log.Println("Initializing WhatsApp client...") - - if err := manager.setupDatabase(); err != nil { - log.Fatalf("Failed to setup WhatsApp database: %v", err) - } - - if err := manager.setupClient(); err != nil { - log.Fatalf("Failed to setup WhatsApp client: %v", err) - } - - if err := manager.handleAuthentication(); err != nil { - log.Fatalf("Failed to authenticate WhatsApp: %v", err) - } - - manager.setupEventHandlers() - - go manager.handleShutdown() - - log.Println("WhatsApp client initialized successfully and ready to send messages!") -} - -func (w *WhatsAppManager) setupDatabase() error { - dbLog := waLog.Stdout("WhatsApp-DB", "ERROR", true) - - dsn := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s?sslmode=disable", + connectionString := fmt.Sprintf( + "user=%s password=%s dbname=%s host=%s port=%s sslmode=disable", os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), + os.Getenv("DB_NAME"), os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), - os.Getenv("DB_NAME"), ) - var err error - w.container, err = sqlstore.New("postgres", dsn, dbLog) + dbLog := waLog.Stdout("Database", "DEBUG", true) + container, err := sqlstore.New("postgres", connectionString, dbLog) if err != nil { - return fmt.Errorf("failed to connect to database: %v", err) + log.Fatalf("Failed to connect to WhatsApp database: %v", err) } - log.Println("WhatsApp database connection established") - return nil + whatsappService = &WhatsAppService{ + Container: container, + } } -func (w *WhatsAppManager) setupClient() error { - deviceStore, err := w.container.GetFirstDevice() - if err != nil { - return fmt.Errorf("failed to get device store: %v", err) - } - - clientLog := waLog.Stdout("WhatsApp-Client", "ERROR", true) - w.Client = whatsmeow.NewClient(deviceStore, clientLog) - - return nil +func GetWhatsAppService() *WhatsAppService { + return whatsappService } -func (w *WhatsAppManager) handleAuthentication() error { - if w.Client.Store.ID == nil { - log.Println("WhatsApp client not logged in, generating QR code...") - return w.authenticateWithQR() +func eventHandler(evt interface{}) { + switch v := evt.(type) { + case *events.Message: + fmt.Println("Received a message!", v.Message.GetConversation()) + case *events.Connected: + fmt.Println("WhatsApp client connected!") + case *events.Disconnected: + fmt.Println("WhatsApp client disconnected!") } - - log.Println("WhatsApp client already logged in, connecting...") - return w.connect() } -func (w *WhatsAppManager) authenticateWithQR() error { - qrChan, err := w.Client.GetQRChannel(w.ctx) +func (wa *WhatsAppService) GenerateQR() (string, error) { + if wa.Container == nil { + return "", fmt.Errorf("container is not initialized") + } + + deviceStore, err := wa.Container.GetFirstDevice() if err != nil { - return fmt.Errorf("failed to get QR channel: %v", err) + return "", fmt.Errorf("failed to get first device: %v", err) } - if err := w.Client.Connect(); err != nil { - return fmt.Errorf("failed to connect client: %v", err) - } + clientLog := waLog.Stdout("Client", "DEBUG", true) + wa.Client = whatsmeow.NewClient(deviceStore, clientLog) + wa.Client.AddEventHandler(eventHandler) - qrTimeout := time.NewTimer(3 * time.Minute) - defer qrTimeout.Stop() + if wa.Client.Store.ID == nil { + fmt.Println("Client is not logged in, generating QR code...") + qrChan, _ := wa.Client.GetQRChannel(context.Background()) + err = wa.Client.Connect() + if err != nil { + return "", fmt.Errorf("failed to connect: %v", err) + } - for { - select { - case evt := <-qrChan: - switch evt.Event { - case "code": - fmt.Println("\n=== QR CODE UNTUK LOGIN WHATSAPP ===") - generateQRCode(evt.Code) - fmt.Println("Scan QR code di atas dengan WhatsApp Anda") - fmt.Println("QR code akan expired dalam 3 menit") - case "success": - log.Println("✅ WhatsApp login successful!") - w.setConnected(true) - return nil - case "timeout": - return fmt.Errorf("QR code expired, please restart") - default: - log.Printf("Login status: %s", evt.Event) + for evt := range qrChan { + if evt.Event == "code" { + fmt.Println("QR code generated:", evt.Code) + png, err := qrcode.Encode(evt.Code, qrcode.Medium, 256) + if err != nil { + return "", fmt.Errorf("failed to create QR code: %v", err) + } + dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png) + return dataURI, nil + } else { + fmt.Println("Login event:", evt.Event) + if evt.Event == "success" { + return "success", nil + } } - case <-qrTimeout.C: - return fmt.Errorf("QR code authentication timeout after 3 minutes") - case <-w.ctx.Done(): - return fmt.Errorf("authentication cancelled") } - } -} - -func (w *WhatsAppManager) connect() error { - if err := w.Client.Connect(); err != nil { - return fmt.Errorf("failed to connect: %v", err) - } - - time.Sleep(2 * time.Second) - w.setConnected(true) - return nil -} - -func (w *WhatsAppManager) setupEventHandlers() { - w.Client.AddEventHandler(func(evt interface{}) { - switch v := evt.(type) { - case *events.Connected: - log.Println("✅ WhatsApp client connected") - w.setConnected(true) - case *events.Disconnected: - log.Println("❌ WhatsApp client disconnected") - w.setConnected(false) - case *events.LoggedOut: - log.Println("🚪 WhatsApp client logged out") - w.setConnected(false) - case *events.Message: - log.Printf("📨 Message received from %s", v.Info.Sender) + } else { + fmt.Println("Client already logged in, connecting...") + err = wa.Client.Connect() + if err != nil { + return "", fmt.Errorf("failed to connect: %v", err) } - }) + return "already_connected", nil + } + + return "", fmt.Errorf("failed to generate QR code") } -func (w *WhatsAppManager) setConnected(status bool) { - w.mu.Lock() - defer w.mu.Unlock() - w.isConnected = status -} - -func (w *WhatsAppManager) IsConnected() bool { - w.mu.RLock() - defer w.mu.RUnlock() - return w.isConnected -} - -func generateQRCode(qrString string) { - qrterminal.GenerateHalfBlock(qrString, qrterminal.M, os.Stdout) -} - -func (w *WhatsAppManager) handleShutdown() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - - select { - case <-sigChan: - log.Println("Received shutdown signal...") - case <-w.ctx.Done(): - log.Println("Context cancelled...") +func (wa *WhatsAppService) SendMessage(phoneNumber, message string) error { + if wa.Client == nil { + return fmt.Errorf("client not initialized") } - w.shutdown() -} - -func (w *WhatsAppManager) shutdown() { - log.Println("Shutting down WhatsApp client...") - - w.cancel() - - if w.Client != nil { - w.Client.Disconnect() - } - - if w.container != nil { - w.container.Close() - } - - close(w.shutdownCh) - log.Println("WhatsApp client shutdown completed") -} - -func SendWhatsAppMessage(phone, message string) error { - manager := GetWhatsAppManager() - - if manager.Client == nil { - return fmt.Errorf("WhatsApp client is not initialized") - } - - if !manager.IsConnected() { - return fmt.Errorf("WhatsApp client is not connected") - } - - if phone == "" || message == "" { - return fmt.Errorf("phone number and message cannot be empty") - } - - if phone[0] == '0' { - phone = "62" + phone[1:] // Convert 08xx menjadi 628xx - } - if phone[:2] != "62" { - phone = "62" + phone // Tambahkan 62 jika belum ada - } - - // Parse JID - targetJID, err := types.ParseJID(phone + "@s.whatsapp.net") + targetJID, err := types.ParseJID(phoneNumber + "@s.whatsapp.net") if err != nil { - return fmt.Errorf("invalid phone number format: %v", err) + return fmt.Errorf("invalid phone number: %v", err) } - // Buat pesan msg := &waE2E.Message{ Conversation: proto.String(message), } - // Kirim dengan timeout - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - resp, err := manager.Client.SendMessage(ctx, targetJID, msg) + _, err = wa.Client.SendMessage(context.Background(), targetJID, msg) if err != nil { return fmt.Errorf("failed to send message: %v", err) } - log.Printf("✅ Message sent to %s (ID: %s)", phone, resp.ID) return nil } -// SendWhatsAppMessageBatch - Kirim pesan ke multiple nomor -func SendWhatsAppMessageBatch(phoneNumbers []string, message string) []error { - var errors []error +func (wa *WhatsAppService) Logout() error { + if wa.Client == nil { + return fmt.Errorf("no active client session") + } - for _, phone := range phoneNumbers { - if err := SendWhatsAppMessage(phone, message); err != nil { - errors = append(errors, fmt.Errorf("failed to send to %s: %v", phone, err)) - continue + err := wa.Client.Logout() + if err != nil { + return fmt.Errorf("failed to logout: %v", err) + } + + wa.Client.Disconnect() + wa.Client = nil + return nil +} + +func (wa *WhatsAppService) IsConnected() bool { + return wa.Client != nil && wa.Client.IsConnected() +} + +func (wa *WhatsAppService) IsLoggedIn() bool { + return wa.Client != nil && wa.Client.Store.ID != nil +} + +func (wa *WhatsAppService) GracefulShutdown() { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + fmt.Println("Shutting down WhatsApp client...") + if wa.Client != nil { + wa.Client.Disconnect() } - - // Delay untuk menghindari rate limit - time.Sleep(1 * time.Second) - } - - return errors -} - -// GetWhatsAppStatus - Cek status koneksi -func GetWhatsAppStatus() map[string]interface{} { - manager := GetWhatsAppManager() - - status := map[string]interface{}{ - "initialized": manager.Client != nil, - "connected": manager.IsConnected(), - "logged_in": false, - "jid": "", - } - - if manager.Client != nil && manager.Client.Store.ID != nil { - status["logged_in"] = true - status["jid"] = manager.Client.Store.ID.String() - } - - return status -} - -// LogoutWhatsApp - Logout dan cleanup -func LogoutWhatsApp() error { - manager := GetWhatsAppManager() - - if manager.Client == nil { - return fmt.Errorf("WhatsApp client is not initialized") - } - - log.Println("Logging out WhatsApp...") - - // Logout - err := manager.Client.Logout() - if err != nil { - log.Printf("Warning: Failed to logout properly: %v", err) - } - - // Disconnect - manager.Client.Disconnect() - manager.setConnected(false) - - // Hapus device dari store - if err := manager.removeDeviceFromStore(); err != nil { - log.Printf("Warning: Failed to remove device: %v", err) - } - - // Close database - if manager.container != nil { - manager.container.Close() - } - - log.Println("✅ WhatsApp logout completed") - return nil -} - -func (w *WhatsAppManager) removeDeviceFromStore() error { - deviceStore, err := w.container.GetFirstDevice() - if err != nil { - return err - } - - if deviceStore != nil && deviceStore.ID != nil { - return deviceStore.Delete() - } - - return nil -} - -// IsValidPhoneNumber - Validasi format nomor telepon Indonesia -func IsValidPhoneNumber(phone string) bool { - // Minimal validasi untuk nomor Indonesia - if len(phone) < 10 || len(phone) > 15 { - return false - } - - // Cek awalan nomor Indonesia - if phone[:2] == "62" || phone[0] == '0' { - return true - } - - return false + os.Exit(0) + }() } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index e152c00..ed90cd2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -51,44 +51,15 @@ services: restart: unless-stopped ports: - "7000:7000" - environment: - # Docker Environment Flag - DOCKER_ENV: "true" - - # Base URL - BASE_URL: /apirijig/v2 - - # Server Settings - SERVER_HOST: 0.0.0.0 - SERVER_PORT: 7000 - - # Database Settings - menggunakan service name sebagai host - DB_HOST: postgres - DB_PORT: 5432 - DB_NAME: apirijig_v2 - DB_USER: postgres - DB_PASSWORD: pahmiadmin - - # Redis Settings - menggunakan service name sebagai host - REDIS_HOST: redis - REDIS_PORT: 6379 - REDIS_PASSWORD: "" - REDIS_DB: 0 - - # Auth Keys - API_KEY: apirijikL0RH64wfkEpPqjAroLVPuFgT0EpsSLBPsmyUvIqZrUAi6X3HNPM7Vter - SECRET_KEY: TJ6h3vPMPlAuv7cbD27RU1/UyRctEih5k4H3+o7tZM1PSwTcoFETL6lqB54= - - # TTL Settings - ACCESS_TOKEN_EXPIRY: 23*time.Hour - REFRESH_TOKEN_EXPIRY: 28*24*time.Hour - PARTIAL_TOKEN_EXPIRY: 2*time.Hour + env_file: + - .env.docker volumes: # Mount source code untuk hot reload - .:/app - # Exclude node_modules dan vendor (jika ada) + # Cache Go modules untuk performance + - go_modules_cache:/go/pkg/mod + # Exclude tmp directory untuk mencegah konflik - /app/tmp - - /app/vendor depends_on: postgres: condition: service_healthy @@ -96,6 +67,7 @@ services: condition: service_healthy networks: - rijig_network_dev + working_dir: /app # pgAdmin (optional - untuk GUI database management) pgadmin: @@ -137,3 +109,4 @@ volumes: postgres_data_dev: redis_data_dev: pgadmin_data_dev: + go_modules_cache: diff --git a/go.mod b/go.mod index 687dde0..36bd859 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( gorm.io/gorm v1.25.12 ) -require ( - golang.org/x/term v0.30.0 // indirect - rsc.io/qr v0.2.0 // indirect -) +// require ( +// golang.org/x/term v0.30.0 // indirect +// rsc.io/qr v0.2.0 // indirect +// ) require ( filippo.io/edwards25519 v1.1.0 // indirect @@ -35,9 +35,9 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mdp/qrterminal/v3 v3.2.0 github.com/rivo/uniseg v0.2.0 // indirect github.com/rs/zerolog v1.33.0 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // direct github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index a238c19..58846b8 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go index cc27327..31f83e4 100644 --- a/internal/authentication/authentication_handler.go +++ b/internal/authentication/authentication_handler.go @@ -210,11 +210,6 @@ func (h *AuthenticationHandler) LogoutAuthentication(c *fiber.Ctx) error { return err } - // deviceID := c.Get("Device-ID") - // if deviceID == "" { - // return utils.BadRequest(c, "Device ID is required") - // } - err = h.service.LogoutAuthentication(c.Context(), claims.UserID, claims.DeviceID) if err != nil { diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index 8a9dff8..3956216 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -33,6 +33,21 @@ func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.R return &authenticationService{authRepo, roleRepo} } +func normalizeRoleName(roleName string) string { + switch strings.ToLower(roleName) { + case "administrator", "admin": + return utils.RoleAdministrator + case "pengelola": + return utils.RolePengelola + case "pengepul": + return utils.RolePengepul + case "masyarakat": + return utils.RoleMasyarakat + default: + return strings.ToLower(roleName) + } +} + func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) { user, err := s.authRepo.FindUserByEmail(ctx, req.Email) if err != nil { @@ -103,14 +118,16 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) { - existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, strings.ToLower(req.RoleName)) + normalizedRole := strings.ToLower(req.RoleName) + + existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole) if err == nil && existingUser != nil { return nil, fmt.Errorf("nomor telepon dengan role %s sudah terdaftar", req.RoleName) } - roleData, err := s.roleRepo.FindRoleByName(ctx, req.RoleName) + roleData, err := s.roleRepo.FindRoleByName(ctx, normalizedRole) if err != nil { - return nil, fmt.Errorf("role tidak valid") + return nil, fmt.Errorf("role tidak valid: %v", err) } rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone) @@ -125,33 +142,34 @@ func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *Lo otpKey := fmt.Sprintf("otp:%s:register", req.Phone) otpData := OTPData{ - Phone: req.Phone, - OTP: otp, - Role: req.RoleName, - RoleID: roleData.ID, - Type: "register", - - Attempts: 0, + Phone: req.Phone, + OTP: otp, + Role: normalizedRole, + RoleID: roleData.ID, + Type: "register", + Attempts: 0, + ExpiresAt: time.Now().Add(90 * time.Second), } - err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute) + err = utils.SetCacheWithTTL(otpKey, otpData, 90*time.Second) if err != nil { return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) } - err = sendOTPViaSMS(req.Phone, otp) + err = sendOTP(req.Phone, otp) if err != nil { return nil, fmt.Errorf("gagal mengirim OTP: %v", err) } return &OTPResponse{ Message: "OTP berhasil dikirim", - ExpiresIn: 60, + ExpiresIn: 90, Phone: maskPhoneNumber(req.Phone), }, nil } func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) { + otpKey := fmt.Sprintf("otp:%s:register", req.Phone) var otpData OTPData err := utils.GetCache(otpKey, &otpData) @@ -166,7 +184,7 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * if otpData.OTP != req.Otp { otpData.Attempts++ - utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt)) + utils.SetCacheWithTTL(otpKey, otpData, time.Until(otpData.ExpiresAt)) return nil, fmt.Errorf("kode OTP salah") } @@ -174,12 +192,14 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("role tidak sesuai") } + normalizedRole := strings.ToLower(req.RoleName) + user := &model.User{ Phone: req.Phone, PhoneVerified: true, RoleID: otpData.RoleID, RegistrationStatus: utils.RegStatusIncomplete, - RegistrationProgress: 0, + RegistrationProgress: utils.ProgressOTPVerified, Name: "", Gender: "", Dateofbirth: "", @@ -191,11 +211,15 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("gagal membuat user: %v", err) } + if user.ID == "" { + return nil, fmt.Errorf("gagal mendapatkan user ID setelah registrasi") + } + utils.DeleteCache(otpKey) tokenResponse, err := utils.GenerateTokenPair( user.ID, - req.RoleName, + normalizedRole, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress), @@ -205,15 +229,18 @@ func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req * return nil, fmt.Errorf("gagal generate token: %v", err) } - nextStep := utils.GetNextRegistrationStep(req.RoleName, int(user.RegistrationProgress),user.RegistrationStatus) + nextStep := utils.GetNextRegistrationStep( + normalizedRole, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) return &AuthResponse{ - Message: "Registrasi berhasil", - AccessToken: tokenResponse.AccessToken, - RefreshToken: tokenResponse.RefreshToken, - TokenType: string(tokenResponse.TokenType), - ExpiresIn: tokenResponse.ExpiresIn, - + Message: "Registrasi berhasil", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, RegistrationStatus: user.RegistrationStatus, NextStep: nextStep, SessionID: tokenResponse.SessionID, @@ -256,7 +283,7 @@ func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRe return nil, fmt.Errorf("gagal menyimpan OTP: %v", err) } - err = sendOTPViaSMS(req.Phone, otp) + err = sendOTP(req.Phone, otp) if err != nil { return nil, fmt.Errorf("gagal mengirim OTP: %v", err) } @@ -349,7 +376,7 @@ func isRateLimited(key string, maxAttempts int, duration time.Duration) bool { return false } -func sendOTPViaSMS(phone, otp string) error { +func sendOTP(phone, otp string) error { fmt.Printf("Sending OTP %s to %s\n", otp, phone) return nil diff --git a/internal/whatsapp/scanner.html b/internal/whatsapp/scanner.html new file mode 100644 index 0000000..160c4e2 --- /dev/null +++ b/internal/whatsapp/scanner.html @@ -0,0 +1,184 @@ + + + + + + WhatsApp QR Scanner + + + +
+ + +

Scan QR code untuk menghubungkan WhatsApp Anda

+ +
+ WhatsApp QR Code +
+ +
+
+

Cara menggunakan:

+
    +
  1. Buka WhatsApp di ponsel Anda
  2. +
  3. Tap Menu atau Settings dan pilih WhatsApp Web
  4. +
  5. Arahkan ponsel Anda ke QR code ini untuk memindainya
  6. +
  7. Tunggu hingga terhubung
  8. +
+
+ +
+ + Menunggu pemindaian QR code... +
+
+
+ + + + \ No newline at end of file diff --git a/internal/whatsapp/success_scan.html b/internal/whatsapp/success_scan.html new file mode 100644 index 0000000..2063fcc --- /dev/null +++ b/internal/whatsapp/success_scan.html @@ -0,0 +1,411 @@ + + + + + + WhatsApp - Berhasil Terhubung + + + +
+ + +
+ +
+ WhatsApp berhasil terhubung dan siap digunakan! +
+ +
+
+ Status Koneksi: +
+ + Checking... +
+
+
+ Status Login: +
+ + Checking... +
+
+
+ +
+ +
+ + + + + +
+
+ + + + \ No newline at end of file diff --git a/internal/whatsapp/whatsapp_handler.go b/internal/whatsapp/whatsapp_handler.go index e086fc3..216c20b 100644 --- a/internal/whatsapp/whatsapp_handler.go +++ b/internal/whatsapp/whatsapp_handler.go @@ -1,24 +1,156 @@ package whatsapp import ( - "log" + "html/template" + "path/filepath" "rijig/config" - "rijig/utils" "github.com/gofiber/fiber/v2" ) -func WhatsAppHandler(c *fiber.Ctx) error { - userID, ok := c.Locals("userID").(string) - if !ok || userID == "" { - return utils.Unauthorized(c, "User is not logged in or invalid session") - } - - err := config.LogoutWhatsApp() - if err != nil { - log.Printf("Error during logout process for user %s: %v", userID, err) - return utils.InternalServerError(c, err.Error()) - } - - return utils.Success(c, "Logged out successfully") +type APIResponse struct { + Meta map[string]interface{} `json:"meta"` + Data interface{} `json:"data,omitempty"` +} + +func WhatsAppQRPageHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + // Jika sudah login, tampilkan halaman success + if wa.IsLoggedIn() { + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + qrDataURI, err := wa.GenerateQR() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Failed to generate QR code: " + err.Error(), + }, + }) + } + + if qrDataURI == "success" { + // Login berhasil, tampilkan halaman success + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + if qrDataURI == "already_connected" { + // Sudah terhubung, tampilkan halaman success + templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load success template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), nil) + } + + // Tampilkan QR code scanner + templatePath := filepath.Join("internal", "whatsapp", "scanner.html") + tmpl, err := template.ParseFiles(templatePath) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "Unable to load scanner template: " + err.Error(), + }, + }) + } + + c.Set("Content-Type", "text/html") + return tmpl.Execute(c.Response().BodyWriter(), template.URL(qrDataURI)) +} + +func WhatsAppLogoutHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + err := wa.Logout() + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": err.Error(), + }, + }) + } + + return c.Status(fiber.StatusOK).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "success", + "message": "Successfully logged out and session deleted", + }, + }) +} + +func WhatsAppStatusHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "error", + "message": "WhatsApp service not initialized", + }, + }) + } + + status := map[string]interface{}{ + "is_connected": wa.IsConnected(), + "is_logged_in": wa.IsLoggedIn(), + } + + return c.Status(fiber.StatusOK).JSON(APIResponse{ + Meta: map[string]interface{}{ + "status": "success", + "message": "WhatsApp status retrieved successfully", + }, + Data: status, + }) } diff --git a/internal/whatsapp/whatsapp_route.go b/internal/whatsapp/whatsapp_route.go index 877bbed..76e2728 100644 --- a/internal/whatsapp/whatsapp_route.go +++ b/internal/whatsapp/whatsapp_route.go @@ -1,11 +1,11 @@ package whatsapp import ( - "rijig/middleware" - "github.com/gofiber/fiber/v2" ) func WhatsAppRouter(api fiber.Router) { - api.Post("/logout/whastapp", middleware.AuthMiddleware(), WhatsAppHandler) + api.Get("/whatsapp-status", WhatsAppStatusHandler) + api.Get("/whatsapp/pw=admin1234", WhatsAppQRPageHandler) + api.Post("/logout/whastapp", WhatsAppLogoutHandler) } diff --git a/model/user_model.go b/model/user_model.go index 7890cb1..faefec9 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -3,20 +3,20 @@ package model import "time" type User struct { - ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4();unique;not null" json:"id"` - Avatar *string `json:"avatar,omitempty"` - Name string `gorm:"not null" json:"name"` - Gender string `gorm:"not null" json:"gender"` - Dateofbirth string `gorm:"not null" json:"dateofbirth"` - Placeofbirth string `gorm:"not null" json:"placeofbirth"` - Phone string `gorm:"not null;index" json:"phone"` - Email string `json:"email,omitempty"` - PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` - Password string `json:"password,omitempty"` - RoleID string `gorm:"not null" json:"roleId"` - Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` - RegistrationStatus string `gorm:"default:uncompleted" json:"registrationstatus"` - RegistrationProgress int8 `json:"registration_progress"` - 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" json:"id"` + Avatar *string `json:"avatar,omitempty"` + Name string `gorm:"not null" json:"name"` + Gender string `gorm:"not null" json:"gender"` + Dateofbirth string `gorm:"not null" json:"dateofbirth"` + Placeofbirth string `gorm:"not null" json:"placeofbirth"` + Phone string `gorm:"not null;index" json:"phone"` + Email string `json:"email,omitempty"` + PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` + Password string `json:"password,omitempty"` + RoleID string `gorm:"not null" json:"roleId"` + Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"` + RegistrationStatus string `json:"registrationstatus"` + RegistrationProgress int8 `json:"registration_progress"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index d06911a..5d37e8a 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -11,14 +11,15 @@ import ( "rijig/internal/userpin" "rijig/internal/whatsapp" "rijig/middleware" - // "rijig/presentation" + // "rijig/presentation" "github.com/gofiber/fiber/v2" ) func SetupRoutes(app *fiber.App) { apa := app.Group(os.Getenv("BASE_URL")) + whatsapp.WhatsAppRouter(apa) apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") api := app.Group(os.Getenv("BASE_URL")) From c26eee0ab989104761caf73a7c1873954b3534f2 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 11 Jun 2025 06:03:08 +0700 Subject: [PATCH 45/48] fix: auth and permission chmod and regist flow --- dto/about_dto.go | 66 ----- dto/address_dto.go | 73 ----- dto/article_dto.go | 48 --- dto/auth/auth_admin_dto.go | 130 --------- dto/auth/auth_masyarakat_dto.go | 1 - dto/auth/auth_pengelola_dto.go | 133 --------- dto/auth/auth_pengepul_dto.go | 1 - dto/auth_dto.go | 42 --- dto/banner_dto.go | 29 -- dto/collector_dto.go | 111 ------- dto/company_profile_dto.go | 62 ---- dto/coveragearea_dto.go | 34 --- dto/identitycard_dto.go | 116 -------- dto/initialcoint_dto.go | 34 --- dto/product_dto.go | 55 ---- dto/rating_dto.go | 25 -- dto/request_pickup_dto.go | 74 ----- dto/requestpickup_dto.go | 88 ------ dto/role_dto.go | 8 - dto/store_dto.go | 68 ----- dto/trash_dto.go | 65 ----- dto/trashcart_dto.go | 49 ---- dto/user_dto.go | 81 ------ dto/userpin_dto.go | 66 ----- dto/wiayah_indonesia_dto.go | 27 -- internal/about/about_handler.go | 9 +- internal/about/about_service.go | 47 ++- internal/address/address_handler.go | 5 +- internal/address/address_service.go | 33 +-- .../authentication_repository.go | 39 +++ .../authentication/authentication_service.go | 19 +- internal/company/company_route.go | 4 +- internal/company/company_service.go | 116 +++++++- internal/identitycart/identitycart_dto.go | 4 +- internal/identitycart/identitycart_handler.go | 169 ++++++++++- internal/identitycart/identitycart_route.go | 21 +- internal/identitycart/identitycart_service.go | 255 ++++++++++++---- internal/userpin/userpin_dto.go | 2 +- internal/userpin/userpin_handler.go | 38 +-- internal/userpin/userpin_route.go | 4 +- internal/userpin/userpin_service.go | 116 +++++--- internal/userprofile/userprofile_handler.go | 77 ++++- internal/userprofile/userprofile_repo.go | 100 ++++++- internal/userprofile/userprofile_route.go | 21 +- internal/userprofile/userprofile_service.go | 161 ++++++++++- internal/wilayahindo/wilayahindo_handler.go | 273 +++++++++++++++++- internal/wilayahindo/wilayahindo_route.go | 33 ++- internal/wilayahindo/wilayahindo_service.go | 99 ++++--- model/identitycard_model.go | 1 + public/document/districts.csv | 0 public/document/provinces.csv | 0 public/document/regencies.csv | 0 public/document/villages.csv | 0 router/setup_routes.go.go | 8 +- 54 files changed, 1385 insertions(+), 1755 deletions(-) delete mode 100644 dto/about_dto.go delete mode 100644 dto/address_dto.go delete mode 100644 dto/article_dto.go delete mode 100644 dto/auth/auth_admin_dto.go delete mode 100644 dto/auth/auth_masyarakat_dto.go delete mode 100644 dto/auth/auth_pengelola_dto.go delete mode 100644 dto/auth/auth_pengepul_dto.go delete mode 100644 dto/auth_dto.go delete mode 100644 dto/banner_dto.go delete mode 100644 dto/collector_dto.go delete mode 100644 dto/company_profile_dto.go delete mode 100644 dto/coveragearea_dto.go delete mode 100644 dto/identitycard_dto.go delete mode 100644 dto/initialcoint_dto.go delete mode 100644 dto/product_dto.go delete mode 100644 dto/rating_dto.go delete mode 100644 dto/request_pickup_dto.go delete mode 100644 dto/requestpickup_dto.go delete mode 100644 dto/role_dto.go delete mode 100644 dto/store_dto.go delete mode 100644 dto/trash_dto.go delete mode 100644 dto/trashcart_dto.go delete mode 100644 dto/user_dto.go delete mode 100644 dto/userpin_dto.go delete mode 100644 dto/wiayah_indonesia_dto.go mode change 100644 => 100755 public/document/districts.csv mode change 100644 => 100755 public/document/provinces.csv mode change 100644 => 100755 public/document/regencies.csv mode change 100644 => 100755 public/document/villages.csv diff --git a/dto/about_dto.go b/dto/about_dto.go deleted file mode 100644 index c4df688..0000000 --- a/dto/about_dto.go +++ /dev/null @@ -1,66 +0,0 @@ -package dto - -import ( - "strings" -) - -type RequestAboutDTO struct { - Title string `json:"title"` - CoverImage string `json:"cover_image"` -} - -func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Title) == "" { - errors["title"] = append(errors["title"], "Title is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type ResponseAboutDTO struct { - ID string `json:"id"` - Title string `json:"title"` - CoverImage string `json:"cover_image"` - AboutDetail *[]ResponseAboutDetailDTO `json:"about_detail"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} - -type RequestAboutDetailDTO struct { - AboutId string `json:"about_id"` - ImageDetail string `json:"image_detail"` - Description string `json:"description"` -} - -func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.AboutId) == "" { - errors["about_id"] = append(errors["about_id"], "about_id is required") - } - - if strings.TrimSpace(r.Description) == "" { - errors["description"] = append(errors["description"], "Description is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type ResponseAboutDetailDTO struct { - ID string `json:"id"` - AboutID string `json:"about_id"` - ImageDetail string `json:"image_detail"` - Description string `json:"description"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` -} diff --git a/dto/address_dto.go b/dto/address_dto.go deleted file mode 100644 index b1bdcd6..0000000 --- a/dto/address_dto.go +++ /dev/null @@ -1,73 +0,0 @@ -package dto - -import "strings" - -type AddressResponseDTO struct { - UserID string `json:"user_id,omitempty"` - ID string `json:"address_id,omitempty"` - Province string `json:"province,omitempty"` - Regency string `json:"regency,omitempty"` - District string `json:"district,omitempty"` - Village string `json:"village,omitempty"` - PostalCode string `json:"postalCode,omitempty"` - Detail string `json:"detail,omitempty"` - Latitude float64 `json:"latitude,omitempty"` - Longitude float64 `json:"longitude,omitempty"` - CreatedAt string `json:"createdAt,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` -} - -type CreateAddressDTO struct { - Province string `json:"province_id"` - Regency string `json:"regency_id"` - District string `json:"district_id"` - Village string `json:"village_id"` - PostalCode string `json:"postalCode"` - Detail string `json:"detail"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` -} - -func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Province) == "" { - errors["province_id"] = append(errors["province_id"], "Province ID is required") - } - - if strings.TrimSpace(r.Regency) == "" { - errors["regency_id"] = append(errors["regency_id"], "Regency ID is required") - } - - if strings.TrimSpace(r.District) == "" { - errors["district_id"] = append(errors["district_id"], "District ID is required") - } - - if strings.TrimSpace(r.Village) == "" { - errors["village_id"] = append(errors["village_id"], "Village ID is required") - } - - if strings.TrimSpace(r.PostalCode) == "" { - errors["postalCode"] = append(errors["postalCode"], "PostalCode is required") - } else if len(r.PostalCode) < 5 { - errors["postalCode"] = append(errors["postalCode"], "PostalCode must be at least 5 characters") - } - - if strings.TrimSpace(r.Detail) == "" { - errors["detail"] = append(errors["detail"], "Detail address is required") - } - - if r.Latitude == 0 { - errors["latitude"] = append(errors["latitude"], "Latitude is required") - } - - if r.Longitude == 0 { - errors["longitude"] = append(errors["longitude"], "Longitude is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/article_dto.go b/dto/article_dto.go deleted file mode 100644 index 1df6ac2..0000000 --- a/dto/article_dto.go +++ /dev/null @@ -1,48 +0,0 @@ -package dto - -import ( - "strings" -) - -type ArticleResponseDTO struct { - ID string `json:"article_id"` - Title string `json:"title"` - CoverImage string `json:"coverImage"` - Author string `json:"author"` - Heading string `json:"heading"` - Content string `json:"content"` - PublishedAt string `json:"publishedAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestArticleDTO struct { - Title string `json:"title"` - CoverImage string `json:"coverImage"` - Author string `json:"author"` - Heading string `json:"heading"` - Content string `json:"content"` -} - -func (r *RequestArticleDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Title) == "" { - errors["title"] = append(errors["title"], "Title is required") - } - - if strings.TrimSpace(r.Author) == "" { - errors["author"] = append(errors["author"], "Author is required") - } - if strings.TrimSpace(r.Heading) == "" { - errors["heading"] = append(errors["heading"], "Heading is required") - } - if strings.TrimSpace(r.Content) == "" { - errors["content"] = append(errors["content"], "Content is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/auth/auth_admin_dto.go b/dto/auth/auth_admin_dto.go deleted file mode 100644 index b15a248..0000000 --- a/dto/auth/auth_admin_dto.go +++ /dev/null @@ -1,130 +0,0 @@ -package dto - -import ( - "regexp" - "strings" -) - -type LoginAdminRequest struct { - Deviceid string `json:"device_id"` - Email string `json:"email"` - Password string `json:"password"` -} - -type LoginResponse struct { - UserID string `json:"user_id"` - Role string `json:"role"` - Token string `json:"token"` -} - -type RegisterAdminRequest struct { - Name string `json:"name"` - Gender string `json:"gender"` - Dateofbirth string `json:"dateofbirth"` - Placeofbirth string `json:"placeofbirth"` - Phone string `json:"phone"` - Email string `json:"email"` - Password string `json:"password"` - PasswordConfirm string `json:"password_confirm"` -} - -type UserAdminDataResponse struct { - UserID string `json:"user_id"` - Name string `json:"name"` - Gender string `json:"gender"` - Dateofbirth string `json:"dateofbirth"` - Placeofbirth string `json:"placeofbirth"` - Phone string `json:"phone"` - Email string `json:"email"` - Password string `json:"password"` - Role string `json:"role"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -func (r *RegisterAdminRequest) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "Name is required") - } - - if strings.TrimSpace(r.Gender) == "" { - errors["gender"] = append(errors["gender"], "Gender is required") - } else if r.Gender != "male" && r.Gender != "female" { - errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'") - } - - if strings.TrimSpace(r.Dateofbirth) == "" { - errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") - } - - if strings.TrimSpace(r.Placeofbirth) == "" { - errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required") - } - - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone is required") - } else if !IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") - } - - if strings.TrimSpace(r.Email) == "" { - errors["email"] = append(errors["email"], "Email is required") - } else if !IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } - - if len(r.Password) < 6 { - errors["password"] = append(errors["password"], "Password must be at least 6 characters") - } else if !IsValidPassword(r.Password) { - errors["password"] = append(errors["password"], "Password must contain at least one uppercase letter, one number, and one special character") - } - - if r.Password != r.PasswordConfirm { - errors["password_confirm"] = append(errors["password_confirm"], "Password and confirmation do not match") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -func IsValidPhoneNumber(phone string) bool { - re := regexp.MustCompile(`^62\d{9,13}$`) - return re.MatchString(phone) -} - -func IsValidEmail(email string) bool { - re := regexp.MustCompile(`^[a-z0-9]+@[a-z0-9]+\.[a-z]{2,}$`) - return re.MatchString(email) -} - -func IsValidPassword(password string) bool { - - if len(password) < 6 { - return false - } - - hasUpper := false - hasDigit := false - hasSpecial := false - - for _, char := range password { - if char >= 'A' && char <= 'Z' { - hasUpper = true - } else if char >= '0' && char <= '9' { - hasDigit = true - } else if isSpecialCharacter(char) { - hasSpecial = true - } - } - - return hasUpper && hasDigit && hasSpecial -} - -func isSpecialCharacter(char rune) bool { - specialChars := "!@#$%^&*()-_=+[]{}|;:'\",.<>?/`~" - return strings.ContainsRune(specialChars, char) -} diff --git a/dto/auth/auth_masyarakat_dto.go b/dto/auth/auth_masyarakat_dto.go deleted file mode 100644 index cbd8f99..0000000 --- a/dto/auth/auth_masyarakat_dto.go +++ /dev/null @@ -1 +0,0 @@ -package dto \ No newline at end of file diff --git a/dto/auth/auth_pengelola_dto.go b/dto/auth/auth_pengelola_dto.go deleted file mode 100644 index 58c916c..0000000 --- a/dto/auth/auth_pengelola_dto.go +++ /dev/null @@ -1,133 +0,0 @@ -package dto - -import ( - "regexp" - "rijig/utils" -) - -type LoginPengelolaRequest struct { - Phone string `json:"phone"` -} - -func (r *LoginPengelolaRequest) ValidateLogin() (map[string][]string, bool) { - errors := make(map[string][]string) - - if r.Phone == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !utils.IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Phone number is not valid") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -type VerifLoginPengelolaRequest struct { - Phone string `json:"phone"` - Otp string `json:"verif_otp"` -} - -func (r *VerifLoginPengelolaRequest) ValidateVerifLogin() (map[string][]string, bool) { - errors := make(map[string][]string) - - if r.Phone == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !utils.IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Phone number is not valid") - } - - if r.Otp == "" { - errors["otp"] = append(errors["otp"], "OTP is required") - } else if len(r.Otp) != 6 { - errors["otp"] = append(errors["otp"], "OTP must be 6 digits") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -type LoginPengelolaResponse struct { - UserID string `json:"user_id"` - Role string `json:"role"` - Token string `json:"token"` -} - -type PengelolaIdentityCard struct { - Cardphoto string `json:"cardphoto"` - Identificationumber string `json:"identificationumber"` - Placeofbirth string `json:"placeofbirth"` - Dateofbirth string `json:"dateofbirth"` - Gender string `json:"gender"` - BloodType string `json:"bloodtype"` - District string `json:"district"` - Village string `json:"village"` - Neighbourhood string `json:"neighbourhood"` - Religion string `json:"religion"` - Maritalstatus string `json:"maritalstatus"` - Job string `json:"job"` - Citizenship string `json:"citizenship"` - Validuntil string `json:"validuntil"` -} - -func (r *PengelolaIdentityCard) ValidateIDcard() (map[string][]string, bool) { - errors := make(map[string][]string) - - if r.Cardphoto == "" { - errors["cardphoto"] = append(errors["cardphoto"], "Card photo is required") - } - - if r.Identificationumber == "" { - errors["identificationumber"] = append(errors["identificationumber"], "Identification number is required") - } - - if r.Dateofbirth == "" { - errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required") - } else if !isValidDate(r.Dateofbirth) { - errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -type PengelolaCompanyProfile struct { - CompanyName string `json:"company_name"` - CompanyPhone string `json:"company_phone"` - CompanyEmail string `json:"company_email"` -} - -func (r *PengelolaCompanyProfile) ValidateCompany() (map[string][]string, bool) { - errors := make(map[string][]string) - - if r.CompanyName == "" { - errors["company_name"] = append(errors["company_name"], "Company name is required") - } - - if r.CompanyPhone == "" { - errors["company_phone"] = append(errors["company_phone"], "Company phone is required") - } else if !utils.IsValidPhoneNumber(r.CompanyPhone) { - errors["company_phone"] = append(errors["company_phone"], "Invalid phone number format") - } - - if r.CompanyEmail == "" { - errors["company_email"] = append(errors["company_email"], "Company email is required") - } else if !utils.IsValidEmail(r.CompanyEmail) { - errors["company_email"] = append(errors["company_email"], "Invalid email format") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -func isValidDate(date string) bool { - re := regexp.MustCompile(`^\d{2}-\d{2}-\d{4}$`) - return re.MatchString(date) -} diff --git a/dto/auth/auth_pengepul_dto.go b/dto/auth/auth_pengepul_dto.go deleted file mode 100644 index cbd8f99..0000000 --- a/dto/auth/auth_pengepul_dto.go +++ /dev/null @@ -1 +0,0 @@ -package dto \ No newline at end of file diff --git a/dto/auth_dto.go b/dto/auth_dto.go deleted file mode 100644 index 5782667..0000000 --- a/dto/auth_dto.go +++ /dev/null @@ -1,42 +0,0 @@ -package dto - -import ( - "regexp" - "strings" -) - -type RegisterRequest struct { - Phone string `json:"phone"` -} - -type VerifyOTPRequest struct { - Phone string `json:"phone"` - OTP string `json:"otp"` - DeviceID string `json:"device_id"` -} - -type UserDataResponse struct { - UserID string `json:"user_id"` - UserRole string `json:"user_role"` - Token string `json:"token"` -} - -func (r *RegisterRequest) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone is required") - } else if !IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Invalid phone number format. Use 62 followed by 9-13 digits") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -func IsValidPhoneNumber(phone string) bool { - re := regexp.MustCompile(`^62\d{9,13}$`) - return re.MatchString(phone) -} diff --git a/dto/banner_dto.go b/dto/banner_dto.go deleted file mode 100644 index 56f214c..0000000 --- a/dto/banner_dto.go +++ /dev/null @@ -1,29 +0,0 @@ -package dto - -import "strings" - -type ResponseBannerDTO struct { - ID string `json:"id"` - BannerName string `json:"bannername"` - BannerImage string `json:"bannerimage"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestBannerDTO struct { - BannerName string `json:"bannername"` - BannerImage string `json:"bannerimage"` -} - -func (r *RequestBannerDTO) ValidateBannerInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.BannerName) == "" { - errors["bannername"] = append(errors["bannername"], "nama banner harus diisi") - } - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/collector_dto.go b/dto/collector_dto.go deleted file mode 100644 index 0a523d7..0000000 --- a/dto/collector_dto.go +++ /dev/null @@ -1,111 +0,0 @@ -package dto - -import ( - "fmt" - "strings" -) - -type NearbyCollectorDTO struct { - CollectorID string `json:"collector_id"` - Name string `json:"name"` - Phone string `json:"phone"` - Rating float32 `json:"rating"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - DistanceKm float64 `json:"distance_km"` - MatchedTrash []string `json:"matched_trash_ids"` -} - -type RequestCollectorDTO struct { - AddressId string `json:"address_id"` - AvaibleTrashbyCollector []RequestAvaibleTrashbyCollector `json:"avaible_trash"` -} - -type RequestAvaibleTrashbyCollector struct { - TrashId string `json:"trash_id"` - TrashPrice float32 `json:"trash_price"` -} - -type RequestAddAvaibleTrash struct { - AvaibleTrash []RequestAvaibleTrashbyCollector `json:"avaible_trash"` -} - -type SelectCollectorRequest struct { - Collector_id string `json:"collector_id"` -} - -func (r *SelectCollectorRequest) ValidateSelectCollectorRequest() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Collector_id) == "" { - errors["collector_id"] = append(errors["collector_id"], "collector_id harus diisi") - } - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func (r *RequestAddAvaibleTrash) ValidateRequestAddAvaibleTrash() (map[string][]string, bool) { - errors := make(map[string][]string) - - if len(r.AvaibleTrash) == 0 { - errors["avaible_trash"] = append(errors["avaible_trash"], "tidak boleh kosong") - } - - for i, trash := range r.AvaibleTrash { - if strings.TrimSpace(trash.TrashId) == "" { - errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)], "trash_id tidak boleh kosong") - } - if trash.TrashPrice <= 0 { - errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)], "trash_price harus lebih dari 0") - } - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -type ResponseCollectorDTO struct { - ID string `json:"collector_id"` - UserId string `json:"user_id"` - User *UserResponseDTO `json:"user,omitempty"` - AddressId string `json:"address_id"` - Address *AddressResponseDTO `json:"address,omitempty"` - JobStatus *string `json:"job_status,omitempty"` - Rating float32 `json:"rating"` - AvaibleTrashbyCollector []ResponseAvaibleTrashByCollector `json:"avaible_trash"` -} - -type ResponseAvaibleTrashByCollector struct { - ID string `json:"id"` - TrashId string `json:"trash_id"` - TrashName string `json:"trash_name"` - TrashIcon string `json:"trash_icon"` - TrashPrice float32 `json:"trash_price"` -} - -func (r *RequestCollectorDTO) ValidateRequestCollector() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.AddressId) == "" { - errors["address_id"] = append(errors["address_id"], "address_id harus diisi") - } - - for i, trash := range r.AvaibleTrashbyCollector { - if strings.TrimSpace(trash.TrashId) == "" { - errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_id", i)], "trash_id tidak boleh kosong") - } - if trash.TrashPrice <= 0 { - errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)] = append(errors[fmt.Sprintf("avaible_trash[%d].trash_price", i)], "trash_price harus lebih dari 0") - } - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} diff --git a/dto/company_profile_dto.go b/dto/company_profile_dto.go deleted file mode 100644 index f3cedd8..0000000 --- a/dto/company_profile_dto.go +++ /dev/null @@ -1,62 +0,0 @@ -package dto - -import ( - "rijig/utils" - "strings" -) - -type ResponseCompanyProfileDTO struct { - ID string `json:"id"` - UserID string `json:"userId"` - CompanyName string `json:"company_name"` - CompanyAddress string `json:"company_address"` - CompanyPhone string `json:"company_phone"` - CompanyEmail string `json:"company_email"` - CompanyLogo string `json:"company_logo,omitempty"` - CompanyWebsite string `json:"company_website,omitempty"` - TaxID string `json:"taxId,omitempty"` - FoundedDate string `json:"founded_date,omitempty"` - CompanyType string `json:"company_type,omitempty"` - CompanyDescription string `json:"company_description"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestCompanyProfileDTO struct { - CompanyName string `json:"company_name"` - CompanyAddress string `json:"company_address"` - CompanyPhone string `json:"company_phone"` - CompanyEmail string `json:"company_email"` - CompanyLogo string `json:"company_logo,omitempty"` - CompanyWebsite string `json:"company_website,omitempty"` - TaxID string `json:"taxId,omitempty"` - FoundedDate string `json:"founded_date,omitempty"` - CompanyType string `json:"company_type,omitempty"` - CompanyDescription string `json:"company_description"` -} - -func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.CompanyName) == "" { - errors["company_Name"] = append(errors["company_name"], "Company name is required") - } - - if strings.TrimSpace(r.CompanyAddress) == "" { - errors["company_Address"] = append(errors["company_address"], "Company address is required") - } - - if !utils.IsValidPhoneNumber(r.CompanyPhone) { - errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit") - } - - if strings.TrimSpace(r.CompanyDescription) == "" { - errors["company_Description"] = append(errors["company_description"], "Company description is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/coveragearea_dto.go b/dto/coveragearea_dto.go deleted file mode 100644 index 3d1f46d..0000000 --- a/dto/coveragearea_dto.go +++ /dev/null @@ -1,34 +0,0 @@ -package dto - -import "strings" - -type RequestCoverageArea struct { - Province string `json:"province"` - Regency string `json:"regency"` -} - -type ResponseCoverageArea struct { - ID string `json:"id"` - Province string `json:"province"` - Regency string `json:"regency"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -func (r *RequestCoverageArea) ValidateCoverageArea() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Province) == "" { - errors["province"] = append(errors["province"], "nama provinsi harus diisi") - } - - if strings.TrimSpace(r.Regency) == "" { - errors["regency"] = append(errors["regency"], "nama regency harus diisi") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/identitycard_dto.go b/dto/identitycard_dto.go deleted file mode 100644 index aa4274e..0000000 --- a/dto/identitycard_dto.go +++ /dev/null @@ -1,116 +0,0 @@ -package dto - -import ( - "strings" -) - -type ResponseIdentityCardDTO struct { - ID string `json:"id"` - UserID string `json:"userId"` - Identificationumber string `json:"identificationumber"` - Placeofbirth string `json:"placeofbirth"` - Dateofbirth string `json:"dateofbirth"` - Gender string `json:"gender"` - BloodType string `json:"bloodtype"` - District string `json:"district"` - Village string `json:"village"` - Neighbourhood string `json:"neighbourhood"` - Religion string `json:"religion"` - Maritalstatus string `json:"maritalstatus"` - Job string `json:"job"` - Citizenship string `json:"citizenship"` - Validuntil string `json:"validuntil"` - Cardphoto string `json:"cardphoto"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestIdentityCardDTO struct { - UserID string `json:"userId"` - Identificationumber string `json:"identificationumber"` - Placeofbirth string `json:"placeofbirth"` - Dateofbirth string `json:"dateofbirth"` - Gender string `json:"gender"` - BloodType string `json:"bloodtype"` - District string `json:"district"` - Village string `json:"village"` - Neighbourhood string `json:"neighbourhood"` - Religion string `json:"religion"` - Maritalstatus string `json:"maritalstatus"` - Job string `json:"job"` - Citizenship string `json:"citizenship"` - Validuntil string `json:"validuntil"` - Cardphoto string `json:"cardphoto"` -} - -func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]string, bool) { - errors := make(map[string][]string) - isValid := true - - if strings.TrimSpace(r.Identificationumber) == "" { - errors["identificationumber"] = append(errors["identificationumber"], "Nomor identifikasi harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Placeofbirth) == "" { - errors["placeofbirth"] = append(errors["placeofbirth"], "Tempat lahir harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Dateofbirth) == "" { - errors["dateofbirth"] = append(errors["dateofbirth"], "Tanggal lahir harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Gender) == "" { - errors["gender"] = append(errors["gender"], "Jenis kelamin harus diisi") - isValid = false - } - - if strings.TrimSpace(r.BloodType) == "" { - errors["bloodtype"] = append(errors["bloodtype"], "Golongan darah harus diisi") - isValid = false - } - - if strings.TrimSpace(r.District) == "" { - errors["district"] = append(errors["district"], "Kecamatan harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Village) == "" { - errors["village"] = append(errors["village"], "Desa harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Neighbourhood) == "" { - errors["neighbourhood"] = append(errors["neighbourhood"], "RT/RW harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Religion) == "" { - errors["religion"] = append(errors["religion"], "Agama harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Maritalstatus) == "" { - errors["maritalstatus"] = append(errors["maritalstatus"], "Status pernikahan harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Job) == "" { - errors["job"] = append(errors["job"], "Pekerjaan harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Citizenship) == "" { - errors["citizenship"] = append(errors["citizenship"], "Kewarganegaraan harus diisi") - isValid = false - } - - if strings.TrimSpace(r.Validuntil) == "" { - errors["validuntil"] = append(errors["validuntil"], "Masa berlaku harus diisi") - isValid = false - } - - return errors, isValid -} diff --git a/dto/initialcoint_dto.go b/dto/initialcoint_dto.go deleted file mode 100644 index 06d1fb2..0000000 --- a/dto/initialcoint_dto.go +++ /dev/null @@ -1,34 +0,0 @@ -package dto - -import "strings" - -type ReponseInitialCointDTO struct { - ID string `json:"coin_id"` - CoinName string `json:"coin_name"` - ValuePerUnit float64 `json:"value_perunit"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestInitialCointDTO struct { - CoinName string `json:"coin_name"` - ValuePerUnit float64 `json:"value_perunit"` -} - -func (r *RequestInitialCointDTO) ValidateCointInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.CoinName) == "" { - errors["coin_name"] = append(errors["coin_name"], "nama coin harus diisi") - } - - if r.ValuePerUnit <= 0 { - errors["value_perunit"] = append(errors["value_perunit"], "value per unit harus lebih besar dari 0") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/product_dto.go b/dto/product_dto.go deleted file mode 100644 index 66a5a58..0000000 --- a/dto/product_dto.go +++ /dev/null @@ -1,55 +0,0 @@ -package dto - -import ( - "mime/multipart" - "regexp" - "strings" -) - -type ResponseProductImageDTO struct { - ID string `json:"id"` - ProductID string `json:"productId"` - ImageURL string `json:"imageURL"` -} - -type ResponseProductDTO struct { - ID string `json:"id"` - StoreID string `json:"storeId"` - ProductName string `json:"productName"` - Quantity int `json:"quantity"` - Saled int `json:"saled"` - ProductImages []ResponseProductImageDTO `json:"productImages,omitempty"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestProductDTO struct { - ProductName string `json:"product_name"` - Quantity int `json:"quantity"` - ProductImages []*multipart.FileHeader `json:"product_images,omitempty"` -} - -func (r *RequestProductDTO) ValidateProductInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.ProductName) == "" { - errors["product_name"] = append(errors["product_name"], "Product name is required") - } else if len(r.ProductName) < 3 { - errors["product_name"] = append(errors["product_name"], "Product name must be at least 3 characters long") - } else { - validNameRegex := `^[a-zA-Z0-9\s_.-]+$` - if matched, _ := regexp.MatchString(validNameRegex, r.ProductName); !matched { - errors["product_name"] = append(errors["product_name"], "Product name can only contain letters, numbers, spaces, underscores, and dashes") - } - } - - if r.Quantity < 1 { - errors["quantity"] = append(errors["quantity"], "Quantity must be at least 1") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/rating_dto.go b/dto/rating_dto.go deleted file mode 100644 index 62b680d..0000000 --- a/dto/rating_dto.go +++ /dev/null @@ -1,25 +0,0 @@ -package dto - -import "strings" - -type CreatePickupRatingDTO struct { - Rating float32 `json:"rating"` - Feedback string `json:"feedback"` -} - -func (r *CreatePickupRatingDTO) ValidateCreatePickupRatingDTO() (map[string][]string, bool) { - errors := make(map[string][]string) - - if r.Rating < 1.0 || r.Rating > 5.0 { - errors["rating"] = append(errors["rating"], "Rating harus antara 1.0 sampai 5.0") - } - - if len(strings.TrimSpace(r.Feedback)) > 255 { - errors["feedback"] = append(errors["feedback"], "Feedback tidak boleh lebih dari 255 karakter") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} diff --git a/dto/request_pickup_dto.go b/dto/request_pickup_dto.go deleted file mode 100644 index ee63b38..0000000 --- a/dto/request_pickup_dto.go +++ /dev/null @@ -1,74 +0,0 @@ -package dto - -import ( - "strings" -) - -type SelectCollectorDTO struct { - CollectorID string `json:"collector_id"` -} - -type UpdateRequestPickupItemDTO struct { - ItemID string `json:"item_id"` - Amount float64 `json:"actual_amount"` -} - -type UpdatePickupItemsRequest struct { - Items []UpdateRequestPickupItemDTO `json:"items"` -} - -func (r *SelectCollectorDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.CollectorID) == "" { - errors["collector_id"] = append(errors["collector_id"], "collector_id tidak boleh kosong") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} - -type AssignedPickupDTO struct { - PickupID string `json:"pickup_id"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Notes string `json:"notes"` - MatchedTrash []string `json:"matched_trash"` -} - -type PickupRequestForCollectorDTO struct { - PickupID string `json:"pickup_id"` - UserID string `json:"user_id"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - DistanceKm float64 `json:"distance_km"` - MatchedTrash []string `json:"matched_trash"` -} - -type RequestPickupDTO struct { - AddressID string `json:"address_id"` - RequestMethod string `json:"request_method"` - Notes string `json:"notes,omitempty"` -} - -func (r *RequestPickupDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.AddressID) == "" { - errors["address_id"] = append(errors["address_id"], "alamat harus dipilih") - } - - method := strings.ToLower(strings.TrimSpace(r.RequestMethod)) - if method != "manual" && method != "otomatis" { - errors["request_method"] = append(errors["request_method"], "harus manual atau otomatis") - } - - if len(errors) > 0 { - return errors, false - } - return nil, true -} diff --git a/dto/requestpickup_dto.go b/dto/requestpickup_dto.go deleted file mode 100644 index 9ac69bd..0000000 --- a/dto/requestpickup_dto.go +++ /dev/null @@ -1,88 +0,0 @@ -package dto - -import ( - "fmt" - "strings" -) - -type RequestPickup struct { - AddressID string `json:"address_id"` - RequestMethod string `json:"request_method"` - EvidenceImage string `json:"evidence_image"` - Notes string `json:"notes"` - RequestItems []RequestPickupItem `json:"request_items"` -} - -type RequestPickupItem struct { - TrashCategoryID string `json:"trash_category_id"` - EstimatedAmount float64 `json:"estimated_amount"` -} - -type ResponseRequestPickup struct { - ID string `json:"id,omitempty"` - UserId string `json:"user_id,omitempty"` - User []UserResponseDTO `json:"user,omitempty"` - AddressID string `json:"address_id,omitempty"` - Address []AddressResponseDTO `json:"address,omitempty"` - EvidenceImage string `json:"evidence_image,omitempty"` - Notes string `json:"notes,omitempty"` - StatusPickup string `json:"status_pickup,omitempty"` - CollectorID string `json:"collectorid,omitempty"` - Collector []ResponseCollectorDTO `json:"collector,omitempty"` - ConfirmedByCollectorAt string `json:"confirmedat,omitempty"` - CreatedAt string `json:"created_at,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` - RequestItems []ResponseRequestPickupItem `json:"request_items,omitempty"` -} - -type ResponseRequestPickupItem struct { - ID string `json:"id,omitempty"` - TrashCategoryID string `json:"trash_category_id,omitempty"` - // TrashCategory []ResponseTrashCategoryDTO `json:"trash_category,omitempty"` - EstimatedAmount float64 `json:"estimated_amount,omitempty"` -} - -func (r *RequestPickup) ValidateRequestPickup() (map[string][]string, bool) { - errors := make(map[string][]string) - - if len(r.RequestItems) == 0 { - errors["request_items"] = append(errors["request_items"], "At least one item must be provided") - } - - if strings.TrimSpace(r.AddressID) == "" { - errors["address_id"] = append(errors["address_id"], "Address ID must be provided") - } - - for i, item := range r.RequestItems { - itemErrors, valid := item.ValidateRequestPickupItem(i) - if !valid { - for field, msgs := range itemErrors { - errors[field] = append(errors[field], msgs...) - } - } - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func (r *RequestPickupItem) ValidateRequestPickupItem(index int) (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.TrashCategoryID) == "" { - errors["trash_category_id"] = append(errors["trash_category_id"], fmt.Sprintf("Trash category ID cannot be empty (Item %d)", index+1)) - } - - if r.EstimatedAmount < 2 { - errors["estimated_amount"] = append(errors["estimated_amount"], fmt.Sprintf("Estimated amount must be >= 2.0 kg (Item %d)", index+1)) - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/role_dto.go b/dto/role_dto.go deleted file mode 100644 index 6002fc7..0000000 --- a/dto/role_dto.go +++ /dev/null @@ -1,8 +0,0 @@ -package dto - -type RoleResponseDTO struct { - ID string `json:"role_id"` - RoleName string `json:"role_name"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} diff --git a/dto/store_dto.go b/dto/store_dto.go deleted file mode 100644 index f5fd606..0000000 --- a/dto/store_dto.go +++ /dev/null @@ -1,68 +0,0 @@ -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["store_name"] = append(errors["store_name"], "Store name is required") - } else if len(r.StoreName) < 3 { - 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["store_name"] = append(errors["store_name"], "Store name can only contain letters, numbers, underscores, and periods") - } - } - - if strings.TrimSpace(r.StoreLogo) == "" { - errors["store_logo"] = append(errors["store_logo"], "Store logo is required") - } - - if strings.TrimSpace(r.StoreBanner) == "" { - errors["store_banner"] = append(errors["store_banner"], "Store banner is required") - } - - if strings.TrimSpace(r.StoreInfo) == "" { - 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["store_address_id"] = append(errors["store_address_id"], "Store address ID is required") - } else if matched, _ := regexp.MatchString(uuidRegex, r.StoreAddressID); !matched { - errors["store_address_id"] = append(errors["store_address_id"], "Invalid Store Address ID format") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/trash_dto.go b/dto/trash_dto.go deleted file mode 100644 index 15d8fcc..0000000 --- a/dto/trash_dto.go +++ /dev/null @@ -1,65 +0,0 @@ -package dto -/* -import ( - "strings" -) - -type RequestTrashCategoryDTO struct { - Name string `json:"name"` - EstimatedPrice string `json:"estimatedprice"` - Icon string `json:"icon"` -} - -type ResponseTrashCategoryDTO struct { - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Icon string `json:"icon,omitempty"` - EstimatedPrice float64 `json:"estimatedprice"` - CreatedAt string `json:"createdAt,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` - Details []ResponseTrashDetailDTO `json:"details,omitempty"` -} - -type ResponseTrashDetailDTO struct { - ID string `json:"id"` - CategoryID string `json:"category_id"` - Description string `json:"description"` - Price float64 `json:"price"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` -} - -type RequestTrashDetailDTO struct { - CategoryID string `json:"category_id"` - Description string `json:"description"` - Price float64 `json:"price"` -} - -func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "name is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Description) == "" { - errors["description"] = append(errors["description"], "description is required") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - */ \ No newline at end of file diff --git a/dto/trashcart_dto.go b/dto/trashcart_dto.go deleted file mode 100644 index d195a00..0000000 --- a/dto/trashcart_dto.go +++ /dev/null @@ -1,49 +0,0 @@ -package dto - -import ( - "fmt" - "strings" -) - -type RequestCartItemDTO struct { - TrashID string `json:"trash_id"` - Amount float64 `json:"amount"` -} - -type RequestCartDTO struct { - CartItems []RequestCartItemDTO `json:"cart_items"` -} - -type ResponseCartDTO struct { - ID string `json:"id"` - UserID string `json:"user_id"` - TotalAmount float64 `json:"total_amount"` - EstimatedTotalPrice float64 `json:"estimated_total_price"` - CartItems []ResponseCartItemDTO `json:"cart_items"` -} - -type ResponseCartItemDTO struct { - ID string `json:"id"` - TrashID string `json:"trash_id"` - TrashName string `json:"trash_name"` - TrashIcon string `json:"trash_icon"` - TrashPrice float64 `json:"trash_price"` - Amount float64 `json:"amount"` - SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"` -} - -func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) { - errors := make(map[string][]string) - - for i, item := range r.CartItems { - if strings.TrimSpace(item.TrashID) == "" { - errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong") - } - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/user_dto.go b/dto/user_dto.go deleted file mode 100644 index 3a1164c..0000000 --- a/dto/user_dto.go +++ /dev/null @@ -1,81 +0,0 @@ -package dto - -import ( - "rijig/utils" - "strings" -) - -type UserResponseDTO struct { - ID string `json:"id,omitempty"` - Username string `json:"username,omitempty"` - Avatar *string `json:"photoprofile,omitempty"` - Name string `json:"name,omitempty"` - Phone string `json:"phone,omitempty"` - Email string `json:"email,omitempty"` - EmailVerified bool `json:"emailVerified,omitempty"` - RoleName string `json:"role,omitempty"` - CreatedAt string `json:"createdAt,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` -} - -type RequestUserDTO struct { - Name string `json:"name"` - Phone string `json:"phone"` - Email string `json:"email"` -} - -func (r *RequestUserDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Name) == "" { - errors["name"] = append(errors["name"], "Name is required") - } - - if strings.TrimSpace(r.Phone) == "" { - errors["phone"] = append(errors["phone"], "Phone number is required") - } else if !utils.IsValidPhoneNumber(r.Phone) { - errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits") - } - - if strings.TrimSpace(r.Email) != "" && !utils.IsValidEmail(r.Email) { - errors["email"] = append(errors["email"], "Invalid email format") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type UpdatePasswordDTO struct { - OldPassword string `json:"old_password"` - NewPassword string `json:"new_password"` - ConfirmNewPassword string `json:"confirm_new_password"` -} - -func (u *UpdatePasswordDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if u.OldPassword == "" { - errors["old_password"] = append(errors["old_password"], "Old password is required") - } - - if u.NewPassword == "" { - errors["new_password"] = append(errors["new_password"], "New password is required") - } else if !utils.IsValidPassword(u.NewPassword) { - errors["new_password"] = append(errors["new_password"], "Password must contain at least one uppercase letter, one digit, and one special character") - } - - if u.ConfirmNewPassword == "" { - errors["confirm_new_password"] = append(errors["confirm_new_password"], "Confirm new password is required") - } else if u.NewPassword != u.ConfirmNewPassword { - errors["confirm_new_password"] = append(errors["confirm_new_password"], "Passwords do not match") - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} diff --git a/dto/userpin_dto.go b/dto/userpin_dto.go deleted file mode 100644 index 146cc96..0000000 --- a/dto/userpin_dto.go +++ /dev/null @@ -1,66 +0,0 @@ -package dto - -import ( - "fmt" - "regexp" - "strings" -) - -type RequestUserPinDTO struct { - Pin string `json:"userpin"` -} - -func (r *RequestUserPinDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(r.Pin) == "" { - errors["pin"] = append(errors["pin"], "Pin is required") - } - - if err := validatePin(r.Pin); err != nil { - errors["pin"] = append(errors["pin"], err.Error()) - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -type UpdateUserPinDTO struct { - OldPin string `json:"old_pin"` - NewPin string `json:"new_pin"` -} - -func (u *UpdateUserPinDTO) Validate() (map[string][]string, bool) { - errors := make(map[string][]string) - - if strings.TrimSpace(u.OldPin) == "" { - errors["old_pin"] = append(errors["old_pin"], "Old pin is required") - } - - if err := validatePin(u.NewPin); err != nil { - errors["new_pin"] = append(errors["new_pin"], err.Error()) - } - - if len(errors) > 0 { - return errors, false - } - - return nil, true -} - -func isNumeric(s string) bool { - re := regexp.MustCompile(`^[0-9]+$`) - return re.MatchString(s) -} - -func validatePin(pin string) error { - if len(pin) != 6 { - return fmt.Errorf("pin harus terdiri dari 6 digit") - } else if !isNumeric(pin) { - return fmt.Errorf("pin harus berupa angka") - } - return nil -} diff --git a/dto/wiayah_indonesia_dto.go b/dto/wiayah_indonesia_dto.go deleted file mode 100644 index b247b59..0000000 --- a/dto/wiayah_indonesia_dto.go +++ /dev/null @@ -1,27 +0,0 @@ -package dto - -type ProvinceResponseDTO struct { - ID string `json:"id"` - Name string `json:"name"` - Regencies []RegencyResponseDTO `json:"regencies,omitempty"` -} - -type RegencyResponseDTO struct { - ID string `json:"id"` - ProvinceID string `json:"province_id"` - Name string `json:"name"` - Districts []DistrictResponseDTO `json:"districts,omitempty"` -} - -type DistrictResponseDTO struct { - ID string `json:"id"` - RegencyID string `json:"regency_id"` - Name string `json:"name"` - Villages []VillageResponseDTO `json:"villages,omitempty"` -} - -type VillageResponseDTO struct { - ID string `json:"id"` - DistrictID string `json:"district_id"` - Name string `json:"name"` -} diff --git a/internal/about/about_handler.go b/internal/about/about_handler.go index ed2ed65..a5f1134 100644 --- a/internal/about/about_handler.go +++ b/internal/about/about_handler.go @@ -3,7 +3,6 @@ package about import ( "fmt" "log" - "rijig/dto" "rijig/utils" "github.com/gofiber/fiber/v2" @@ -20,7 +19,7 @@ func NewAboutHandler(aboutService AboutService) *AboutHandler { } func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { - var request dto.RequestAboutDTO + var request RequestAboutDTO if err := c.BodyParser(&request); err != nil { return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}}) } @@ -47,7 +46,7 @@ func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error { func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error { id := c.Params("id") - var request dto.RequestAboutDTO + var request RequestAboutDTO if err := c.BodyParser(&request); err != nil { log.Printf("Error parsing request body: %v", err) return utils.BadRequest(c, "Invalid input data") @@ -114,7 +113,7 @@ func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error { } func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { - var request dto.RequestAboutDetailDTO + var request RequestAboutDetailDTO if err := c.BodyParser(&request); err != nil { log.Printf("Error parsing request body: %v", err) return utils.BadRequest(c, "Invalid input data") @@ -143,7 +142,7 @@ func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error { func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error { id := c.Params("id") - var request dto.RequestAboutDetailDTO + var request RequestAboutDetailDTO if err := c.BodyParser(&request); err != nil { log.Printf("Error parsing request body: %v", err) return utils.BadRequest(c, "Invalid input data") diff --git a/internal/about/about_service.go b/internal/about/about_service.go index 4cec901..c6653e7 100644 --- a/internal/about/about_service.go +++ b/internal/about/about_service.go @@ -7,7 +7,6 @@ import ( "mime/multipart" "os" "path/filepath" - "rijig/dto" "rijig/model" "rijig/utils" "time" @@ -24,15 +23,15 @@ const ( ) type AboutService interface { - CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) - UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) - GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error) - GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error) - GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error) + CreateAbout(ctx context.Context, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) + UpdateAbout(ctx context.Context, id string, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) + GetAllAbout(ctx context.Context) ([]ResponseAboutDTO, error) + GetAboutByID(ctx context.Context, id string) (*ResponseAboutDTO, error) + GetAboutDetailById(ctx context.Context, id string) (*ResponseAboutDetailDTO, error) DeleteAbout(ctx context.Context, id string) error - CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) - UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) + CreateAboutDetail(ctx context.Context, request RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) + UpdateAboutDetail(ctx context.Context, id string, request RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) DeleteAboutDetail(ctx context.Context, id string) error } @@ -66,11 +65,11 @@ func (s *aboutService) invalidateAboutDetailCaches(aboutDetailID, aboutID string s.invalidateAboutCaches(aboutID) } -func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutDetailDTO, error) { +func formatResponseAboutDetailDTO(about *model.AboutDetail) (*ResponseAboutDetailDTO, error) { createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) - response := &dto.ResponseAboutDetailDTO{ + response := &ResponseAboutDetailDTO{ ID: about.ID, AboutID: about.AboutID, ImageDetail: about.ImageDetail, @@ -82,11 +81,11 @@ func formatResponseAboutDetailDTO(about *model.AboutDetail) (*dto.ResponseAboutD return response, nil } -func formatResponseAboutDTO(about *model.About) (*dto.ResponseAboutDTO, error) { +func formatResponseAboutDTO(about *model.About) (*ResponseAboutDTO, error) { createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt) - response := &dto.ResponseAboutDTO{ + response := &ResponseAboutDTO{ ID: about.ID, Title: about.Title, CoverImage: about.CoverImage, @@ -194,7 +193,7 @@ func deleteCoverImageAbout(coverimageAboutPath string) error { return nil } -func (s *aboutService) CreateAbout(ctx context.Context, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { +func (s *aboutService) CreateAbout(ctx context.Context, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) { errors, valid := request.ValidateAbout() if !valid { return nil, fmt.Errorf("validation error: %v", errors) @@ -224,7 +223,7 @@ func (s *aboutService) CreateAbout(ctx context.Context, request dto.RequestAbout return response, nil } -func (s *aboutService) UpdateAbout(ctx context.Context, id string, request dto.RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*dto.ResponseAboutDTO, error) { +func (s *aboutService) UpdateAbout(ctx context.Context, id string, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) { errors, valid := request.ValidateAbout() if !valid { return nil, fmt.Errorf("validation error: %v", errors) @@ -271,9 +270,9 @@ func (s *aboutService) UpdateAbout(ctx context.Context, id string, request dto.R return response, nil } -func (s *aboutService) GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, error) { +func (s *aboutService) GetAllAbout(ctx context.Context) ([]ResponseAboutDTO, error) { - var cachedAbouts []dto.ResponseAboutDTO + var cachedAbouts []ResponseAboutDTO if err := utils.GetCache(cacheKeyAllAbout, &cachedAbouts); err == nil { return cachedAbouts, nil } @@ -283,7 +282,7 @@ func (s *aboutService) GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, return nil, fmt.Errorf("failed to get About list: %v", err) } - var aboutDTOList []dto.ResponseAboutDTO + var aboutDTOList []ResponseAboutDTO for _, about := range aboutList { response, err := formatResponseAboutDTO(&about) if err != nil { @@ -300,10 +299,10 @@ func (s *aboutService) GetAllAbout(ctx context.Context) ([]dto.ResponseAboutDTO, return aboutDTOList, nil } -func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*dto.ResponseAboutDTO, error) { +func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*ResponseAboutDTO, error) { cacheKey := fmt.Sprintf(cacheKeyAboutByID, id) - var cachedAbout dto.ResponseAboutDTO + var cachedAbout ResponseAboutDTO if err := utils.GetCache(cacheKey, &cachedAbout); err == nil { return &cachedAbout, nil } @@ -318,7 +317,7 @@ func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*dto.Respon return nil, fmt.Errorf("error formatting About response: %v", err) } - var responseDetails []dto.ResponseAboutDetailDTO + var responseDetails []ResponseAboutDetailDTO for _, detail := range about.AboutDetail { formattedDetail, err := formatResponseAboutDetailDTO(&detail) if err != nil { @@ -336,10 +335,10 @@ func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*dto.Respon return response, nil } -func (s *aboutService) GetAboutDetailById(ctx context.Context, id string) (*dto.ResponseAboutDetailDTO, error) { +func (s *aboutService) GetAboutDetailById(ctx context.Context, id string) (*ResponseAboutDetailDTO, error) { cacheKey := fmt.Sprintf(cacheKeyAboutDetail, id) - var cachedDetail dto.ResponseAboutDetailDTO + var cachedDetail ResponseAboutDetailDTO if err := utils.GetCache(cacheKey, &cachedDetail); err == nil { return &cachedDetail, nil } @@ -390,7 +389,7 @@ func (s *aboutService) DeleteAbout(ctx context.Context, id string) error { return nil } -func (s *aboutService) CreateAboutDetail(ctx context.Context, request dto.RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { +func (s *aboutService) CreateAboutDetail(ctx context.Context, request RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) { errors, valid := request.ValidateAboutDetail() if !valid { return nil, fmt.Errorf("validation error: %v", errors) @@ -426,7 +425,7 @@ func (s *aboutService) CreateAboutDetail(ctx context.Context, request dto.Reques return response, nil } -func (s *aboutService) UpdateAboutDetail(ctx context.Context, id string, request dto.RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*dto.ResponseAboutDetailDTO, error) { +func (s *aboutService) UpdateAboutDetail(ctx context.Context, id string, request RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) { errors, valid := request.ValidateAboutDetail() if !valid { return nil, fmt.Errorf("validation error: %v", errors) diff --git a/internal/address/address_handler.go b/internal/address/address_handler.go index 1c4eb44..cdc4514 100644 --- a/internal/address/address_handler.go +++ b/internal/address/address_handler.go @@ -1,7 +1,6 @@ package address import ( - "rijig/dto" "rijig/middleware" "rijig/utils" @@ -17,7 +16,7 @@ func NewAddressHandler(addressService AddressService) *AddressHandler { } func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error { - var request dto.CreateAddressDTO + var request CreateAddressDTO claims, err := middleware.GetUserFromContext(c) if err != nil { return err @@ -74,7 +73,7 @@ func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error { addressID := c.Params("address_id") - var request dto.CreateAddressDTO + var request CreateAddressDTO claims, err := middleware.GetUserFromContext(c) if err != nil { return err diff --git a/internal/address/address_service.go b/internal/address/address_service.go index 12b9625..5189173 100644 --- a/internal/address/address_service.go +++ b/internal/address/address_service.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "rijig/dto" "rijig/internal/wilayahindo" "rijig/model" "rijig/utils" @@ -20,10 +19,10 @@ const ( ) type AddressService interface { - CreateAddress(ctx context.Context, userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) - GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) - GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error) - UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) + CreateAddress(ctx context.Context, userID string, request CreateAddressDTO) (*AddressResponseDTO, error) + GetAddressByUserID(ctx context.Context, userID string) ([]AddressResponseDTO, error) + GetAddressByID(ctx context.Context, userID, id string) (*AddressResponseDTO, error) + UpdateAddress(ctx context.Context, userID, id string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error) DeleteAddress(ctx context.Context, userID, id string) error } @@ -39,7 +38,7 @@ func NewAddressService(addressRepo AddressRepository, wilayahRepo wilayahindo.Wi } } -func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO dto.CreateAddressDTO) (string, string, string, string, error) { +func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO CreateAddressDTO) (string, string, string, string, error) { province, _, err := s.wilayahRepo.FindProvinceByID(ctx, addressDTO.Province, 0, 0) if err != nil { @@ -64,11 +63,11 @@ func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO dto. return province.Name, regency.Name, district.Name, village.Name, nil } -func (s *addressService) mapToResponseDTO(address *model.Address) *dto.AddressResponseDTO { +func (s *addressService) mapToResponseDTO(address *model.Address) *AddressResponseDTO { createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt) - return &dto.AddressResponseDTO{ + return &AddressResponseDTO{ UserID: address.UserID, ID: address.ID, Province: address.Province, @@ -98,20 +97,20 @@ func (s *addressService) invalidateAddressCaches(userID, addressID string) { } } -func (s *addressService) cacheAddress(addressDTO *dto.AddressResponseDTO) { +func (s *addressService) cacheAddress(addressDTO *AddressResponseDTO) { cacheKey := fmt.Sprintf(addressCacheKeyPattern, addressDTO.ID) if err := utils.SetCache(cacheKey, addressDTO, cacheTTL); err != nil { fmt.Printf("Error caching address to Redis: %v\n", err) } } -func (s *addressService) cacheUserAddresses(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) { +func (s *addressService) cacheUserAddresses(ctx context.Context, userID string) ([]AddressResponseDTO, error) { addresses, err := s.addressRepo.FindAddressByUserID(ctx, userID) if err != nil { return nil, fmt.Errorf("failed to fetch addresses: %w", err) } - var addressDTOs []dto.AddressResponseDTO + var addressDTOs []AddressResponseDTO for _, address := range addresses { addressDTOs = append(addressDTOs, *s.mapToResponseDTO(&address)) } @@ -137,7 +136,7 @@ func (s *addressService) checkAddressOwnership(ctx context.Context, userID, addr return address, nil } -func (s *addressService) CreateAddress(ctx context.Context, userID string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { +func (s *addressService) CreateAddress(ctx context.Context, userID string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error) { provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO) if err != nil { @@ -168,10 +167,10 @@ func (s *addressService) CreateAddress(ctx context.Context, userID string, addre return responseDTO, nil } -func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) ([]dto.AddressResponseDTO, error) { +func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) ([]AddressResponseDTO, error) { cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID) - var cachedAddresses []dto.AddressResponseDTO + var cachedAddresses []AddressResponseDTO if err := utils.GetCache(cacheKey, &cachedAddresses); err == nil { return cachedAddresses, nil @@ -180,7 +179,7 @@ func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) return s.cacheUserAddresses(ctx, userID) } -func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) (*dto.AddressResponseDTO, error) { +func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) (*AddressResponseDTO, error) { address, err := s.checkAddressOwnership(ctx, userID, id) if err != nil { @@ -188,7 +187,7 @@ func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) } cacheKey := fmt.Sprintf(addressCacheKeyPattern, id) - var cachedAddress dto.AddressResponseDTO + var cachedAddress AddressResponseDTO if err := utils.GetCache(cacheKey, &cachedAddress); err == nil { return &cachedAddress, nil @@ -200,7 +199,7 @@ func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) return responseDTO, nil } -func (s *addressService) UpdateAddress(ctx context.Context, userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { +func (s *addressService) UpdateAddress(ctx context.Context, userID, id string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error) { address, err := s.checkAddressOwnership(ctx, userID, id) if err != nil { diff --git a/internal/authentication/authentication_repository.go b/internal/authentication/authentication_repository.go index 4993150..23d367e 100644 --- a/internal/authentication/authentication_repository.go +++ b/internal/authentication/authentication_repository.go @@ -2,6 +2,8 @@ package authentication import ( "context" + "fmt" + "log" "rijig/model" "gorm.io/gorm" @@ -15,6 +17,9 @@ type AuthenticationRepository interface { CreateUser(ctx context.Context, user *model.User) error UpdateUser(ctx context.Context, user *model.User) error PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error + + GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) + GetCompanyProfilesByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.CompanyProfile, error) } type authenticationRepository struct { @@ -84,3 +89,37 @@ func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, Where("id = ?", userID). Updates(updates).Error } + +func (r *authenticationRepository) GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) { + var identityCards []model.IdentityCard + + if err := r.db.WithContext(ctx). + Joins("JOIN users ON identity_cards.user_id = users.id"). + Where("users.registration_status = ?", userRegStatus). + Preload("User"). + Preload("User.Role"). + Find(&identityCards).Error; err != nil { + log.Printf("Error fetching identity cards by user registration status: %v", err) + return nil, fmt.Errorf("error fetching identity cards by user registration status: %w", err) + } + + log.Printf("Found %d identity cards with registration status: %s", len(identityCards), userRegStatus) + return identityCards, nil +} + +func (r *authenticationRepository) GetCompanyProfilesByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.CompanyProfile, error) { + var companyProfiles []model.CompanyProfile + + if err := r.db.WithContext(ctx). + Joins("JOIN users ON company_profiles.user_id = users.id"). + Where("users.registration_status = ?", userRegStatus). + Preload("User"). + Preload("User.Role"). + Find(&companyProfiles).Error; err != nil { + log.Printf("Error fetching company profiles by user registration status: %v", err) + return nil, fmt.Errorf("error fetching company profiles by user registration status: %w", err) + } + + log.Printf("Found %d company profiles with registration status: %s", len(companyProfiles), userRegStatus) + return companyProfiles, nil +} diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index 3956216..f2be14f 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -315,7 +315,9 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO return nil, fmt.Errorf("kode OTP salah") } - user, err := s.authRepo.FindUserByID(ctx, otpData.UserID) + normalizedRole := strings.ToLower(req.RoleName) + + user, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole) if err != nil { return nil, fmt.Errorf("user tidak ditemukan") } @@ -324,24 +326,29 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO tokenResponse, err := utils.GenerateTokenPair( user.ID, - user.Role.RoleName, + normalizedRole, req.DeviceID, - "pin_verification_required", + user.RegistrationStatus, int(user.RegistrationProgress), ) if err != nil { return nil, fmt.Errorf("gagal generate token: %v", err) } + nextStep := utils.GetNextRegistrationStep( + normalizedRole, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) + return &AuthResponse{ - Message: "OTP berhasil diverifikasi, silakan masukkan PIN", + Message: "otp berhasil diverifikasi", AccessToken: tokenResponse.AccessToken, RefreshToken: tokenResponse.RefreshToken, TokenType: string(tokenResponse.TokenType), ExpiresIn: tokenResponse.ExpiresIn, - User: convertUserToResponse(user), RegistrationStatus: user.RegistrationStatus, - NextStep: "Masukkan PIN", + NextStep: nextStep, SessionID: tokenResponse.SessionID, }, nil } diff --git a/internal/company/company_route.go b/internal/company/company_route.go index 78012aa..4e8b5ae 100644 --- a/internal/company/company_route.go +++ b/internal/company/company_route.go @@ -2,6 +2,7 @@ package company import ( "rijig/config" + "rijig/internal/authentication" "rijig/middleware" "github.com/gofiber/fiber/v2" @@ -9,7 +10,8 @@ import ( func CompanyRouter(api fiber.Router) { companyProfileRepo := NewCompanyProfileRepository(config.DB) - companyProfileService := NewCompanyProfileService(companyProfileRepo) + authRepo := authentication.NewAuthenticationRepository(config.DB) + companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo) companyProfileHandler := NewCompanyProfileHandler(companyProfileService) companyProfileAPI := api.Group("/companyprofile") diff --git a/internal/company/company_service.go b/internal/company/company_service.go index ebef795..291086a 100644 --- a/internal/company/company_service.go +++ b/internal/company/company_service.go @@ -3,8 +3,13 @@ package company import ( "context" "fmt" + "log" + "rijig/internal/authentication" + "rijig/internal/role" + "rijig/internal/userprofile" "rijig/model" "rijig/utils" + "time" ) type CompanyProfileService interface { @@ -13,15 +18,19 @@ type CompanyProfileService interface { GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) DeleteCompanyProfile(ctx context.Context, userID string) error + + GetAllCompanyProfilesByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseCompanyProfileDTO, error) + UpdateUserRegistrationStatusByCompany(ctx context.Context, companyUserID string, newStatus string) error } type companyProfileService struct { companyRepo CompanyProfileRepository + authRepo authentication.AuthenticationRepository } -func NewCompanyProfileService(companyRepo CompanyProfileRepository) CompanyProfileService { +func NewCompanyProfileService(companyRepo CompanyProfileRepository, authRepo authentication.AuthenticationRepository) CompanyProfileService { return &companyProfileService{ - companyRepo: companyRepo, + companyRepo, authRepo, } } @@ -48,9 +57,9 @@ func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*Respon } func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) { - if errors, valid := request.ValidateCompanyProfileInput(); !valid { - return nil, fmt.Errorf("validation failed: %v", errors) - } + // if errors, valid := request.ValidateCompanyProfileInput(); !valid { + // return nil, fmt.Errorf("validation failed: %v", errors) + // } companyProfile := &model.CompanyProfile{ UserID: userID, @@ -134,3 +143,100 @@ func (s *companyProfileService) UpdateCompanyProfile(ctx context.Context, userID func (s *companyProfileService) DeleteCompanyProfile(ctx context.Context, userID string) error { return s.companyRepo.DeleteCompanyProfileByUserID(ctx, userID) } + +func (s *companyProfileService) GetAllCompanyProfilesByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseCompanyProfileDTO, error) { + companyProfiles, err := s.authRepo.GetCompanyProfilesByUserRegStatus(ctx, userRegStatus) + if err != nil { + log.Printf("Error getting company profiles by registration status: %v", err) + return nil, fmt.Errorf("failed to get company profiles: %w", err) + } + + var response []ResponseCompanyProfileDTO + for _, profile := range companyProfiles { + dto := ResponseCompanyProfileDTO{ + ID: profile.ID, + UserID: profile.UserID, + CompanyName: profile.CompanyName, + CompanyAddress: profile.CompanyAddress, + CompanyPhone: profile.CompanyPhone, + CompanyEmail: profile.CompanyEmail, + CompanyLogo: profile.CompanyLogo, + CompanyWebsite: profile.CompanyWebsite, + TaxID: profile.TaxID, + FoundedDate: profile.FoundedDate, + CompanyType: profile.CompanyType, + CompanyDescription: profile.CompanyDescription, + CreatedAt: profile.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: profile.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + response = append(response, dto) + } + + return response, nil +} + +func (s *companyProfileService) UpdateUserRegistrationStatusByCompany(ctx context.Context, companyUserID string, newStatus string) error { + + user, err := s.authRepo.FindUserByID(ctx, companyUserID) + if err != nil { + log.Printf("Error finding user by ID %s: %v", companyUserID, err) + return fmt.Errorf("user not found: %w", err) + } + + updates := map[string]interface{}{ + "registration_status": newStatus, + "updated_at": time.Now(), + } + + switch newStatus { + case utils.RegStatusConfirmed: + updates["registration_progress"] = utils.ProgressDataSubmitted + case utils.RegStatusRejected: + updates["registration_progress"] = utils.ProgressOTPVerified + } + + err = s.authRepo.PatchUser(ctx, user.ID, updates) + if err != nil { + log.Printf("Error updating user registration status for user ID %s: %v", user.ID, err) + return fmt.Errorf("failed to update user registration status: %w", err) + } + + log.Printf("Successfully updated registration status for user ID %s to %s", user.ID, newStatus) + return nil +} + +func (s *companyProfileService) GetUserProfile(ctx context.Context, userID string) (*userprofile.UserProfileResponseDTO, error) { + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + log.Printf("Error getting user profile for ID %s: %v", userID, err) + return nil, fmt.Errorf("failed to get user profile: %w", err) + } + + response := &userprofile.UserProfileResponseDTO{ + ID: user.ID, + Name: user.Name, + Gender: user.Gender, + Dateofbirth: user.Dateofbirth, + Placeofbirth: user.Placeofbirth, + Phone: user.Phone, + Email: user.Email, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + if user.Avatar != nil { + response.Avatar = *user.Avatar + } + + if user.Role != nil { + response.Role = role.RoleResponseDTO{ + ID: user.Role.ID, + RoleName: user.Role.RoleName, + CreatedAt: user.Role.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: user.Role.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + } + + return response, nil +} diff --git a/internal/identitycart/identitycart_dto.go b/internal/identitycart/identitycart_dto.go index 78dc475..ea7a4d7 100644 --- a/internal/identitycart/identitycart_dto.go +++ b/internal/identitycart/identitycart_dto.go @@ -9,6 +9,7 @@ type ResponseIdentityCardDTO struct { ID string `json:"id"` UserID string `json:"userId"` Identificationumber string `json:"identificationumber"` + Fullname string `json:"fullname"` Placeofbirth string `json:"placeofbirth"` Dateofbirth string `json:"dateofbirth"` Gender string `json:"gender"` @@ -31,9 +32,10 @@ type ResponseIdentityCardDTO struct { } type RequestIdentityCardDTO struct { - DeviceID string `json:"device_id"` + // DeviceID string `json:"device_id"` UserID string `json:"userId"` Identificationumber string `json:"identificationumber"` + Fullname string `json:"fullname"` Placeofbirth string `json:"placeofbirth"` Dateofbirth string `json:"dateofbirth"` Gender string `json:"gender"` diff --git a/internal/identitycart/identitycart_handler.go b/internal/identitycart/identitycart_handler.go index 049b9b1..325ca37 100644 --- a/internal/identitycart/identitycart_handler.go +++ b/internal/identitycart/identitycart_handler.go @@ -1,8 +1,10 @@ package identitycart import ( + "log" "rijig/middleware" "rijig/utils" + "strings" "github.com/gofiber/fiber/v2" ) @@ -18,27 +20,33 @@ func NewIdentityCardHandler(service IdentityCardService) *IdentityCardHandler { func (h *IdentityCardHandler) CreateIdentityCardHandler(c *fiber.Ctx) error { claims, err := middleware.GetUserFromContext(c) if err != nil { - return err - } - - cardPhoto, err := c.FormFile("cardphoto") - if err != nil { - return utils.BadRequest(c, "KTP photo is required") + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "unauthorized access") } var input RequestIdentityCardDTO if err := c.BodyParser(&input); err != nil { + log.Printf("Error parsing body: %v", err) return utils.BadRequest(c, "Invalid input format") } - if errs, valid := input.ValidateIdentityCardInput(); !valid { return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs) } - response, err := h.service.CreateIdentityCard(c.Context(), claims.UserID, &input, cardPhoto) + cardPhoto, err := c.FormFile("cardphoto") if err != nil { - return utils.InternalServerError(c, err.Error()) + log.Printf("Error getting card photo: %v", err) + return utils.BadRequest(c, "KTP photo is required") + } + + response, err := h.service.CreateIdentityCard(c.Context(), claims.UserID, claims.DeviceID, &input, cardPhoto) + if err != nil { + log.Printf("Error creating identity card: %v", err) + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to create identity card") } return utils.SuccessWithData(c, "KTP successfully submitted", response) @@ -47,27 +55,158 @@ func (h *IdentityCardHandler) CreateIdentityCardHandler(c *fiber.Ctx) error { func (h *IdentityCardHandler) GetIdentityByID(c *fiber.Ctx) error { id := c.Params("id") if id == "" { - return utils.BadRequest(c, "id is required") + return utils.BadRequest(c, "ID is required") } result, err := h.service.GetIdentityCardByID(c.Context(), id) if err != nil { - return utils.NotFound(c, "data not found") + log.Printf("Error getting identity card by ID %s: %v", id, err) + return utils.NotFound(c, "Identity card not found") } - return utils.SuccessWithData(c, "success retrieve identity card", result) + return utils.SuccessWithData(c, "Successfully retrieved identity card", result) } func (h *IdentityCardHandler) GetIdentityByUserId(c *fiber.Ctx) error { claims, err := middleware.GetUserFromContext(c) if err != nil { - return err + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "Unauthorized access") } result, err := h.service.GetIdentityCardsByUserID(c.Context(), claims.UserID) if err != nil { - return utils.InternalServerError(c, "failed to fetch your identity card data") + log.Printf("Error getting identity cards for user %s: %v", claims.UserID, err) + return utils.InternalServerError(c, "Failed to fetch your identity card data") } - return utils.SuccessWithData(c, "success retrieve your identity card", result) + return utils.SuccessWithData(c, "Successfully retrieved your identity cards", result) +} + +func (h *IdentityCardHandler) UpdateIdentityCardHandler(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "Unauthorized access") + } + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Identity card ID is required") + } + + var input RequestIdentityCardDTO + if err := c.BodyParser(&input); err != nil { + log.Printf("Error parsing body: %v", err) + return utils.BadRequest(c, "Invalid input format") + } + + if errs, valid := input.ValidateIdentityCardInput(); !valid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs) + } + + cardPhoto, err := c.FormFile("cardphoto") + if err != nil && err.Error() != "there is no uploaded file associated with the given key" { + log.Printf("Error getting card photo: %v", err) + return utils.BadRequest(c, "Invalid card photo") + } + + if cardPhoto != nil && cardPhoto.Size > 5*1024*1024 { + return utils.BadRequest(c, "File size must be less than 5MB") + } + + response, err := h.service.UpdateIdentityCard(c.Context(), claims.UserID, id, &input, cardPhoto) + if err != nil { + log.Printf("Error updating identity card: %v", err) + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Identity card not found") + } + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to update identity card") + } + + return utils.SuccessWithData(c, "Identity card successfully updated", response) +} + +func (h *IdentityCardHandler) GetAllIdentityCardsByRegStatus(c *fiber.Ctx) error { + _, err := middleware.GetUserFromContext(c) + if err != nil { + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "Unauthorized access") + } + + // if claims.Role != "admin" { + // return utils.Forbidden(c, "Access denied: admin role required") + // } + + status := c.Query("status", utils.RegStatusPending) + + validStatuses := map[string]bool{ + utils.RegStatusPending: true, + "confirmed": true, + "rejected": true, + } + + if !validStatuses[status] { + return utils.BadRequest(c, "Invalid status. Valid values: pending, confirmed, rejected") + } + + result, err := h.service.GetAllIdentityCardsByRegStatus(c.Context(), status) + if err != nil { + log.Printf("Error getting identity cards by status %s: %v", status, err) + return utils.InternalServerError(c, "Failed to fetch identity cards") + } + + return utils.SuccessWithData(c, "Successfully retrieved identity cards", result) +} + +func (h *IdentityCardHandler) UpdateUserRegistrationStatusByIdentityCard(c *fiber.Ctx) error { + _, err := middleware.GetUserFromContext(c) + if err != nil { + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "Unauthorized access") + } + + userID := c.Params("userId") + if userID == "" { + return utils.BadRequest(c, "User ID is required") + } + + type StatusUpdateRequest struct { + Status string `json:"status" validate:"required,oneof=confirmed rejected"` + } + + var input StatusUpdateRequest + if err := c.BodyParser(&input); err != nil { + log.Printf("Error parsing body: %v", err) + return utils.BadRequest(c, "Invalid input format") + } + + if input.Status != "confirmed" && input.Status != "rejected" { + return utils.BadRequest(c, "Invalid status. Valid values: confirmed, rejected") + } + + err = h.service.UpdateUserRegistrationStatusByIdentityCard(c.Context(), userID, input.Status) + if err != nil { + log.Printf("Error updating user registration status: %v", err) + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "User not found") + } + return utils.InternalServerError(c, "Failed to update registration status") + } + + message := "User registration status successfully updated to " + input.Status + return utils.Success(c, message) +} + +func (h *IdentityCardHandler) DeleteIdentityCardHandler(c *fiber.Ctx) error { + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Identity card ID is required") + } + + return utils.Success(c, "Identity card successfully deleted") } diff --git a/internal/identitycart/identitycart_route.go b/internal/identitycart/identitycart_route.go index f9f0673..51fd8ca 100644 --- a/internal/identitycart/identitycart_route.go +++ b/internal/identitycart/identitycart_route.go @@ -3,7 +3,9 @@ package identitycart import ( "rijig/config" "rijig/internal/authentication" + "rijig/internal/userprofile" "rijig/middleware" + "rijig/utils" "github.com/gofiber/fiber/v2" ) @@ -11,25 +13,34 @@ import ( func UserIdentityCardRoute(api fiber.Router) { identityRepo := NewIdentityCardRepository(config.DB) authRepo := authentication.NewAuthenticationRepository(config.DB) - identityService := NewIdentityCardService(identityRepo, authRepo) + userRepo := userprofile.NewUserProfileRepository(config.DB) + identityService := NewIdentityCardService(identityRepo, authRepo, userRepo) identityHandler := NewIdentityCardHandler(identityService) identity := api.Group("/identity") identity.Post("/create", middleware.AuthMiddleware(), - middleware.RequireRoles("pengelola", "pengepul"), + middleware.RequireRoles(utils.RolePengepul), identityHandler.CreateIdentityCardHandler, ) identity.Get("/:id", middleware.AuthMiddleware(), - middleware.RequireRoles("pengelola", "pengepul"), identityHandler.GetIdentityByID, ) + identity.Get("/s", + middleware.AuthMiddleware(), + identityHandler.GetIdentityByUserId, + ) identity.Get("/", middleware.AuthMiddleware(), - middleware.RequireRoles("pengelola", "pengepul"), - identityHandler.GetIdentityByUserId, + middleware.RequireRoles(utils.RoleAdministrator), + identityHandler.GetAllIdentityCardsByRegStatus, + ) + identity.Patch("/:userId/status", + middleware.AuthMiddleware(), + middleware.RequireRoles(utils.RoleAdministrator), + identityHandler.UpdateUserRegistrationStatusByIdentityCard, ) } diff --git a/internal/identitycart/identitycart_service.go b/internal/identitycart/identitycart_service.go index e72e219..0c557f9 100644 --- a/internal/identitycart/identitycart_service.go +++ b/internal/identitycart/identitycart_service.go @@ -2,36 +2,49 @@ package identitycart import ( "context" + "errors" "fmt" + "io" "log" "mime/multipart" "os" "path/filepath" "rijig/internal/authentication" + "rijig/internal/role" + "rijig/internal/userprofile" "rijig/model" "rijig/utils" - "strings" + "time" ) type IdentityCardService interface { - CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) + CreateIdentityCard(ctx context.Context, userID, deviceID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error) UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error) + + GetAllIdentityCardsByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseIdentityCardDTO, error) + UpdateUserRegistrationStatusByIdentityCard(ctx context.Context, identityCardUserID string, newStatus string) error } type identityCardService struct { identityRepo IdentityCardRepository authRepo authentication.AuthenticationRepository + userRepo userprofile.UserProfileRepository } -func NewIdentityCardService(identityRepo IdentityCardRepository, authRepo authentication.AuthenticationRepository) IdentityCardService { +func NewIdentityCardService(identityRepo IdentityCardRepository, authRepo authentication.AuthenticationRepository, userRepo userprofile.UserProfileRepository) IdentityCardService { return &identityCardService{ - identityRepo: identityRepo, - authRepo: authRepo, + identityRepo, + authRepo, userRepo, } } +type IdentityCardWithUserDTO struct { + IdentityCard ResponseIdentityCardDTO `json:"identity_card"` + User userprofile.UserProfileResponseDTO `json:"user"` +} + func FormatResponseIdentityCard(identityCard *model.IdentityCard) (*ResponseIdentityCardDTO, error) { createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt) updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt) @@ -93,7 +106,7 @@ func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *mu } defer dst.Close() - if _, err := dst.ReadFrom(src); err != nil { + if _, err := io.Copy(dst, src); err != nil { return "", fmt.Errorf("failed to save card photo: %v", err) } @@ -123,16 +136,7 @@ func deleteIdentityCardImage(imagePath string) error { return nil } -func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) { - - // Validate essential parameters - if userID == "" { - return nil, fmt.Errorf("userID cannot be empty") - } - - if request.DeviceID == "" { - return nil, fmt.Errorf("deviceID cannot be empty") - } +func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID, deviceID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) { cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto) if err != nil { @@ -172,33 +176,13 @@ func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID str return nil, fmt.Errorf("failed to find user: %v", err) } - // Validate user data if user.Role.RoleName == "" { return nil, fmt.Errorf("user role not found") } - roleName := strings.ToLower(user.Role.RoleName) - - // Determine new registration status and progress - var newRegistrationStatus string - var newRegistrationProgress int - - switch roleName { - case "pengepul": - newRegistrationProgress = 2 - newRegistrationStatus = utils.RegStatusPending - case "pengelola": - newRegistrationProgress = 2 - newRegistrationStatus = user.RegistrationStatus - default: - newRegistrationProgress = int(user.RegistrationProgress) - newRegistrationStatus = user.RegistrationStatus - } - - // Update user registration progress and status updates := map[string]interface{}{ - "registration_progress": newRegistrationProgress, - "registration_status": newRegistrationStatus, + "registration_progress": utils.ProgressDataSubmitted, + "registration_status": utils.RegStatusPending, } err = s.authRepo.PatchUser(ctx, userID, updates) @@ -206,34 +190,46 @@ func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID str return nil, fmt.Errorf("failed to update user: %v", err) } - // Debug logging before token generation + updated, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, userprofile.ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get updated user: %w", err) + } + log.Printf("Token Generation Parameters:") log.Printf("- UserID: '%s'", user.ID) log.Printf("- Role: '%s'", user.Role.RoleName) - log.Printf("- DeviceID: '%s'", request.DeviceID) - log.Printf("- Registration Status: '%s'", newRegistrationStatus) + log.Printf("- DeviceID: '%s'", deviceID) + log.Printf("- Registration Status: '%s'", utils.RegStatusPending) - // Generate token pair with updated status tokenResponse, err := utils.GenerateTokenPair( - user.ID, - user.Role.RoleName, - request.DeviceID, - newRegistrationStatus, - newRegistrationProgress, + updated.ID, + updated.Role.RoleName, + deviceID, + updated.RegistrationStatus, + int(updated.RegistrationProgress), ) if err != nil { log.Printf("GenerateTokenPair error: %v", err) return nil, fmt.Errorf("failed to generate token: %v", err) } + nextStep := utils.GetNextRegistrationStep( + updated.Role.RoleName, + int(updated.RegistrationProgress), + updated.RegistrationStatus, + ) + return &authentication.AuthResponse{ Message: "identity card berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam", AccessToken: tokenResponse.AccessToken, RefreshToken: tokenResponse.RefreshToken, TokenType: string(tokenResponse.TokenType), ExpiresIn: tokenResponse.ExpiresIn, - RegistrationStatus: newRegistrationStatus, - NextStep: tokenResponse.NextStep, + RegistrationStatus: updated.RegistrationStatus, + NextStep: nextStep, SessionID: tokenResponse.SessionID, }, nil } @@ -319,3 +315,164 @@ func (s *identityCardService) UpdateIdentityCard(ctx context.Context, userID str return idcardResponseDTO, nil } + +func (s *identityCardService) GetAllIdentityCardsByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseIdentityCardDTO, error) { + identityCards, err := s.authRepo.GetIdentityCardsByUserRegStatus(ctx, userRegStatus) + if err != nil { + log.Printf("Error getting identity cards by registration status: %v", err) + return nil, fmt.Errorf("failed to get identity cards: %w", err) + } + + var response []ResponseIdentityCardDTO + for _, card := range identityCards { + createdAt, _ := utils.FormatDateToIndonesianFormat(card.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(card.UpdatedAt) + dto := ResponseIdentityCardDTO{ + ID: card.ID, + UserID: card.UserID, + Identificationumber: card.Identificationumber, + Placeofbirth: card.Placeofbirth, + Dateofbirth: card.Dateofbirth, + Gender: card.Gender, + BloodType: card.BloodType, + Province: card.Province, + District: card.District, + SubDistrict: card.SubDistrict, + Hamlet: card.Hamlet, + Village: card.Village, + Neighbourhood: card.Neighbourhood, + PostalCode: card.PostalCode, + Religion: card.Religion, + Maritalstatus: card.Maritalstatus, + Job: card.Job, + Citizenship: card.Citizenship, + Validuntil: card.Validuntil, + Cardphoto: card.Cardphoto, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + response = append(response, dto) + } + + return response, nil +} + +func (s *identityCardService) UpdateUserRegistrationStatusByIdentityCard(ctx context.Context, identityCardUserID string, newStatus string) error { + + user, err := s.authRepo.FindUserByID(ctx, identityCardUserID) + if err != nil { + log.Printf("Error finding user by ID %s: %v", identityCardUserID, err) + return fmt.Errorf("user not found: %w", err) + } + + updates := map[string]interface{}{ + "registration_status": newStatus, + "updated_at": time.Now(), + } + + switch newStatus { + case utils.RegStatusConfirmed: + updates["registration_progress"] = utils.ProgressDataSubmitted + + identityCards, err := s.GetIdentityCardsByUserID(ctx, identityCardUserID) + if err != nil { + log.Printf("Error fetching identity cards for user ID %s: %v", identityCardUserID, err) + return fmt.Errorf("failed to fetch identity card data: %w", err) + } + + if len(identityCards) == 0 { + log.Printf("No identity card found for user ID %s", identityCardUserID) + return fmt.Errorf("no identity card found for user") + } + + identityCard := identityCards[0] + + updates["name"] = identityCard.Fullname + updates["gender"] = identityCard.Gender + updates["dateofbirth"] = identityCard.Dateofbirth + updates["placeofbirth"] = identityCard.District + + log.Printf("Syncing user data for ID %s: name=%s, gender=%s, dob=%s, pob=%s", + identityCardUserID, identityCard.Fullname, identityCard.Gender, + identityCard.Dateofbirth, identityCard.District) + + case utils.RegStatusRejected: + updates["registration_progress"] = utils.ProgressOTPVerified + + } + + err = s.authRepo.PatchUser(ctx, user.ID, updates) + if err != nil { + log.Printf("Error updating user registration status for user ID %s: %v", user.ID, err) + return fmt.Errorf("failed to update user registration status: %w", err) + } + + log.Printf("Successfully updated registration status for user ID %s to %s", user.ID, newStatus) + + if newStatus == utils.RegStatusConfirmed { + log.Printf("User profile data synced successfully for user ID %s", user.ID) + } + + return nil +} + +func (s *identityCardService) mapIdentityCardToDTO(card model.IdentityCard) ResponseIdentityCardDTO { + createdAt, _ := utils.FormatDateToIndonesianFormat(card.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(card.UpdatedAt) + return ResponseIdentityCardDTO{ + ID: card.ID, + UserID: card.UserID, + Identificationumber: card.Identificationumber, + Placeofbirth: card.Placeofbirth, + Dateofbirth: card.Dateofbirth, + Gender: card.Gender, + BloodType: card.BloodType, + Province: card.Province, + District: card.District, + SubDistrict: card.SubDistrict, + Hamlet: card.Hamlet, + Village: card.Village, + Neighbourhood: card.Neighbourhood, + PostalCode: card.PostalCode, + Religion: card.Religion, + Maritalstatus: card.Maritalstatus, + Job: card.Job, + Citizenship: card.Citizenship, + Validuntil: card.Validuntil, + Cardphoto: card.Cardphoto, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} + +func (s *identityCardService) mapUserToDTO(user model.User) userprofile.UserProfileResponseDTO { + avatar := "" + if user.Avatar != nil { + avatar = *user.Avatar + } + + var roleDTO role.RoleResponseDTO + if user.Role != nil { + roleDTO = role.RoleResponseDTO{ + ID: user.Role.ID, + RoleName: user.Role.RoleName, + } + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + return userprofile.UserProfileResponseDTO{ + ID: user.ID, + Avatar: avatar, + Name: user.Name, + Gender: user.Gender, + Dateofbirth: user.Dateofbirth, + Placeofbirth: user.Placeofbirth, + Phone: user.Phone, + Email: user.Email, + PhoneVerified: user.PhoneVerified, + Role: roleDTO, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} diff --git a/internal/userpin/userpin_dto.go b/internal/userpin/userpin_dto.go index 5370ea6..c3a3302 100644 --- a/internal/userpin/userpin_dto.go +++ b/internal/userpin/userpin_dto.go @@ -6,7 +6,7 @@ import ( ) type RequestPinDTO struct { - DeviceId string `json:"device_id"` + // DeviceId string `json:"device_id"` Pin string `json:"userpin"` } diff --git a/internal/userpin/userpin_handler.go b/internal/userpin/userpin_handler.go index 518551e..eced2fd 100644 --- a/internal/userpin/userpin_handler.go +++ b/internal/userpin/userpin_handler.go @@ -15,47 +15,39 @@ func NewUserPinHandler(service UserPinService) *UserPinHandler { return &UserPinHandler{service} } -// userID, ok := c.Locals("user_id").(string) -// -// if !ok || userID == "" { -// return utils.Unauthorized(c, "user_id is missing or invalid") -// } func (h *UserPinHandler) CreateUserPinHandler(c *fiber.Ctx) error { - // Ambil klaim pengguna yang sudah diautentikasi + claims, err := middleware.GetUserFromContext(c) if err != nil { - return err + return utils.Unauthorized(c, "Authentication required") + } + + if claims.UserID == "" || claims.DeviceID == "" { + return utils.BadRequest(c, "Invalid user claims") } - // Parsing body request untuk PIN var req RequestPinDTO if err := c.BodyParser(&req); err != nil { return utils.BadRequest(c, "Invalid request body") } - // Validasi request PIN if errs, ok := req.ValidateRequestPinDTO(); !ok { return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation error", errs) } - // Panggil service untuk membuat PIN - err = h.service.CreateUserPin(c.Context(), claims.UserID, &req) + pintokenresponse, err := h.service.CreateUserPin(c.Context(), claims.UserID, claims.DeviceID, &req) if err != nil { - if err.Error() == "PIN already created" { - return utils.BadRequest(c, err.Error()) // Jika PIN sudah ada, kembalikan error 400 + if err.Error() == Pinhasbeencreated { + return utils.BadRequest(c, err.Error()) } - return utils.InternalServerError(c, err.Error()) // Jika terjadi error lain, internal server error + return utils.InternalServerError(c, err.Error()) } - // Mengembalikan response sukses jika berhasil - return utils.Success(c, "PIN created successfully") + return utils.SuccessWithData(c, "PIN created successfully", pintokenresponse) } func (h *UserPinHandler) VerifyPinHandler(c *fiber.Ctx) error { - // userID, ok := c.Locals("user_id").(string) - // if !ok || userID == "" { - // return utils.Unauthorized(c, "user_id is missing or invalid") - // } + claims, err := middleware.GetUserFromContext(c) if err != nil { return err @@ -66,12 +58,10 @@ func (h *UserPinHandler) VerifyPinHandler(c *fiber.Ctx) error { return utils.BadRequest(c, "Invalid request body") } - token, err := h.service.VerifyUserPin(c.Context(), claims.UserID, &req) + token, err := h.service.VerifyUserPin(c.Context(), claims.UserID, claims.DeviceID, &req) if err != nil { return utils.BadRequest(c, err.Error()) } - return utils.SuccessWithData(c, "PIN verified successfully", fiber.Map{ - "token": token, - }) + return utils.SuccessWithData(c, "PIN verified successfully", token) } diff --git a/internal/userpin/userpin_route.go b/internal/userpin/userpin_route.go index 81f4dc4..3c34411 100644 --- a/internal/userpin/userpin_route.go +++ b/internal/userpin/userpin_route.go @@ -3,6 +3,7 @@ package userpin import ( "rijig/config" "rijig/internal/authentication" + "rijig/internal/userprofile" "rijig/middleware" "github.com/gofiber/fiber/v2" @@ -11,8 +12,9 @@ import ( func UsersPinRoute(api fiber.Router) { userPinRepo := NewUserPinRepository(config.DB) authRepo := authentication.NewAuthenticationRepository(config.DB) + userprofileRepo := userprofile.NewUserProfileRepository(config.DB) - userPinService := NewUserPinService(userPinRepo, authRepo) + userPinService := NewUserPinService(userPinRepo, authRepo, userprofileRepo) userPinHandler := NewUserPinHandler(userPinService) diff --git a/internal/userpin/userpin_service.go b/internal/userpin/userpin_service.go index fd6415f..1c663b2 100644 --- a/internal/userpin/userpin_service.go +++ b/internal/userpin/userpin_service.go @@ -2,45 +2,51 @@ package userpin import ( "context" + "errors" "fmt" "rijig/internal/authentication" + "rijig/internal/userprofile" "rijig/model" "rijig/utils" "strings" + + "gorm.io/gorm" ) type UserPinService interface { - CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error - VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error) + CreateUserPin(ctx context.Context, userID, deviceId string, dto *RequestPinDTO) (*authentication.AuthResponse, error) + VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error) } type userPinService struct { - UserPinRepo UserPinRepository - authRepo authentication.AuthenticationRepository + UserPinRepo UserPinRepository + authRepo authentication.AuthenticationRepository + userProfileRepo userprofile.UserProfileRepository } func NewUserPinService(UserPinRepo UserPinRepository, - authRepo authentication.AuthenticationRepository) UserPinService { - return &userPinService{UserPinRepo, authRepo} + authRepo authentication.AuthenticationRepository, + userProfileRepo userprofile.UserProfileRepository) UserPinService { + return &userPinService{UserPinRepo, authRepo, userProfileRepo} } -func (s *userPinService) CreateUserPin(ctx context.Context, userID string, dto *RequestPinDTO) error { +var ( + Pinhasbeencreated = "PIN already created" +) - if errs, ok := dto.ValidateRequestPinDTO(); !ok { - return fmt.Errorf("validation error: %v", errs) - } +func (s *userPinService) CreateUserPin(ctx context.Context, userID, deviceId string, dto *RequestPinDTO) (*authentication.AuthResponse, error) { - existingPin, err := s.UserPinRepo.FindByUserID(ctx, userID) + _, err := s.UserPinRepo.FindByUserID(ctx, userID) if err != nil { - return fmt.Errorf("failed to check existing PIN: %w", err) - } - if existingPin != nil { - return fmt.Errorf("PIN already created") + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("%v", Pinhasbeencreated) } hashed, err := utils.HashingPlainText(dto.Pin) if err != nil { - return fmt.Errorf("failed to hash PIN: %w", err) + return nil, fmt.Errorf("failed to hash PIN: %w", err) } userPin := &model.UserPin{ @@ -49,35 +55,63 @@ func (s *userPinService) CreateUserPin(ctx context.Context, userID string, dto * } if err := s.UserPinRepo.Create(ctx, userPin); err != nil { - return fmt.Errorf("failed to create PIN: %w", err) - } - - user, err := s.authRepo.FindUserByID(ctx, userID) - if err != nil { - return fmt.Errorf("user not found") - } - - roleName := strings.ToLower(user.Role.RoleName) - - progress := authentication.IsRegistrationComplete(roleName, int(user.RegistrationProgress)) - // progress := utils.GetNextRegistrationStep(roleName, int(user.RegistrationProgress)) - // progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress) - // progress := utils.GetNextRegistrationStep(roleName, user.RegistrationProgress) - - if !progress { - err = s.authRepo.PatchUser(ctx, userID, map[string]interface{}{ - "registration_progress": int(user.RegistrationProgress) + 1, - "registration_status": utils.RegStatusComplete, - }) - if err != nil { - return fmt.Errorf("failed to update user progress: %w", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user not found") } + return nil, fmt.Errorf("failed to create pin: %w", err) } - return nil + updates := map[string]interface{}{ + "registration_progress": utils.ProgressComplete, + "registration_status": utils.RegStatusComplete, + } + + if err = s.authRepo.PatchUser(ctx, userID, updates); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to update user profile: %w", err) + } + + updated, err := s.userProfileRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, userprofile.ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get updated user: %w", err) + } + + tokenResponse, err := utils.GenerateTokenPair( + updated.ID, + updated.Role.RoleName, + deviceId, + updated.RegistrationStatus, + int(updated.RegistrationProgress), + ) + + if err != nil { + return nil, fmt.Errorf("gagal generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep( + updated.Role.RoleName, + int(updated.RegistrationProgress), + updated.RegistrationStatus, + ) + + return &authentication.AuthResponse{ + Message: "Isi data diri berhasil", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: updated.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil } -func (s *userPinService) VerifyUserPin(ctx context.Context, userID string, pin *RequestPinDTO) (*utils.TokenResponse, error) { +func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error) { user, err := s.authRepo.FindUserByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found") @@ -93,5 +127,5 @@ func (s *userPinService) VerifyUserPin(ctx context.Context, userID string, pin * } roleName := strings.ToLower(user.Role.RoleName) - return utils.GenerateTokenPair(user.ID, roleName, pin.DeviceId, user.RegistrationStatus, int(user.RegistrationProgress)) + return utils.GenerateTokenPair(user.ID, roleName, deviceID, user.RegistrationStatus, int(user.RegistrationProgress)) } diff --git a/internal/userprofile/userprofile_handler.go b/internal/userprofile/userprofile_handler.go index 27ac07d..05cc96d 100644 --- a/internal/userprofile/userprofile_handler.go +++ b/internal/userprofile/userprofile_handler.go @@ -1 +1,76 @@ -package userprofile \ No newline at end of file +package userprofile + +import ( + "context" + "log" + "rijig/middleware" + "rijig/utils" + "strings" + "time" + + "github.com/gofiber/fiber/v2" +) + +type UserProfileHandler struct { + service UserProfileService +} + +func NewUserProfileHandler(service UserProfileService) *UserProfileHandler { + return &UserProfileHandler{ + service: service, + } +} + +func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { + + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + userProfile, err := h.service.GetUserProfile(ctx, claims.UserID) + if err != nil { + if strings.Contains(err.Error(), ErrUserNotFound.Error()) { + return utils.NotFound(c, "User profile not found") + } + + log.Printf("Error getting user profile: %v", err) + return utils.InternalServerError(c, "Failed to retrieve user profile") + } + + return utils.SuccessWithData(c, "User profile retrieved successfully", userProfile) +} + +func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + var req RequestUserProfileDTO + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + if validationErrors, isValid := req.ValidateRequestUserProfileDTO(); !isValid { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", validationErrors) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + updatedProfile, err := h.service.UpdateRegistUserProfile(ctx, claims.UserID, claims.DeviceID, &req) + if err != nil { + + if strings.Contains(err.Error(), "user not found") { + return utils.NotFound(c, "User not found") + } + + log.Printf("Error updating user profile: %v", err) + return utils.InternalServerError(c, "Failed to update user profile") + } + + return utils.SuccessWithData(c, "User profile updated successfully", updatedProfile) +} diff --git a/internal/userprofile/userprofile_repo.go b/internal/userprofile/userprofile_repo.go index 052c798..926950f 100644 --- a/internal/userprofile/userprofile_repo.go +++ b/internal/userprofile/userprofile_repo.go @@ -2,34 +2,106 @@ package userprofile import ( "context" + "errors" "rijig/model" "gorm.io/gorm" ) -type AuthenticationRepository interface { - UpdateUser(ctx context.Context, user *model.User) error - PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error +type UserProfileRepository interface { + GetByID(ctx context.Context, userID string) (*model.User, error) + GetByRoleName(ctx context.Context, roleName string) ([]*model.User, error) + // GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) + // GetCompanyProfileByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) + Update(ctx context.Context, userID string, user *model.User) error } -type authenticationRepository struct { +type userProfileRepository struct { db *gorm.DB } -func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository { - return &authenticationRepository{db} +func NewUserProfileRepository(db *gorm.DB) UserProfileRepository { + return &userProfileRepository{ + db: db, + } } -func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error { - return r.db.WithContext(ctx). - Model(&model.User{}). - Where("id = ?", user.ID). - Updates(user).Error +func (r *userProfileRepository) GetByID(ctx context.Context, userID string) (*model.User, error) { + var user model.User + + err := r.db.WithContext(ctx). + Preload("Role"). + Where("id = ?", userID). + First(&user).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserNotFound + } + return nil, err + } + + return &user, nil } -func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error { - return r.db.WithContext(ctx). +func (r *userProfileRepository) GetByRoleName(ctx context.Context, roleName string) ([]*model.User, error) { + var users []*model.User + + err := r.db.WithContext(ctx). + Preload("Role"). + Joins("JOIN roles ON users.role_id = roles.id"). + Where("roles.role_name = ?", roleName). + Find(&users).Error + + if err != nil { + return nil, err + } + + return users, nil +} + +/* func (r *userProfileRepository) GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) { + var identityCards []model.IdentityCard + + if err := r.db.WithContext(ctx). + Joins("JOIN users ON identity_cards.user_id = users.id"). + Where("users.registration_status = ?", userRegStatus). + Preload("User"). + Find(&identityCards).Error; err != nil { + log.Printf("Error fetching identity cards by user registration status: %v", err) + return nil, fmt.Errorf("error fetching identity cards by user registration status: %w", err) + } + + return identityCards, nil +} + +func (r *userProfileRepository) GetCompanyProfileByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) { + var identityCards []model.IdentityCard + + if err := r.db.WithContext(ctx). + Joins("JOIN users ON company_profiles.user_id = users.id"). + Where("users.registration_status = ?", userRegStatus). + Preload("User"). + Find(&identityCards).Error; err != nil { + log.Printf("Error fetching identity cards by user registration status: %v", err) + return nil, fmt.Errorf("error fetching identity cards by user registration status: %w", err) + } + return identityCards, nil +} */ + +func (r *userProfileRepository) Update(ctx context.Context, userID string, user *model.User) error { + result := r.db.WithContext(ctx). Model(&model.User{}). Where("id = ?", userID). - Updates(updates).Error + Updates(user) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return ErrUserNotFound + } + + return nil } diff --git a/internal/userprofile/userprofile_route.go b/internal/userprofile/userprofile_route.go index 27ac07d..e011fa4 100644 --- a/internal/userprofile/userprofile_route.go +++ b/internal/userprofile/userprofile_route.go @@ -1 +1,20 @@ -package userprofile \ No newline at end of file +package userprofile + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func UserProfileRouter(api fiber.Router) { + userProfileRepo := NewUserProfileRepository(config.DB) + userProfileService := NewUserProfileService(userProfileRepo) + userProfileHandler := NewUserProfileHandler(userProfileService) + + userRoute := api.Group("/userprofile") + userRoute.Use(middleware.AuthMiddleware()) + + userRoute.Get("/", userProfileHandler.GetUserProfile) + userRoute.Put("/update", userProfileHandler.UpdateUserProfile) +} \ No newline at end of file diff --git a/internal/userprofile/userprofile_service.go b/internal/userprofile/userprofile_service.go index 27ac07d..dd405d1 100644 --- a/internal/userprofile/userprofile_service.go +++ b/internal/userprofile/userprofile_service.go @@ -1 +1,160 @@ -package userprofile \ No newline at end of file +package userprofile + +import ( + "context" + "errors" + "fmt" + "rijig/internal/authentication" + "rijig/internal/role" + "rijig/model" + "rijig/utils" + "time" +) + +var ( + ErrUserNotFound = errors.New("user tidak ditemukan") +) + +type UserProfileService interface { + GetUserProfile(ctx context.Context, userID string) (*UserProfileResponseDTO, error) + UpdateRegistUserProfile(ctx context.Context, userID, deviceId string, req *RequestUserProfileDTO) (*authentication.AuthResponse, error) +} + +type userProfileService struct { + repo UserProfileRepository +} + +func NewUserProfileService(repo UserProfileRepository) UserProfileService { + return &userProfileService{ + repo: repo, + } +} + +func (s *userProfileService) GetUserProfile(ctx context.Context, userID string) (*UserProfileResponseDTO, error) { + user, err := s.repo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get user profile: %w", err) + } + + return s.mapToResponseDTO(user), nil +} + +func (s *userProfileService) UpdateRegistUserProfile(ctx context.Context, userID, deviceId string, req *RequestUserProfileDTO) (*authentication.AuthResponse, error) { + + _, err := s.repo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get user: %w", err) + } + + updateUser := &model.User{ + Name: req.Name, + Gender: req.Gender, + Dateofbirth: req.Dateofbirth, + Placeofbirth: req.Placeofbirth, + Phone: req.Phone, + RegistrationProgress: utils.ProgressDataSubmitted, + } + + if err := s.repo.Update(ctx, userID, updateUser); err != nil { + if errors.Is(err, ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to update user profile: %w", err) + } + + updatedUser, err := s.repo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get updated user: %w", err) + } + + tokenResponse, err := utils.GenerateTokenPair( + updatedUser.ID, + updatedUser.Role.RoleName, + // req.DeviceID, + deviceId, + updatedUser.RegistrationStatus, + int(updatedUser.RegistrationProgress), + ) + + if err != nil { + return nil, fmt.Errorf("gagal generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep( + updatedUser.Role.RoleName, + int(updatedUser.RegistrationProgress), + updateUser.RegistrationStatus, + ) + + return &authentication.AuthResponse{ + Message: "Isi data diri berhasil", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: updateUser.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil + + // return s.mapToResponseDTO(updatedUser), nil +} + +func (s *userProfileService) mapToResponseDTO(user *model.User) *UserProfileResponseDTO { + + createdAt, err := utils.FormatDateToIndonesianFormat(user.CreatedAt) + if err != nil { + createdAt = user.CreatedAt.Format(time.RFC3339) + } + + updatedAt, err := utils.FormatDateToIndonesianFormat(user.UpdatedAt) + if err != nil { + updatedAt = user.UpdatedAt.Format(time.RFC3339) + } + + response := &UserProfileResponseDTO{ + ID: user.ID, + Name: user.Name, + Gender: user.Gender, + Dateofbirth: user.Dateofbirth, + Placeofbirth: user.Placeofbirth, + Phone: user.Phone, + PhoneVerified: user.PhoneVerified, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + if user.Avatar != nil { + response.Avatar = *user.Avatar + } + + if user.Role != nil { + roleCreatedAt, err := utils.FormatDateToIndonesianFormat(user.Role.CreatedAt) + if err != nil { + roleCreatedAt = user.Role.CreatedAt.Format(time.RFC3339) + } + + roleUpdatedAt, err := utils.FormatDateToIndonesianFormat(user.Role.UpdatedAt) + if err != nil { + roleUpdatedAt = user.Role.UpdatedAt.Format(time.RFC3339) + } + + response.Role = role.RoleResponseDTO{ + ID: user.Role.ID, + RoleName: user.Role.RoleName, + CreatedAt: roleCreatedAt, + UpdatedAt: roleUpdatedAt, + } + } + + return response +} diff --git a/internal/wilayahindo/wilayahindo_handler.go b/internal/wilayahindo/wilayahindo_handler.go index 2445724..cf23a6e 100644 --- a/internal/wilayahindo/wilayahindo_handler.go +++ b/internal/wilayahindo/wilayahindo_handler.go @@ -1 +1,272 @@ -package wilayahindo \ No newline at end of file +package wilayahindo + +import ( + "strconv" + "strings" + + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type WilayahIndonesiaHandler struct { + WilayahService WilayahIndonesiaService +} + +func NewWilayahIndonesiaHandler(wilayahService WilayahIndonesiaService) *WilayahIndonesiaHandler { + return &WilayahIndonesiaHandler{ + WilayahService: wilayahService, + } +} + +func (h *WilayahIndonesiaHandler) ImportDataFromCSV(c *fiber.Ctx) error { + ctx := c.Context() + + if err := h.WilayahService.ImportDataFromCSV(ctx); err != nil { + return utils.InternalServerError(c, "Failed to import data from CSV: "+err.Error()) + } + + return utils.Success(c, "Data imported successfully from CSV") +} + +func (h *WilayahIndonesiaHandler) GetAllProvinces(c *fiber.Ctx) error { + ctx := c.Context() + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + provinces, total, err := h.WilayahService.GetAllProvinces(ctx, page, limit) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch provinces: "+err.Error()) + } + + response := map[string]interface{}{ + "provinces": provinces, + "total": total, + } + + return utils.SuccessWithPagination(c, "Provinces retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetProvinceByID(c *fiber.Ctx) error { + ctx := c.Context() + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Province ID is required") + } + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + province, totalRegencies, err := h.WilayahService.GetProvinceByID(ctx, id, page, limit) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Province not found") + } + return utils.InternalServerError(c, "Failed to fetch province: "+err.Error()) + } + + response := map[string]interface{}{ + "province": province, + "total_regencies": totalRegencies, + } + + return utils.SuccessWithPagination(c, "Province retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetAllRegencies(c *fiber.Ctx) error { + ctx := c.Context() + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + regencies, total, err := h.WilayahService.GetAllRegencies(ctx, page, limit) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch regencies: "+err.Error()) + } + + response := map[string]interface{}{ + "regencies": regencies, + "total": total, + } + + return utils.SuccessWithPagination(c, "Regencies retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetRegencyByID(c *fiber.Ctx) error { + ctx := c.Context() + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Regency ID is required") + } + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + regency, totalDistricts, err := h.WilayahService.GetRegencyByID(ctx, id, page, limit) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Regency not found") + } + return utils.InternalServerError(c, "Failed to fetch regency: "+err.Error()) + } + + response := map[string]interface{}{ + "regency": regency, + "total_districts": totalDistricts, + } + + return utils.SuccessWithPagination(c, "Regency retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetAllDistricts(c *fiber.Ctx) error { + ctx := c.Context() + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + districts, total, err := h.WilayahService.GetAllDistricts(ctx, page, limit) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch districts: "+err.Error()) + } + + response := map[string]interface{}{ + "districts": districts, + "total": total, + } + + return utils.SuccessWithPagination(c, "Districts retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetDistrictByID(c *fiber.Ctx) error { + ctx := c.Context() + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "District ID is required") + } + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + district, totalVillages, err := h.WilayahService.GetDistrictByID(ctx, id, page, limit) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "District not found") + } + return utils.InternalServerError(c, "Failed to fetch district: "+err.Error()) + } + + response := map[string]interface{}{ + "district": district, + "total_villages": totalVillages, + } + + return utils.SuccessWithPagination(c, "District retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetAllVillages(c *fiber.Ctx) error { + ctx := c.Context() + + page, limit, err := h.parsePaginationParams(c) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + villages, total, err := h.WilayahService.GetAllVillages(ctx, page, limit) + if err != nil { + return utils.InternalServerError(c, "Failed to fetch villages: "+err.Error()) + } + + response := map[string]interface{}{ + "villages": villages, + "total": total, + } + + return utils.SuccessWithPagination(c, "Villages retrieved successfully", response, page, limit) +} + +func (h *WilayahIndonesiaHandler) GetVillageByID(c *fiber.Ctx) error { + ctx := c.Context() + + id := c.Params("id") + if id == "" { + return utils.BadRequest(c, "Village ID is required") + } + + village, err := h.WilayahService.GetVillageByID(ctx, id) + if err != nil { + if strings.Contains(err.Error(), "not found") { + return utils.NotFound(c, "Village not found") + } + return utils.InternalServerError(c, "Failed to fetch village: "+err.Error()) + } + + return utils.SuccessWithData(c, "Village retrieved successfully", village) +} + +func (h *WilayahIndonesiaHandler) parsePaginationParams(c *fiber.Ctx) (int, int, error) { + + page := 1 + limit := 10 + + if pageStr := c.Query("page"); pageStr != "" { + parsedPage, err := strconv.Atoi(pageStr) + if err != nil { + return 0, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid page parameter") + } + if parsedPage < 1 { + return 0, 0, fiber.NewError(fiber.StatusBadRequest, "Page must be greater than 0") + } + page = parsedPage + } + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid limit parameter") + } + if parsedLimit < 1 { + return 0, 0, fiber.NewError(fiber.StatusBadRequest, "Limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fiber.NewError(fiber.StatusBadRequest, "Limit cannot exceed 100") + } + limit = parsedLimit + } + + return page, limit, nil +} + +func (h *WilayahIndonesiaHandler) SetupRoutes(app *fiber.App) { + + api := app.Group("/api/v1/wilayah") + + api.Post("/import", h.ImportDataFromCSV) + + api.Get("/provinces", h.GetAllProvinces) + api.Get("/provinces/:id", h.GetProvinceByID) + + api.Get("/regencies", h.GetAllRegencies) + api.Get("/regencies/:id", h.GetRegencyByID) + + api.Get("/districts", h.GetAllDistricts) + api.Get("/districts/:id", h.GetDistrictByID) + + api.Get("/villages", h.GetAllVillages) + api.Get("/villages/:id", h.GetVillageByID) +} diff --git a/internal/wilayahindo/wilayahindo_route.go b/internal/wilayahindo/wilayahindo_route.go index 2445724..7d11403 100644 --- a/internal/wilayahindo/wilayahindo_route.go +++ b/internal/wilayahindo/wilayahindo_route.go @@ -1 +1,32 @@ -package wilayahindo \ No newline at end of file +package wilayahindo + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func WilayahRouter(api fiber.Router) { + + wilayahRepo := NewWilayahIndonesiaRepository(config.DB) + wilayahService := NewWilayahIndonesiaService(wilayahRepo) + wilayahHandler := NewWilayahIndonesiaHandler(wilayahService) + + api.Post("/import/data-wilayah-indonesia", middleware.RequireAdminRole(), wilayahHandler.ImportDataFromCSV) + + wilayahAPI := api.Group("/wilayah-indonesia") + + wilayahAPI.Get("/provinces", wilayahHandler.GetAllProvinces) + wilayahAPI.Get("/provinces/:provinceid", wilayahHandler.GetProvinceByID) + + wilayahAPI.Get("/regencies", wilayahHandler.GetAllRegencies) + wilayahAPI.Get("/regencies/:regencyid", wilayahHandler.GetRegencyByID) + + wilayahAPI.Get("/districts", wilayahHandler.GetAllDistricts) + wilayahAPI.Get("/districts/:districtid", wilayahHandler.GetDistrictByID) + + wilayahAPI.Get("/villages", wilayahHandler.GetAllVillages) + wilayahAPI.Get("/villages/:villageid", wilayahHandler.GetVillageByID) + +} diff --git a/internal/wilayahindo/wilayahindo_service.go b/internal/wilayahindo/wilayahindo_service.go index 492b54f..b1a0407 100644 --- a/internal/wilayahindo/wilayahindo_service.go +++ b/internal/wilayahindo/wilayahindo_service.go @@ -5,7 +5,6 @@ import ( "fmt" "time" - "rijig/dto" "rijig/model" "rijig/utils" ) @@ -13,17 +12,17 @@ import ( type WilayahIndonesiaService interface { ImportDataFromCSV(ctx context.Context) error - GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error) - GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) + GetAllProvinces(ctx context.Context, page, limit int) ([]ProvinceResponseDTO, int, error) + GetProvinceByID(ctx context.Context, id string, page, limit int) (*ProvinceResponseDTO, int, error) - GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error) - GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error) + GetAllRegencies(ctx context.Context, page, limit int) ([]RegencyResponseDTO, int, error) + GetRegencyByID(ctx context.Context, id string, page, limit int) (*RegencyResponseDTO, int, error) - GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error) - GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error) + GetAllDistricts(ctx context.Context, page, limit int) ([]DistrictResponseDTO, int, error) + GetDistrictByID(ctx context.Context, id string, page, limit int) (*DistrictResponseDTO, int, error) - GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error) - GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error) + GetAllVillages(ctx context.Context, page, limit int) ([]VillageResponseDTO, int, error) + GetVillageByID(ctx context.Context, id string) (*VillageResponseDTO, error) } type wilayahIndonesiaService struct { @@ -122,11 +121,11 @@ func (s *wilayahIndonesiaService) ImportDataFromCSV(ctx context.Context) error { return nil } -func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, limit int) ([]dto.ProvinceResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, limit int) ([]ProvinceResponseDTO, int, error) { cacheKey := fmt.Sprintf("provinces_page:%d_limit:%d", page, limit) var cachedResponse struct { - Data []dto.ProvinceResponseDTO `json:"data"` + Data []ProvinceResponseDTO `json:"data"` Total int `json:"total"` } @@ -139,16 +138,16 @@ func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, lim return nil, 0, fmt.Errorf("failed to fetch provinces: %w", err) } - provinceDTOs := make([]dto.ProvinceResponseDTO, len(provinces)) + provinceDTOs := make([]ProvinceResponseDTO, len(provinces)) for i, province := range provinces { - provinceDTOs[i] = dto.ProvinceResponseDTO{ + provinceDTOs[i] = ProvinceResponseDTO{ ID: province.ID, Name: province.Name, } } cacheData := struct { - Data []dto.ProvinceResponseDTO `json:"data"` + Data []ProvinceResponseDTO `json:"data"` Total int `json:"total"` }{ Data: provinceDTOs, @@ -162,11 +161,11 @@ func (s *wilayahIndonesiaService) GetAllProvinces(ctx context.Context, page, lim return provinceDTOs, total, nil } -func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string, page, limit int) (*dto.ProvinceResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string, page, limit int) (*ProvinceResponseDTO, int, error) { cacheKey := fmt.Sprintf("province:%s_page:%d_limit:%d", id, page, limit) var cachedResponse struct { - Data dto.ProvinceResponseDTO `json:"data"` + Data ProvinceResponseDTO `json:"data"` TotalRegencies int `json:"total_regencies"` } @@ -179,14 +178,14 @@ func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string return nil, 0, err } - provinceDTO := dto.ProvinceResponseDTO{ + provinceDTO := ProvinceResponseDTO{ ID: province.ID, Name: province.Name, } - regencyDTOs := make([]dto.RegencyResponseDTO, len(province.Regencies)) + regencyDTOs := make([]RegencyResponseDTO, len(province.Regencies)) for i, regency := range province.Regencies { - regencyDTOs[i] = dto.RegencyResponseDTO{ + regencyDTOs[i] = RegencyResponseDTO{ ID: regency.ID, ProvinceID: regency.ProvinceID, Name: regency.Name, @@ -195,7 +194,7 @@ func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string provinceDTO.Regencies = regencyDTOs cacheData := struct { - Data dto.ProvinceResponseDTO `json:"data"` + Data ProvinceResponseDTO `json:"data"` TotalRegencies int `json:"total_regencies"` }{ Data: provinceDTO, @@ -209,11 +208,11 @@ func (s *wilayahIndonesiaService) GetProvinceByID(ctx context.Context, id string return &provinceDTO, totalRegencies, nil } -func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, limit int) ([]dto.RegencyResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, limit int) ([]RegencyResponseDTO, int, error) { cacheKey := fmt.Sprintf("regencies_page:%d_limit:%d", page, limit) var cachedResponse struct { - Data []dto.RegencyResponseDTO `json:"data"` + Data []RegencyResponseDTO `json:"data"` Total int `json:"total"` } @@ -226,9 +225,9 @@ func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, lim return nil, 0, fmt.Errorf("failed to fetch regencies: %w", err) } - regencyDTOs := make([]dto.RegencyResponseDTO, len(regencies)) + regencyDTOs := make([]RegencyResponseDTO, len(regencies)) for i, regency := range regencies { - regencyDTOs[i] = dto.RegencyResponseDTO{ + regencyDTOs[i] = RegencyResponseDTO{ ID: regency.ID, ProvinceID: regency.ProvinceID, Name: regency.Name, @@ -236,7 +235,7 @@ func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, lim } cacheData := struct { - Data []dto.RegencyResponseDTO `json:"data"` + Data []RegencyResponseDTO `json:"data"` Total int `json:"total"` }{ Data: regencyDTOs, @@ -250,11 +249,11 @@ func (s *wilayahIndonesiaService) GetAllRegencies(ctx context.Context, page, lim return regencyDTOs, total, nil } -func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, page, limit int) (*dto.RegencyResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, page, limit int) (*RegencyResponseDTO, int, error) { cacheKey := fmt.Sprintf("regency:%s_page:%d_limit:%d", id, page, limit) var cachedResponse struct { - Data dto.RegencyResponseDTO `json:"data"` + Data RegencyResponseDTO `json:"data"` TotalDistricts int `json:"total_districts"` } @@ -267,15 +266,15 @@ func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, return nil, 0, err } - regencyDTO := dto.RegencyResponseDTO{ + regencyDTO := RegencyResponseDTO{ ID: regency.ID, ProvinceID: regency.ProvinceID, Name: regency.Name, } - districtDTOs := make([]dto.DistrictResponseDTO, len(regency.Districts)) + districtDTOs := make([]DistrictResponseDTO, len(regency.Districts)) for i, district := range regency.Districts { - districtDTOs[i] = dto.DistrictResponseDTO{ + districtDTOs[i] = DistrictResponseDTO{ ID: district.ID, RegencyID: district.RegencyID, Name: district.Name, @@ -284,7 +283,7 @@ func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, regencyDTO.Districts = districtDTOs cacheData := struct { - Data dto.RegencyResponseDTO `json:"data"` + Data RegencyResponseDTO `json:"data"` TotalDistricts int `json:"total_districts"` }{ Data: regencyDTO, @@ -298,11 +297,11 @@ func (s *wilayahIndonesiaService) GetRegencyByID(ctx context.Context, id string, return ®encyDTO, totalDistricts, nil } -func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, limit int) ([]dto.DistrictResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, limit int) ([]DistrictResponseDTO, int, error) { cacheKey := fmt.Sprintf("districts_page:%d_limit:%d", page, limit) var cachedResponse struct { - Data []dto.DistrictResponseDTO `json:"data"` + Data []DistrictResponseDTO `json:"data"` Total int `json:"total"` } @@ -315,9 +314,9 @@ func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, lim return nil, 0, fmt.Errorf("failed to fetch districts: %w", err) } - districtDTOs := make([]dto.DistrictResponseDTO, len(districts)) + districtDTOs := make([]DistrictResponseDTO, len(districts)) for i, district := range districts { - districtDTOs[i] = dto.DistrictResponseDTO{ + districtDTOs[i] = DistrictResponseDTO{ ID: district.ID, RegencyID: district.RegencyID, Name: district.Name, @@ -325,7 +324,7 @@ func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, lim } cacheData := struct { - Data []dto.DistrictResponseDTO `json:"data"` + Data []DistrictResponseDTO `json:"data"` Total int `json:"total"` }{ Data: districtDTOs, @@ -339,11 +338,11 @@ func (s *wilayahIndonesiaService) GetAllDistricts(ctx context.Context, page, lim return districtDTOs, total, nil } -func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string, page, limit int) (*dto.DistrictResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string, page, limit int) (*DistrictResponseDTO, int, error) { cacheKey := fmt.Sprintf("district:%s_page:%d_limit:%d", id, page, limit) var cachedResponse struct { - Data dto.DistrictResponseDTO `json:"data"` + Data DistrictResponseDTO `json:"data"` TotalVillages int `json:"total_villages"` } @@ -356,15 +355,15 @@ func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string return nil, 0, err } - districtDTO := dto.DistrictResponseDTO{ + districtDTO := DistrictResponseDTO{ ID: district.ID, RegencyID: district.RegencyID, Name: district.Name, } - villageDTOs := make([]dto.VillageResponseDTO, len(district.Villages)) + villageDTOs := make([]VillageResponseDTO, len(district.Villages)) for i, village := range district.Villages { - villageDTOs[i] = dto.VillageResponseDTO{ + villageDTOs[i] = VillageResponseDTO{ ID: village.ID, DistrictID: village.DistrictID, Name: village.Name, @@ -373,7 +372,7 @@ func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string districtDTO.Villages = villageDTOs cacheData := struct { - Data dto.DistrictResponseDTO `json:"data"` + Data DistrictResponseDTO `json:"data"` TotalVillages int `json:"total_villages"` }{ Data: districtDTO, @@ -387,11 +386,11 @@ func (s *wilayahIndonesiaService) GetDistrictByID(ctx context.Context, id string return &districtDTO, totalVillages, nil } -func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limit int) ([]dto.VillageResponseDTO, int, error) { +func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limit int) ([]VillageResponseDTO, int, error) { cacheKey := fmt.Sprintf("villages_page:%d_limit:%d", page, limit) var cachedResponse struct { - Data []dto.VillageResponseDTO `json:"data"` + Data []VillageResponseDTO `json:"data"` Total int `json:"total"` } @@ -404,9 +403,9 @@ func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limi return nil, 0, fmt.Errorf("failed to fetch villages: %w", err) } - villageDTOs := make([]dto.VillageResponseDTO, len(villages)) + villageDTOs := make([]VillageResponseDTO, len(villages)) for i, village := range villages { - villageDTOs[i] = dto.VillageResponseDTO{ + villageDTOs[i] = VillageResponseDTO{ ID: village.ID, DistrictID: village.DistrictID, Name: village.Name, @@ -414,7 +413,7 @@ func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limi } cacheData := struct { - Data []dto.VillageResponseDTO `json:"data"` + Data []VillageResponseDTO `json:"data"` Total int `json:"total"` }{ Data: villageDTOs, @@ -428,10 +427,10 @@ func (s *wilayahIndonesiaService) GetAllVillages(ctx context.Context, page, limi return villageDTOs, total, nil } -func (s *wilayahIndonesiaService) GetVillageByID(ctx context.Context, id string) (*dto.VillageResponseDTO, error) { +func (s *wilayahIndonesiaService) GetVillageByID(ctx context.Context, id string) (*VillageResponseDTO, error) { cacheKey := fmt.Sprintf("village:%s", id) - var cachedResponse dto.VillageResponseDTO + var cachedResponse VillageResponseDTO if err := utils.GetCache(cacheKey, &cachedResponse); err == nil { return &cachedResponse, nil } @@ -441,7 +440,7 @@ func (s *wilayahIndonesiaService) GetVillageByID(ctx context.Context, id string) return nil, fmt.Errorf("village not found: %w", err) } - villageResponse := &dto.VillageResponseDTO{ + villageResponse := &VillageResponseDTO{ ID: village.ID, DistrictID: village.DistrictID, Name: village.Name, diff --git a/model/identitycard_model.go b/model/identitycard_model.go index c35cfc8..088e28f 100644 --- a/model/identitycard_model.go +++ b/model/identitycard_model.go @@ -7,6 +7,7 @@ type IdentityCard struct { UserID string `gorm:"not null" json:"userId"` User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` Identificationumber string `gorm:"not null" json:"identificationumber"` + Fullname string `gorm:"not null" json:"fullname"` Placeofbirth string `gorm:"not null" json:"placeofbirth"` Dateofbirth string `gorm:"not null" json:"dateofbirth"` Gender string `gorm:"not null" json:"gender"` diff --git a/public/document/districts.csv b/public/document/districts.csv old mode 100644 new mode 100755 diff --git a/public/document/provinces.csv b/public/document/provinces.csv old mode 100644 new mode 100755 diff --git a/public/document/regencies.csv b/public/document/regencies.csv old mode 100644 new mode 100755 diff --git a/public/document/villages.csv b/public/document/villages.csv old mode 100644 new mode 100755 diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 5d37e8a..d653c9c 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -9,7 +9,9 @@ import ( "rijig/internal/identitycart" "rijig/internal/role" "rijig/internal/userpin" + "rijig/internal/userprofile" "rijig/internal/whatsapp" + "rijig/internal/wilayahindo" "rijig/middleware" // "rijig/presentation" @@ -19,8 +21,9 @@ import ( func SetupRoutes(app *fiber.App) { apa := app.Group(os.Getenv("BASE_URL")) - whatsapp.WhatsAppRouter(apa) apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") + a := app.Group(os.Getenv("BASE_URL")) + whatsapp.WhatsAppRouter(a) api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) @@ -32,6 +35,8 @@ func SetupRoutes(app *fiber.App) { role.UserRoleRouter(api) article.ArticleRouter(api) + userprofile.UserProfileRouter(api) + wilayahindo.WilayahRouter(api) // || auth router || // // presentation.AuthRouter(api) @@ -58,5 +63,4 @@ func SetupRoutes(app *fiber.App) { // // presentation.AboutRouter(api) // presentation.TrashRouter(api) // presentation.CoverageAreaRouter(api) - whatsapp.WhatsAppRouter(api) } From baccdd696b12e2e8801c902375e93263d654b584 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 27 Jun 2025 19:26:26 +0700 Subject: [PATCH 46/48] refact: reafctor code for optimize --- README.md | 280 +++++++++- cmd/main.go | 32 +- config/database.go | 59 +-- config/migration.go | 66 +++ config/setup_config.go | 2 +- internal/authentication/authentication_dto.go | 5 +- .../authentication/authentication_handler.go | 30 +- .../authentication/authentication_route.go | 1 + .../authentication/authentication_service.go | 84 ++- internal/cart/cart_dto.go | 14 +- internal/cart/cart_handler.go | 92 +++- internal/cart/cart_redis.go | 72 +++ internal/cart/cart_route.go | 25 +- internal/cart/cart_service.go | 483 +++++++----------- internal/chat/model/chat_model.go | 1 + internal/collector/collector_dto.go | 14 + internal/collector/collector_repository.go | 33 ++ internal/company/company_dto.go | 8 +- internal/company/company_handler.go | 24 +- internal/company/company_route.go | 4 +- internal/company/company_service.go | 153 +++++- .../pickup_history_repository.go | 34 ++ .../requestpickup/pickup_maching_service.go | 146 ++++++ .../requestpickup/pickup_matching_handler.go | 50 ++ internal/requestpickup/requestpickup_dto.go | 75 ++- .../requestpickup/requestpickup_repository.go | 143 +++++- internal/trash/trash_repository.go | 40 +- internal/trash/trash_route.go | 85 ++- internal/userpin/userpin_service.go | 47 +- internal/whatsapp/whatsapp_handler.go | 367 +++++++++---- internal/whatsapp/whatsapp_route.go | 27 +- internal/worker/cart_worker.go | 28 +- middleware/additional_middleware.go | 199 -------- middleware/middleware.go | 2 +- router/setup_routes.go.go | 9 +- 35 files changed, 1982 insertions(+), 752 deletions(-) create mode 100644 config/migration.go create mode 100644 internal/cart/cart_redis.go create mode 100644 internal/chat/model/chat_model.go create mode 100644 internal/requestpickup/pickup_history_repository.go create mode 100644 internal/requestpickup/pickup_maching_service.go create mode 100644 internal/requestpickup/pickup_matching_handler.go delete mode 100644 middleware/additional_middleware.go diff --git a/README.md b/README.md index d7a7fc2..8b05eb2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,280 @@ # build_api_golang -this is rest API using go lang and go fiber with postgreql database for my personal project. +# 🗂️ Waste Management System API + +> **RESTful API untuk sistem pengelolaan sampah terintegrasi yang menghubungkan masyarakat, pengepul, dan pengelola dalam satu ekosistem digital.** + +[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=for-the-badge&logo=go)](https://golang.org/) +[![Fiber](https://img.shields.io/badge/Fiber-v2.52+-00ADD8?style=for-the-badge&logo=go)](https://gofiber.io/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-316192?style=for-the-badge&logo=postgresql)](https://www.postgresql.org/) +[![Redis](https://img.shields.io/badge/Redis-7.0+-DC382D?style=for-the-badge&logo=redis)](https://redis.io/) +[![Docker](https://img.shields.io/badge/Docker-24.0+-2496ED?style=for-the-badge&logo=docker)](https://www.docker.com/) +[![GORM](https://img.shields.io/badge/GORM-Latest-00ADD8?style=for-the-badge)](https://gorm.io/) + +## 📋 Deskripsi Aplikasi + +Waste Management System adalah backend API yang dikembangkan untuk mendigitalisasi sistem pengelolaan sampah di Indonesia. Aplikasi ini menghubungkan tiga stakeholder utama dalam rantai pengelolaan sampah melalui platform terintegrasi yang efisien dan transparan. + +### 🎯 Latar Belakang Masalah + +Indonesia menghadapi krisis pengelolaan sampah dengan berbagai tantangan: + +- **Volume Sampah Tinggi**: 67.8 juta ton sampah yang dihasilkan per tahun +- **Koordinasi Lemah**: Minimnya sinergi antar stakeholder pengelolaan sampah +- **Tracking Tidak Optimal**: Kurangnya visibility dalam proses pengelolaan sampah +- **Partisipasi Rendah**: Minimnya engagement masyarakat dalam program daur ulang +- **Inefisiensi Operasional**: Proses manual yang memakan waktu dan biaya tinggi + +### 💡 Solusi yang Ditawarkan + +Platform digital komprehensif yang menyediakan: + +- **Koordinasi Terintegrasi**: Menghubungkan seluruh stakeholder dalam satu platform +- **Tracking System**: Pelacakan sampah dari sumber hingga pengolahan akhir +- **Optimasi Proses**: Automasi dan optimasi rute pengumpulan +- **Engagement Platform**: Sistem gamifikasi untuk meningkatkan partisipasi +- **Data-Driven Insights**: Analytics untuk pengambilan keputusan berbasis data + +## 👥 Stakeholder Sistem + +### 🏠 **Masyarakat (Citizens)** +*Pengguna akhir yang menghasilkan sampah rumah tangga* + +**Peran dalam Sistem:** +- Melaporkan jenis dan volume sampah yang dihasilkan +- Mengakses informasi jadwal pengumpulan sampah +- Menerima edukasi tentang pemilahan sampah yang benar +- Berpartisipasi dalam program reward dan gamifikasi +- Melacak kontribusi personal terhadap lingkungan + +**Manfaat yang Diperoleh:** +- Kemudahan dalam melaporkan sampah +- Reward dan insentif dari partisipasi aktif +- Edukasi lingkungan yang berkelanjutan +- Transparansi dalam proses pengelolaan sampah + +### ♻️ **Pengepul (Collectors)** +*Pelaku usaha yang mengumpulkan dan mendistribusikan sampah* + +**Peran dalam Sistem:** +- Mengelola rute dan jadwal pengumpulan sampah optimal +- Memvalidasi dan menimbang sampah yang dikumpulkan +- Melakukan pemilahan awal berdasarkan kategori sampah +- Mengatur distribusi sampah ke berbagai pengelola +- Melaporkan volume dan jenis sampah yang berhasil dikumpulkan + +**Manfaat yang Diperoleh:** +- Optimasi rute untuk efisiensi operasional +- System tracking untuk akuntabilitas +- Platform untuk memperluas jangkauan bisnis +- Data analytics untuk business intelligence + +### 🏭 **Pengelola (Processors)** +*Institusi atau perusahaan pengolahan akhir sampah* + +**Peran dalam Sistem:** +- Mengelola fasilitas pengolahan sampah +- Memproses sampah menjadi produk daur ulang bernilai +- Melaporkan hasil pengolahan dan dampak lingkungan +- Memberikan feedback ke pengepul dan masyarakat +- Mengelola sistem pembayaran dan insentif + +**Manfaat yang Diperoleh:** +- Supply chain management yang terorganisir +- Traceability sampah untuk quality control +- Data untuk compliance dan sustainability reporting +- Platform untuk program CSR dan community engagement + +## ✨ Fitur Unggulan + +### 🔄 **End-to-End Waste Tracking** +Sistem pelacakan komprehensif yang memungkinkan monitoring sampah dari sumber hingga pengolahan akhir, memberikan transparansi penuh dalam setiap tahap proses. + +### 📊 **Real-time Analytics Dashboard** +Interface dashboard yang menampilkan data statistik, trend analysis, dan key performance indicators dengan visualisasi yang mudah dipahami semua stakeholder. + +### 🗺️ **Geographic Information System** +Sistem pemetaan cerdas untuk optimasi rute pengumpulan, identifikasi titik pengumpulan strategis, dan monitoring coverage area secara real-time. + +### 🎁 **Gamification & Reward System** +Program insentif untuk mendorong partisipasi aktif masyarakat melalui sistem poin, achievement badges, leaderboard, dan berbagai reward menarik. + +### 🔔 **Smart Notification System** +Sistem notifikasi multi-channel yang memberikan informasi real-time tentang jadwal pengumpulan, status sampah, achievement unlock, dan update penting lainnya. + +### 📈 **Comprehensive Reporting** +Modul pelaporan dengan kemampuan generate report otomatis, export dalam berbagai format, dan customizable dashboard untuk setiap role pengguna. + +## 🛠️ Tech Stack & Architecture + +### **Backend Development** + +#### **🚀 Golang (Go)** +*Primary Backend Language* + +**Mengapa Memilih Golang:** +- **Performance Excellence**: Compiled language dengan execution speed yang sangat tinggi +- **Concurrency Native**: Goroutines dan channels untuk handle ribuan concurrent requests +- **Memory Efficiency**: Garbage collector yang optimal dengan memory footprint rendah +- **Scalability Ready**: Mampu handle high-traffic dengan minimal resource consumption +- **Simple yet Powerful**: Syntax yang clean namun feature-rich untuk rapid development + +**Keunggulan untuk Waste Management System:** +- Mampu menangani concurrent requests dari multiple stakeholders secara simultan +- Processing real-time data tracking dengan performa tinggi +- Ideal untuk microservices architecture dan distributed systems +- Strong typing system untuk data integrity dalam financial transactions + +#### **⚡ Fiber Framework** +*High-Performance Web Framework* + +**Mengapa Memilih Fiber:** +- **Speed Optimized**: Salah satu framework tercepat untuk Go dengan minimal overhead +- **Memory Efficient**: Extremely low memory usage bahkan pada high load +- **Express-like API**: Familiar syntax bagi developer dengan background Node.js/Express +- **Rich Middleware Ecosystem**: Built-in middleware untuk authentication, CORS, logging, rate limiting +- **Zero Allocation**: Optimized untuk minimize memory allocation + +**Keunggulan untuk Waste Management System:** +- RESTful API development yang rapid dan efficient +- Middleware ecosystem yang mendukung complex business logic requirements +- Auto-recovery dan error handling untuk system reliability +- Built-in JSON serialization yang optimal untuk mobile app integration + +### **Database & Data Management** + +#### **🐘 PostgreSQL** +*Advanced Relational Database Management System* + +**Mengapa Memilih PostgreSQL:** +- **ACID Compliance**: Full transactional integrity untuk financial dan tracking data +- **Advanced Data Types**: JSON, Array, Geographic data types untuk flexible schema +- **Geospatial Support**: PostGIS extension untuk location-based features +- **Full-Text Search**: Built-in search capabilities untuk content discovery +- **Scalability Options**: Horizontal dan vertical scaling dengan replication support + +**Keunggulan untuk Waste Management System:** +- Geospatial data support untuk location tracking dan route optimization +- JSON storage untuk flexible metadata dan dynamic content +- Complex relationship handling untuk multi-stakeholder interactions +- Data consistency untuk transaction processing dan reward calculations + +#### **🔧 GORM (Go ORM)** +*Developer-Friendly Object-Relational Mapping* + +**Mengapa Memilih GORM:** +- **Auto Migration**: Automatic database schema migration dan versioning +- **Association Handling**: Powerful relationship management dengan lazy/eager loading +- **Hook System**: Lifecycle events untuk implement business rules +- **Query Builder**: Type-safe dan flexible query construction +- **Database Agnostic**: Support multiple database dengan same codebase + +**Keunggulan untuk Waste Management System:** +- Model relationship yang complex untuk stakeholder interactions +- Data validation dan business rules enforcement di ORM level +- Performance optimization dengan intelligent query generation +- Schema evolution yang safe untuk production deployments + +#### **⚡ Redis** +*In-Memory Data Structure Store* + +**Mengapa Memilih Redis:** +- **Ultra-High Performance**: Sub-millisecond response times untuk real-time features +- **Rich Data Structures**: Strings, Hashes, Lists, Sets, Sorted Sets, Streams +- **Pub/Sub Messaging**: Real-time communication untuk notification system +- **Persistence Options**: Data durability dengan configurable persistence +- **Clustering Support**: Horizontal scaling dengan Redis Cluster + +**Keunggulan untuk Waste Management System:** +- Session management untuk multi-role authentication system +- Real-time notifications dan messaging antar stakeholders +- Caching layer untuk frequently accessed data (routes, user profiles) +- Rate limiting untuk API protection dan fair usage +- Leaderboard dan ranking system untuk gamification features + +### **Infrastructure & Deployment** + +#### **🐳 Docker** +*Application Containerization Platform* + +**Mengapa Memilih Docker:** +- **Environment Consistency**: Identical environment dari development hingga production +- **Scalability Ready**: Easy horizontal scaling dengan container orchestration +- **Resource Efficiency**: Lightweight containers dibanding traditional virtual machines +- **Deployment Simplicity**: One-command deployment dengan reproducible builds +- **Microservices Architecture**: Perfect untuk distributed system deployment + +**Keunggulan untuk Waste Management System:** +- Development environment yang consistent untuk seluruh tim developer +- Production deployment yang reliable dan reproducible +- Easy scaling berdasarkan load dari multiple stakeholders +- Integration yang seamless dengan CI/CD pipeline +- Service isolation untuk better security dan debugging + +## 🏗️ System Architecture + +### **Layered Architecture Pattern** + +``` +┌─────────────────────────────────────┐ +│ Presentation Layer │ +│ (Mobile Apps, Web Dashboard) │ +└─────────────────┬───────────────────┘ + │ RESTful API +┌─────────────────▼───────────────────┐ +│ API Gateway Layer │ +│ (Fiber + Middleware) │ +└─────────────────┬───────────────────┘ + │ +┌─────────────────▼───────────────────┐ +│ Business Logic Layer │ +│ (Service Components) │ +└─────────────────┬───────────────────┘ + │ +┌─────────────────▼───────────────────┐ +│ Data Access Layer │ +│ (Repository Pattern + GORM) │ +└─────────────────┬───────────────────┘ + │ +┌─────────────────▼───────────────────┐ +│ Persistence Layer │ +│ (PostgreSQL + Redis) │ +└─────────────────────────────────────┘ +``` + +### **Key Architectural Principles** + +- **Separation of Concerns**: Clear separation antara business logic, data access, dan presentation +- **Dependency Injection**: Loose coupling antar components untuk better testability +- **Repository Pattern**: Abstraction layer untuk data access operations +- **Middleware Pattern**: Cross-cutting concerns seperti authentication, logging, validation +- **Event-Driven Architecture**: Pub/sub pattern untuk real-time notifications + +## 🌟 Competitive Advantages + +### **Technical Excellence** +- **High Performance**: Sub-100ms response time untuk critical operations +- **Scalability**: Ready untuk handle growth hingga millions of users +- **Security First**: Multi-layer security dengan encryption dan secure authentication +- **Real-time Capabilities**: Instant updates dan notifications untuk better user experience + +### **Business Value** +- **Cost Efficiency**: Significant reduction dalam operational cost melalui automation +- **Environmental Impact**: Measurable contribution untuk sustainability goals +- **Stakeholder Engagement**: User-friendly platform yang mendorong active participation +- **Data-Driven Decision**: Comprehensive analytics untuk strategic planning + +### **Innovation Features** +- **AI-Ready Architecture**: Prepared untuk integration dengan machine learning models +- **IoT Integration**: Ready untuk connect dengan smart waste bins dan sensors +- **Blockchain Compatibility**: Architecture yang support untuk blockchain integration +- **Multi-tenancy Support**: Scalable untuk multiple cities dan regions + +--- + +
+ +**Waste Management System** menggunakan cutting-edge technology stack untuk menciptakan solusi digital yang sustainable, scalable, dan user-centric dalam pengelolaan sampah di Indonesia. + +🌱 **Built for Sustainability • Designed for Scale • Engineered for Impact** 🌱 + +
diff --git a/cmd/main.go b/cmd/main.go index 85c404a..7d80cb5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,13 @@ package main import ( + "log" "rijig/config" + "rijig/internal/cart" + "rijig/internal/trash" + "rijig/internal/worker" + "time" + // "rijig/internal/repositories" // "rijig/internal/services" @@ -13,21 +19,21 @@ import ( func main() { config.SetupConfig() - // cartRepo := repositories.NewCartRepository() - // trashRepo := repositories.NewTrashRepository(config.DB) - // cartService := services.NewCartService(cartRepo, trashRepo) - // worker := worker.NewCartWorker(cartService, cartRepo, trashRepo) + cartRepo := cart.NewCartRepository() + trashRepo := trash.NewTrashRepository(config.DB) + cartService := cart.NewCartService(cartRepo, trashRepo) + worker := worker.NewCartWorker(cartService, cartRepo, trashRepo) - // go func() { - // ticker := time.NewTicker(30 * time.Second) - // defer ticker.Stop() + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() - // for range ticker.C { - // if err := worker.AutoCommitExpiringCarts(); err != nil { - // log.Printf("Auto-commit error: %v", err) - // } - // } - // }() + for range ticker.C { + if err := worker.AutoCommitExpiringCarts(); err != nil { + log.Printf("Auto-commit error: %v", err) + } + } + }() app := fiber.New(fiber.Config{ ErrorHandler: func(c *fiber.Ctx, err error) error { diff --git a/config/database.go b/config/database.go index 04707b3..92a4356 100644 --- a/config/database.go +++ b/config/database.go @@ -5,8 +5,6 @@ import ( "log" "os" - "rijig/model" - "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -14,7 +12,6 @@ import ( var DB *gorm.DB func ConnectDatabase() { - dsn := fmt.Sprintf( "host=%s user=%s password=%s dbname=%s port=%s sslmode=disable", os.Getenv("DB_HOST"), @@ -31,59 +28,7 @@ func ConnectDatabase() { } log.Println("Database connected successfully!") - err = DB.AutoMigrate( - // ==wilayah indonesia== - &model.Province{}, - &model.Regency{}, - &model.District{}, - &model.Village{}, - // ==wilayah indonesia== - - // ==============main feature============== - // =>user preparation<= - &model.User{}, - &model.Collector{}, - &model.AvaibleTrashByCollector{}, - &model.Role{}, - &model.UserPin{}, - &model.Address{}, - &model.IdentityCard{}, - &model.CompanyProfile{}, - - // =>user preparation<= - // =>requestpickup preparation<= - &model.RequestPickup{}, - &model.RequestPickupItem{}, - &model.PickupStatusHistory{}, - &model.PickupRating{}, - - &model.Cart{}, - &model.CartItem{}, - // =>requestpickup preparation<= - - // =>store preparation<= - &model.Store{}, - &model.Product{}, - &model.ProductImage{}, - // =>store preparation<= - // ==============main feature============== - - // ==============additional content======== - &model.Article{}, - &model.Banner{}, - &model.InitialCoint{}, - &model.About{}, - &model.AboutDetail{}, - &model.CoverageArea{}, - - // =>Trash Model<= - &model.TrashCategory{}, - &model.TrashDetail{}, - // =>Trash Model<= - // ==============additional content======== - ) - if err != nil { + if err := RunMigrations(DB); err != nil { log.Fatalf("Error performing auto-migration: %v", err) } - log.Println("Database migrated successfully!") -} +} \ No newline at end of file diff --git a/config/migration.go b/config/migration.go new file mode 100644 index 0000000..d00f0e6 --- /dev/null +++ b/config/migration.go @@ -0,0 +1,66 @@ +package config + +import ( + "log" + + "rijig/model" + + "gorm.io/gorm" +) + +func RunMigrations(db *gorm.DB) error { + log.Println("Starting database migration...") + + err := db.AutoMigrate( + // Location models + &model.Province{}, + &model.Regency{}, + &model.District{}, + &model.Village{}, + + // User related models + &model.User{}, + &model.Collector{}, + &model.AvaibleTrashByCollector{}, + &model.Role{}, + &model.UserPin{}, + &model.Address{}, + &model.IdentityCard{}, + &model.CompanyProfile{}, + + // Pickup related models + &model.RequestPickup{}, + &model.RequestPickupItem{}, + &model.PickupStatusHistory{}, + &model.PickupRating{}, + + // Cart related models + &model.Cart{}, + &model.CartItem{}, + + // Store related models + &model.Store{}, + &model.Product{}, + &model.ProductImage{}, + + // Content models + &model.Article{}, + &model.Banner{}, + &model.InitialCoint{}, + &model.About{}, + &model.AboutDetail{}, + &model.CoverageArea{}, + + // Trash related models + &model.TrashCategory{}, + &model.TrashDetail{}, + ) + + if err != nil { + log.Printf("Error performing auto-migration: %v", err) + return err + } + + log.Println("Database migrated successfully!") + return nil +} \ No newline at end of file diff --git a/config/setup_config.go b/config/setup_config.go index fc7b531..5464832 100644 --- a/config/setup_config.go +++ b/config/setup_config.go @@ -1,5 +1,5 @@ -package config +package config import ( "log" "os" diff --git a/internal/authentication/authentication_dto.go b/internal/authentication/authentication_dto.go index 5e2608a..b29aee8 100644 --- a/internal/authentication/authentication_dto.go +++ b/internal/authentication/authentication_dto.go @@ -322,8 +322,9 @@ func (r *RegisterAdminRequest) ValidateRegisterAdminRequest() (map[string][]stri errors["name"] = append(errors["name"], "Name is required") } - if r.Gender != "male" && r.Gender != "female" { - errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'") + genderLower := strings.ToLower(strings.TrimSpace(r.Gender)) + if genderLower != "laki-laki" && genderLower != "perempuan" { + errors["gender"] = append(errors["gender"], "Gender must be either 'laki-laki' or 'perempuan'") } if strings.TrimSpace(r.DateOfBirth) == "" { diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go index 31f83e4..60a6a86 100644 --- a/internal/authentication/authentication_handler.go +++ b/internal/authentication/authentication_handler.go @@ -1,6 +1,7 @@ package authentication import ( + "log" "rijig/middleware" "rijig/utils" @@ -16,8 +17,11 @@ func NewAuthenticationHandler(service AuthenticationService) *AuthenticationHand } func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error { - deviceID := c.Get("X-Device-ID") - if deviceID == "" { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + if claims.DeviceID == "" { return utils.BadRequest(c, "Device ID is required") } @@ -31,12 +35,11 @@ func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error { return utils.BadRequest(c, "Refresh token is required") } - userID, ok := c.Locals("user_id").(string) - if !ok || userID == "" { - return utils.Unauthorized(c, "Unauthorized or missing user ID") + if claims.UserID == "" { + return utils.BadRequest(c, "userid is required") } - tokenData, err := utils.RefreshAccessToken(userID, deviceID, body.RefreshToken) + tokenData, err := utils.RefreshAccessToken(claims.UserID, claims.DeviceID, body.RefreshToken) if err != nil { return utils.Unauthorized(c, err.Error()) } @@ -62,6 +65,21 @@ func (h *AuthenticationHandler) GetMe(c *fiber.Ctx) error { } +func (h *AuthenticationHandler) GetRegistrationStatus(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "unauthorized access") + } + + res, err := h.service.GetRegistrationStatus(c.Context(), claims.UserID, claims.DeviceID) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "Registration status retrieved successfully", res) +} + func (h *AuthenticationHandler) Login(c *fiber.Ctx) error { var req LoginAdminRequest diff --git a/internal/authentication/authentication_route.go b/internal/authentication/authentication_route.go index 38b489e..149a019 100644 --- a/internal/authentication/authentication_route.go +++ b/internal/authentication/authentication_route.go @@ -30,6 +30,7 @@ func AuthenticationRouter(api fiber.Router) { // authHandler.GetMe, // ) + authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus) authRoute.Post("/login/admin", authHandler.Login) authRoute.Post("/register/admin", authHandler.Register) authRoute.Post("/request-otp", authHandler.RequestOtpHandler) diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index f2be14f..d984642 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -3,6 +3,7 @@ package authentication import ( "context" "fmt" + "log" "strings" "time" @@ -12,6 +13,7 @@ import ( ) type AuthenticationService interface { + GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error @@ -48,6 +50,78 @@ func normalizeRoleName(roleName string) string { } } +type GetRegistrationStatusResponse struct { + UserID string `json:"userId"` + RegistrationStatus string `json:"registrationStatus"` + RegistrationProgress int8 `json:"registrationProgress"` + Name string `json:"name"` + Phone string `json:"phone"` + Role string `json:"role"` +} + +func (s *authenticationService) GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error) { + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("user not found: %w", err) + } + + if user.Role.RoleName == "" { + return nil, fmt.Errorf("user role not found") + } + + if user.RegistrationStatus == utils.RegStatusPending { + log.Printf("⏳ User %s (%s) registration is still pending approval", user.Name, user.Phone) + + return &AuthResponse{ + Message: "Your registration is currently under review. Please wait for approval.", + RegistrationStatus: user.RegistrationStatus, + NextStep: "wait_for_approval", + }, nil + } + + if user.RegistrationStatus == utils.RegStatusConfirmed || user.RegistrationStatus == utils.RegStatusRejected { + tokenResponse, err := utils.GenerateTokenPair( + user.ID, + user.Role.RoleName, + deviceID, + user.RegistrationStatus, + int(user.RegistrationProgress), + ) + if err != nil { + log.Printf("GenerateTokenPair error: %v", err) + return nil, fmt.Errorf("failed to generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep( + user.Role.RoleName, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) + + var message string + if user.RegistrationStatus == utils.RegStatusConfirmed { + message = "Registration approved successfully" + log.Printf("✅ User %s (%s) registration approved - generating tokens", user.Name, user.Phone) + } else if user.RegistrationStatus == utils.RegStatusRejected { + message = "Registration has been rejected" + log.Printf("❌ User %s (%s) registration rejected - generating tokens for rejection flow", user.Name, user.Phone) + } + + return &AuthResponse{ + Message: message, + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: user.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil + } + + return nil, fmt.Errorf("unsupported registration status: %s", user.RegistrationStatus) +} + func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) { user, err := s.authRepo.FindUserByEmail(ctx, req.Email) if err != nil { @@ -341,8 +415,16 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO user.RegistrationStatus, ) + var message string + if user.RegistrationStatus == utils.RegStatusComplete { + message = "verif pin" + nextStep = "verif_pin" + } else { + message = "otp berhasil diverifikasi" + } + return &AuthResponse{ - Message: "otp berhasil diverifikasi", + Message: message, AccessToken: tokenResponse.AccessToken, RefreshToken: tokenResponse.RefreshToken, TokenType: string(tokenResponse.TokenType), diff --git a/internal/cart/cart_dto.go b/internal/cart/cart_dto.go index 3c6dfc5..2505092 100644 --- a/internal/cart/cart_dto.go +++ b/internal/cart/cart_dto.go @@ -14,15 +14,15 @@ type RequestCartDTO struct { CartItems []RequestCartItemDTO `json:"cart_items"` } -type CartResponse struct { - ID string `json:"id"` - UserID string `json:"user_id"` - TotalAmount float64 `json:"total_amount"` - EstimatedTotalPrice float64 `json:"estimated_total_price"` - CartItems []CartItemResponse `json:"cart_items"` +type ResponseCartDTO struct { + ID string `json:"id"` + UserID string `json:"user_id"` + TotalAmount float64 `json:"total_amount"` + EstimatedTotalPrice float64 `json:"estimated_total_price"` + CartItems []ResponseCartItemDTO `json:"cart_items"` } -type CartItemResponse struct { +type ResponseCartItemDTO struct { ID string `json:"id"` TrashID string `json:"trash_id"` TrashName string `json:"trash_name"` diff --git a/internal/cart/cart_handler.go b/internal/cart/cart_handler.go index 795236c..a7ea250 100644 --- a/internal/cart/cart_handler.go +++ b/internal/cart/cart_handler.go @@ -1 +1,91 @@ -package cart \ No newline at end of file +package cart + +import ( + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type CartHandler struct { + cartService CartService +} + +func NewCartHandler(cartService CartService) *CartHandler { + return &CartHandler{cartService: cartService} +} + +func (h *CartHandler) AddOrUpdateItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + var req RequestCartItemDTO + + if err := c.BodyParser(&req); err != nil { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Payload tidak valid", map[string][]string{ + "request": {"Payload tidak valid"}, + }) + } + + hasErrors := req.Amount <= 0 || req.TrashID == "" + if hasErrors { + errs := make(map[string][]string) + if req.Amount <= 0 { + errs["amount"] = append(errs["amount"], "Amount harus lebih dari 0") + } + if req.TrashID == "" { + errs["trash_id"] = append(errs["trash_id"], "Trash ID tidak boleh kosong") + } + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validasi gagal", errs) + } + + if err := h.cartService.AddOrUpdateItem(c.Context(), userID, req); err != nil { + return utils.InternalServerError(c, "Gagal menambahkan item ke keranjang") + } + + return utils.Success(c, "Item berhasil ditambahkan ke keranjang") +} + +func (h *CartHandler) GetCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + cart, err := h.cartService.GetCart(c.Context(), userID) + if err != nil { + return utils.InternalServerError(c, "Gagal mengambil data keranjang") + } + + return utils.SuccessWithData(c, "Berhasil mengambil data keranjang", cart) +} + +func (h *CartHandler) DeleteItem(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + trashID := c.Params("trash_id") + + if trashID == "" { + return utils.BadRequest(c, "Trash ID tidak boleh kosong") + } + + if err := h.cartService.DeleteItem(c.Context(), userID, trashID); err != nil { + return utils.InternalServerError(c, "Gagal menghapus item dari keranjang") + } + + return utils.Success(c, "Item berhasil dihapus dari keranjang") +} + +func (h *CartHandler) Checkout(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + if err := h.cartService.Checkout(c.Context(), userID); err != nil { + return utils.InternalServerError(c, "Gagal melakukan checkout keranjang") + } + + return utils.Success(c, "Checkout berhasil. Permintaan pickup telah dibuat.") +} + +func (h *CartHandler) ClearCart(c *fiber.Ctx) error { + userID := c.Locals("userID").(string) + + err := h.cartService.ClearCart(c.Context(), userID) + if err != nil { + return utils.InternalServerError(c, "Gagal menghapus keranjang") + } + + return utils.Success(c, "Keranjang berhasil dikosongkan") +} \ No newline at end of file diff --git a/internal/cart/cart_redis.go b/internal/cart/cart_redis.go new file mode 100644 index 0000000..cd91d47 --- /dev/null +++ b/internal/cart/cart_redis.go @@ -0,0 +1,72 @@ +package cart + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "rijig/config" + + "github.com/go-redis/redis/v8" +) + +const CartTTL = 30 * time.Minute +const CartKeyPrefix = "cart:" + +func buildCartKey(userID string) string { + return fmt.Sprintf("%s%s", CartKeyPrefix, userID) +} + +func SetCartToRedis(ctx context.Context, userID string, cart RequestCartDTO) error { + data, err := json.Marshal(cart) + if err != nil { + return err + } + + return config.RedisClient.Set(ctx, buildCartKey(userID), data, CartTTL).Err() +} + +func RefreshCartTTL(ctx context.Context, userID string) error { + return config.RedisClient.Expire(ctx, buildCartKey(userID), CartTTL).Err() +} + +func GetCartFromRedis(ctx context.Context, userID string) (*RequestCartDTO, error) { + val, err := config.RedisClient.Get(ctx, buildCartKey(userID)).Result() + if err == redis.Nil { + return nil, nil + } else if err != nil { + return nil, err + } + + var cart RequestCartDTO + if err := json.Unmarshal([]byte(val), &cart); err != nil { + return nil, err + } + return &cart, nil +} + +func DeleteCartFromRedis(ctx context.Context, userID string) error { + return config.RedisClient.Del(ctx, buildCartKey(userID)).Err() +} + +func GetExpiringCartKeys(ctx context.Context, threshold time.Duration) ([]string, error) { + keys, err := config.RedisClient.Keys(ctx, CartKeyPrefix+"*").Result() + if err != nil { + return nil, err + } + + var expiringKeys []string + for _, key := range keys { + ttl, err := config.RedisClient.TTL(ctx, key).Result() + if err != nil { + continue + } + + if ttl > 0 && ttl <= threshold { + expiringKeys = append(expiringKeys, key) + } + } + + return expiringKeys, nil +} diff --git a/internal/cart/cart_route.go b/internal/cart/cart_route.go index 795236c..76bbc2b 100644 --- a/internal/cart/cart_route.go +++ b/internal/cart/cart_route.go @@ -1 +1,24 @@ -package cart \ No newline at end of file +package cart + +import ( + "rijig/config" + "rijig/internal/trash" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func TrashCartRouter(api fiber.Router) { + repo := NewCartRepository() + trashRepo := trash.NewTrashRepository(config.DB) + cartService := NewCartService(repo, trashRepo) + cartHandler := NewCartHandler(cartService) + + cart := api.Group("/cart") + cart.Use(middleware.AuthMiddleware()) + + cart.Get("/", cartHandler.GetCart) + cart.Post("/item", cartHandler.AddOrUpdateItem) + cart.Delete("/item/:trash_id", cartHandler.DeleteItem) + cart.Delete("/clear", cartHandler.ClearCart) +} diff --git a/internal/cart/cart_service.go b/internal/cart/cart_service.go index 6684252..ab2eb6a 100644 --- a/internal/cart/cart_service.go +++ b/internal/cart/cart_service.go @@ -2,355 +2,266 @@ package cart import ( "context" - "fmt" + "errors" "log" - "time" + // "rijig/dto" + // "rijig/internal/repositories" "rijig/internal/trash" "rijig/model" - "rijig/utils" - - "github.com/google/uuid" ) -type CartService struct { - cartRepo CartRepository +type CartService interface { + AddOrUpdateItem(ctx context.Context, userID string, req RequestCartItemDTO) error + GetCart(ctx context.Context, userID string) (*ResponseCartDTO, error) + DeleteItem(ctx context.Context, userID string, trashID string) error + ClearCart(ctx context.Context, userID string) error + Checkout(ctx context.Context, userID string) error +} + +type cartService struct { + repo CartRepository trashRepo trash.TrashRepositoryInterface } -func NewCartService(cartRepo CartRepository, trashRepo trash.TrashRepositoryInterface) *CartService { - return &CartService{ - cartRepo: cartRepo, - trashRepo: trashRepo, - } +func NewCartService(repo CartRepository, trashRepo trash.TrashRepositoryInterface) CartService { + return &cartService{repo, trashRepo} } -func (s *CartService) AddToCart(ctx context.Context, userID, trashCategoryID string, amount float64) error { - cartKey := fmt.Sprintf("cart:%s", userID) - - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) - if err != nil && err.Error() != "ErrCacheMiss" { - return fmt.Errorf("failed to get cart from cache: %w", err) +func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req RequestCartItemDTO) error { + if req.Amount <= 0 { + return errors.New("amount harus lebih dari 0") } - if cartItems == nil { - cartItems = make(map[string]model.CartItem) - } - - trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, trashCategoryID) + _, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID) if err != nil { - return fmt.Errorf("failed to get trash category: %w", err) + return err } - cartItems[trashCategoryID] = model.CartItem{ - TrashCategoryID: trashCategoryID, - Amount: amount, - SubTotalEstimatedPrice: amount * float64(trashCategory.EstimatedPrice), - } - - return utils.SetCache(cartKey, cartItems, 24*time.Hour) -} - -func (s *CartService) RemoveFromCart(ctx context.Context, userID, trashCategoryID string) error { - cartKey := fmt.Sprintf("cart:%s", userID) - - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) + existingCart, err := GetCartFromRedis(ctx, userID) if err != nil { - if err.Error() == "ErrCacheMiss" { - return nil - } - return fmt.Errorf("failed to get cart from cache: %w", err) + return err } - delete(cartItems, trashCategoryID) - - if len(cartItems) == 0 { - return utils.DeleteCache(cartKey) + if existingCart == nil { + existingCart = &RequestCartDTO{ + CartItems: []RequestCartItemDTO{}, + } } - return utils.SetCache(cartKey, cartItems, 24*time.Hour) -} - -func (s *CartService) ClearCart(userID string) error { - cartKey := fmt.Sprintf("cart:%s", userID) - return utils.DeleteCache(cartKey) -} - -func (s *CartService) GetCartFromRedis(ctx context.Context, userID string) (*CartResponse, error) { - cartKey := fmt.Sprintf("cart:%s", userID) - - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) - if err != nil { - if err.Error() == "ErrCacheMiss" { - return &CartResponse{ - ID: "N/A", - UserID: userID, - TotalAmount: 0, - EstimatedTotalPrice: 0, - CartItems: []CartItemResponse{}, - }, nil + updated := false + for i, item := range existingCart.CartItems { + if item.TrashID == req.TrashID { + existingCart.CartItems[i].Amount = req.Amount + updated = true + break } - return nil, fmt.Errorf("failed to get cart from cache: %w", err) } - var totalAmount float64 - var estimatedTotal float64 - var cartItemDTOs []CartItemResponse - - for _, item := range cartItems { - trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID) - if err != nil { - log.Printf("Failed to get trash category %s: %v", item.TrashCategoryID, err) - continue - } - - totalAmount += item.Amount - estimatedTotal += item.SubTotalEstimatedPrice - - cartItemDTOs = append(cartItemDTOs, CartItemResponse{ - ID: uuid.NewString(), - TrashID: trashCategory.ID, - TrashName: trashCategory.Name, - TrashIcon: trashCategory.IconTrash, - TrashPrice: float64(trashCategory.EstimatedPrice), - Amount: item.Amount, - SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, + if !updated { + existingCart.CartItems = append(existingCart.CartItems, RequestCartItemDTO{ + TrashID: req.TrashID, + Amount: req.Amount, }) } - resp := &CartResponse{ - ID: "N/A", - UserID: userID, - TotalAmount: totalAmount, - EstimatedTotalPrice: estimatedTotal, - CartItems: cartItemDTOs, - } - - return resp, nil + return SetCartToRedis(ctx, userID, *existingCart) } -func (s *CartService) CommitCartToDatabase(ctx context.Context, userID string) error { - cartKey := fmt.Sprintf("cart:%s", userID) +func (s *cartService) GetCart(ctx context.Context, userID string) (*ResponseCartDTO, error) { - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) + cached, err := GetCartFromRedis(ctx, userID) if err != nil { - if err.Error() == "ErrCacheMiss" { - log.Printf("No cart items found in Redis for user: %s", userID) - return fmt.Errorf("no cart items found") - } - return fmt.Errorf("failed to get cart from cache: %w", err) + return nil, err } - if len(cartItems) == 0 { - log.Printf("No items to commit for user: %s", userID) - return fmt.Errorf("no items to commit") - } + if cached != nil { - hasCart, err := s.cartRepo.HasExistingCart(ctx, userID) - if err != nil { - return fmt.Errorf("failed to check existing cart: %w", err) - } - - var cart *model.Cart - if hasCart { - - cart, err = s.cartRepo.GetCartByUser(ctx, userID) - if err != nil { - return fmt.Errorf("failed to get existing cart: %w", err) - } - } else { - - cart, err = s.cartRepo.FindOrCreateCart(ctx, userID) - if err != nil { - return fmt.Errorf("failed to create cart: %w", err) - } - } - - for _, item := range cartItems { - trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID) - if err != nil { - log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID) - continue + if err := RefreshCartTTL(ctx, userID); err != nil { + log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err) } - err = s.cartRepo.AddOrUpdateCartItem( - ctx, - cart.ID, - item.TrashCategoryID, - item.Amount, - float64(trashCategory.EstimatedPrice), - ) - if err != nil { - log.Printf("Failed to add/update cart item: %v", err) - continue - } + return s.buildResponseFromCache(ctx, userID, cached) } - if err := s.cartRepo.UpdateCartTotals(ctx, cart.ID); err != nil { - return fmt.Errorf("failed to update cart totals: %w", err) - } - - if err := utils.DeleteCache(cartKey); err != nil { - log.Printf("Failed to clear Redis cart: %v", err) - } - - log.Printf("Cart committed successfully for user: %s", userID) - return nil -} - -func (s *CartService) GetCart(ctx context.Context, userID string) (*CartResponse, error) { - - cartRedis, err := s.GetCartFromRedis(ctx, userID) - if err == nil && len(cartRedis.CartItems) > 0 { - return cartRedis, nil - } - - cartDB, err := s.cartRepo.GetCartByUser(ctx, userID) + cart, err := s.repo.GetCartByUser(ctx, userID) if err != nil { - return &CartResponse{ - ID: "N/A", + return &ResponseCartDTO{ + ID: "", UserID: userID, TotalAmount: 0, EstimatedTotalPrice: 0, - CartItems: []CartItemResponse{}, + CartItems: []ResponseCartItemDTO{}, }, nil + } - var items []CartItemResponse - for _, item := range cartDB.CartItems { - items = append(items, CartItemResponse{ + response := s.buildResponseFromDB(cart) + + cacheData := RequestCartDTO{CartItems: []RequestCartItemDTO{}} + for _, item := range cart.CartItems { + cacheData.CartItems = append(cacheData.CartItems, RequestCartItemDTO{ + TrashID: item.TrashCategoryID, + Amount: item.Amount, + }) + } + + if err := SetCartToRedis(ctx, userID, cacheData); err != nil { + log.Printf("Warning: Failed to cache cart for user %s: %v", userID, err) + } + + return response, nil +} + +func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error { + + existingCart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + if existingCart == nil { + return errors.New("keranjang tidak ditemukan") + } + + filtered := []RequestCartItemDTO{} + for _, item := range existingCart.CartItems { + if item.TrashID != trashID { + filtered = append(filtered, item) + } + } + existingCart.CartItems = filtered + + return SetCartToRedis(ctx, userID, *existingCart) +} + +func (s *cartService) ClearCart(ctx context.Context, userID string) error { + + if err := DeleteCartFromRedis(ctx, userID); err != nil { + return err + } + + return s.repo.DeleteCart(ctx, userID) +} + +func (s *cartService) Checkout(ctx context.Context, userID string) error { + + cachedCart, err := GetCartFromRedis(ctx, userID) + if err != nil { + return err + } + + if cachedCart != nil { + if err := s.commitCartFromRedis(ctx, userID, cachedCart); err != nil { + return err + } + } + + _, err = s.repo.GetCartByUser(ctx, userID) + if err != nil { + return err + } + + DeleteCartFromRedis(ctx, userID) + return s.repo.DeleteCart(ctx, userID) +} + +func (s *cartService) buildResponseFromCache(ctx context.Context, userID string, cached *RequestCartDTO) (*ResponseCartDTO, error) { + totalQty := 0.0 + totalPrice := 0.0 + items := []ResponseCartItemDTO{} + + for _, item := range cached.CartItems { + trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + log.Printf("Warning: Trash category %s not found for cached cart item", item.TrashID) + continue + } + + subtotal := item.Amount * trash.EstimatedPrice + totalQty += item.Amount + totalPrice += subtotal + + items = append(items, ResponseCartItemDTO{ + ID: "", + TrashID: item.TrashID, + TrashName: trash.Name, + TrashIcon: trash.IconTrash, + TrashPrice: trash.EstimatedPrice, + Amount: item.Amount, + SubTotalEstimatedPrice: subtotal, + }) + } + + return &ResponseCartDTO{ + ID: "-", + UserID: userID, + TotalAmount: totalQty, + EstimatedTotalPrice: totalPrice, + CartItems: items, + }, nil +} + +func (s *cartService) buildResponseFromDB(cart *model.Cart) *ResponseCartDTO { + var items []ResponseCartItemDTO + for _, item := range cart.CartItems { + items = append(items, ResponseCartItemDTO{ ID: item.ID, TrashID: item.TrashCategoryID, TrashName: item.TrashCategory.Name, TrashIcon: item.TrashCategory.IconTrash, - TrashPrice: float64(item.TrashCategory.EstimatedPrice), + TrashPrice: item.TrashCategory.EstimatedPrice, Amount: item.Amount, SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, }) } - resp := &CartResponse{ - ID: cartDB.ID, - UserID: cartDB.UserID, - TotalAmount: cartDB.TotalAmount, - EstimatedTotalPrice: cartDB.EstimatedTotalPrice, + return &ResponseCartDTO{ + ID: cart.ID, + UserID: cart.UserID, + TotalAmount: cart.TotalAmount, + EstimatedTotalPrice: cart.EstimatedTotalPrice, CartItems: items, } - - return resp, nil } -func (s *CartService) SyncCartFromDatabaseToRedis(ctx context.Context, userID string) error { - - cartDB, err := s.cartRepo.GetCartByUser(ctx, userID) - if err != nil { - return fmt.Errorf("failed to get cart from database: %w", err) +func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *RequestCartDTO) error { + if len(cachedCart.CartItems) == 0 { + return nil } - cartItems := make(map[string]model.CartItem) - for _, item := range cartDB.CartItems { - cartItems[item.TrashCategoryID] = model.CartItem{ - TrashCategoryID: item.TrashCategoryID, + totalAmount := 0.0 + totalPrice := 0.0 + var cartItems []model.CartItem + + for _, item := range cachedCart.CartItems { + trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID) + if err != nil { + log.Printf("Warning: Skipping invalid trash category %s during commit", item.TrashID) + continue + } + + subtotal := item.Amount * trash.EstimatedPrice + totalAmount += item.Amount + totalPrice += subtotal + + cartItems = append(cartItems, model.CartItem{ + TrashCategoryID: item.TrashID, Amount: item.Amount, - SubTotalEstimatedPrice: item.SubTotalEstimatedPrice, - } - } - - cartKey := fmt.Sprintf("cart:%s", userID) - return utils.SetCache(cartKey, cartItems, 24*time.Hour) -} - -func (s *CartService) GetCartItemCount(userID string) (int, error) { - cartKey := fmt.Sprintf("cart:%s", userID) - - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) - if err != nil { - if err.Error() == "ErrCacheMiss" { - return 0, nil - } - return 0, fmt.Errorf("failed to get cart from cache: %w", err) - } - - return len(cartItems), nil -} - -func (s *CartService) DeleteCart(ctx context.Context, userID string) error { - - cartKey := fmt.Sprintf("cart:%s", userID) - if err := utils.DeleteCache(cartKey); err != nil { - log.Printf("Failed to delete cart from Redis: %v", err) - } - - return s.cartRepo.DeleteCart(ctx, userID) -} - -func (s *CartService) UpdateCartWithDTO(ctx context.Context, userID string, cartDTO *RequestCartDTO) error { - - if errors, valid := cartDTO.ValidateRequestCartDTO(); !valid { - return fmt.Errorf("validation failed: %v", errors) - } - - cartKey := fmt.Sprintf("cart:%s", userID) - cartItems := make(map[string]model.CartItem) - - for _, itemDTO := range cartDTO.CartItems { - - trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID) - if err != nil { - log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err) - continue - } - - subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice) - - cartItems[itemDTO.TrashID] = model.CartItem{ - TrashCategoryID: itemDTO.TrashID, - Amount: itemDTO.Amount, SubTotalEstimatedPrice: subtotal, - } + }) } - return utils.SetCache(cartKey, cartItems, 24*time.Hour) -} - -func (s *CartService) AddItemsToCart(ctx context.Context, userID string, items []RequestCartItemDTO) error { - cartKey := fmt.Sprintf("cart:%s", userID) - - var cartItems map[string]model.CartItem - err := utils.GetCache(cartKey, &cartItems) - if err != nil && err.Error() != "ErrCacheMiss" { - return fmt.Errorf("failed to get cart from cache: %w", err) - } - - if cartItems == nil { - cartItems = make(map[string]model.CartItem) - } - - for _, itemDTO := range items { - if itemDTO.TrashID == "" { - continue - } - - trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID) - if err != nil { - log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err) - continue - } - - subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice) - - cartItems[itemDTO.TrashID] = model.CartItem{ - TrashCategoryID: itemDTO.TrashID, - Amount: itemDTO.Amount, - SubTotalEstimatedPrice: subtotal, - } - } - - return utils.SetCache(cartKey, cartItems, 24*time.Hour) + if len(cartItems) == 0 { + return nil + } + + newCart := &model.Cart{ + UserID: userID, + TotalAmount: totalAmount, + EstimatedTotalPrice: totalPrice, + CartItems: cartItems, + } + + return s.repo.CreateCartWithItems(ctx, newCart) } diff --git a/internal/chat/model/chat_model.go b/internal/chat/model/chat_model.go new file mode 100644 index 0000000..0c3b4f2 --- /dev/null +++ b/internal/chat/model/chat_model.go @@ -0,0 +1 @@ +package model \ No newline at end of file diff --git a/internal/collector/collector_dto.go b/internal/collector/collector_dto.go index f7a4db4..955cd39 100644 --- a/internal/collector/collector_dto.go +++ b/internal/collector/collector_dto.go @@ -7,6 +7,20 @@ import ( "strings" "time" ) +type NearbyCollectorDTO struct { + CollectorID string `json:"collector_id"` + Name string `json:"name"` + Phone string `json:"phone"` + Rating float32 `json:"rating"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + DistanceKm float64 `json:"distance_km"` + MatchedTrash []string `json:"matched_trash_ids"` +} + +type SelectCollectorRequest struct { + Collector_id string `json:"collector_id"` +} type CreateCollectorRequest struct { UserID string `json:"user_id" binding:"required"` diff --git a/internal/collector/collector_repository.go b/internal/collector/collector_repository.go index 5bea6c9..5f9dbc7 100644 --- a/internal/collector/collector_repository.go +++ b/internal/collector/collector_repository.go @@ -30,6 +30,9 @@ type CollectorRepository interface { BulkUpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashList []*model.AvaibleTrashByCollector) error DeleteAvailableTrashByCollectorID(ctx context.Context, collectorID string) error + GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) + GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) + WithTx(tx *gorm.DB) CollectorRepository } @@ -56,6 +59,36 @@ func (r *collectorRepository) Create(ctx context.Context, collector *model.Colle return nil } +func (r *collectorRepository) GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) { + var collectors []model.Collector + err := r.db.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("AvaibleTrashbyCollector.TrashCategory"). + Where("job_status = ?", "active"). + Find(&collectors).Error + + if err != nil { + return nil, err + } + + return collectors, nil +} + +func (r *collectorRepository) GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) { + var collector model.Collector + err := r.db.WithContext(ctx). + Preload("Address"). + Preload("AvaibleTrashbyCollector"). + Where("id = ?", collectorID). + First(&collector).Error + + if err != nil { + return nil, err + } + return &collector, nil +} + func (r *collectorRepository) GetByID(ctx context.Context, id string) (*model.Collector, error) { var collector model.Collector diff --git a/internal/company/company_dto.go b/internal/company/company_dto.go index eed6e4d..979f73b 100644 --- a/internal/company/company_dto.go +++ b/internal/company/company_dto.go @@ -39,19 +39,19 @@ func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]s errors := make(map[string][]string) if strings.TrimSpace(r.CompanyName) == "" { - errors["company_Name"] = append(errors["company_name"], "Company name is required") + errors["company_name"] = append(errors["company_name"], "Company name is required") } if strings.TrimSpace(r.CompanyAddress) == "" { - errors["company_Address"] = append(errors["company_address"], "Company address is required") + errors["company_address"] = append(errors["company_address"], "Company address is required") } if !utils.IsValidPhoneNumber(r.CompanyPhone) { - errors["company_Phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit") + errors["company_phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit") } if strings.TrimSpace(r.CompanyDescription) == "" { - errors["company_Description"] = append(errors["company_description"], "Company description is required") + errors["company_description"] = append(errors["company_description"], "Company description is required") } if len(errors) > 0 { diff --git a/internal/company/company_handler.go b/internal/company/company_handler.go index dcdd13e..bdf6ee0 100644 --- a/internal/company/company_handler.go +++ b/internal/company/company_handler.go @@ -2,7 +2,10 @@ package company import ( "context" + "log" + "rijig/middleware" "rijig/utils" + "strings" "github.com/gofiber/fiber/v2" ) @@ -18,9 +21,10 @@ func NewCompanyProfileHandler(service CompanyProfileService) *CompanyProfileHand } func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error { - userID, ok := c.Locals("user_id").(string) - if !ok || userID == "" { - return utils.Unauthorized(c, "User not authenticated") + claims, err := middleware.GetUserFromContext(c) + if err != nil { + log.Printf("Error getting user from context: %v", err) + return utils.Unauthorized(c, "unauthorized access") } var req RequestCompanyProfileDTO @@ -32,9 +36,19 @@ func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error { return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors) } - res, err := h.service.CreateCompanyProfile(context.Background(), userID, &req) + companyLogo, err := c.FormFile("company_logo") if err != nil { - return utils.InternalServerError(c, err.Error()) + log.Printf("Error getting company logo: %v", err) + return utils.BadRequest(c, "company logo is required") + } + + res, err := h.service.CreateCompanyProfile(c.Context(), claims.UserID, claims.DeviceID, &req, companyLogo) + if err != nil { + log.Printf("Error creating identity card: %v", err) + if strings.Contains(err.Error(), "invalid file type") { + return utils.BadRequest(c, err.Error()) + } + return utils.InternalServerError(c, "Failed to create company logo") } return utils.SuccessWithData(c, "company profile created successfully", res) diff --git a/internal/company/company_route.go b/internal/company/company_route.go index 4e8b5ae..8476568 100644 --- a/internal/company/company_route.go +++ b/internal/company/company_route.go @@ -3,6 +3,7 @@ package company import ( "rijig/config" "rijig/internal/authentication" + "rijig/internal/userprofile" "rijig/middleware" "github.com/gofiber/fiber/v2" @@ -11,7 +12,8 @@ import ( func CompanyRouter(api fiber.Router) { companyProfileRepo := NewCompanyProfileRepository(config.DB) authRepo := authentication.NewAuthenticationRepository(config.DB) - companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo) + userRepo := userprofile.NewUserProfileRepository(config.DB) + companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo, userRepo) companyProfileHandler := NewCompanyProfileHandler(companyProfileService) companyProfileAPI := api.Group("/companyprofile") diff --git a/internal/company/company_service.go b/internal/company/company_service.go index 291086a..bad3652 100644 --- a/internal/company/company_service.go +++ b/internal/company/company_service.go @@ -2,8 +2,13 @@ package company import ( "context" + "errors" "fmt" + "io" "log" + "mime/multipart" + "os" + "path/filepath" "rijig/internal/authentication" "rijig/internal/role" "rijig/internal/userprofile" @@ -13,7 +18,7 @@ import ( ) type CompanyProfileService interface { - CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) + CreateCompanyProfile(ctx context.Context, userID, deviceID string, request *RequestCompanyProfileDTO, companyLogo *multipart.FileHeader) (*authentication.AuthResponse, error) GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) @@ -25,12 +30,15 @@ type CompanyProfileService interface { type companyProfileService struct { companyRepo CompanyProfileRepository - authRepo authentication.AuthenticationRepository + authRepo authentication.AuthenticationRepository + userRepo userprofile.UserProfileRepository } -func NewCompanyProfileService(companyRepo CompanyProfileRepository, authRepo authentication.AuthenticationRepository) CompanyProfileService { +func NewCompanyProfileService(companyRepo CompanyProfileRepository, + authRepo authentication.AuthenticationRepository, + userRepo userprofile.UserProfileRepository) CompanyProfileService { return &companyProfileService{ - companyRepo, authRepo, + companyRepo, authRepo, userRepo, } } @@ -56,10 +64,73 @@ func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*Respon }, nil } -func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) { - // if errors, valid := request.ValidateCompanyProfileInput(); !valid { - // return nil, fmt.Errorf("validation failed: %v", errors) - // } +func (s *companyProfileService) saveCompanyLogo(userID string, companyLogo *multipart.FileHeader) (string, error) { + pathImage := "/uploads/companyprofile/" + companyLogoDir := "./public" + os.Getenv("BASE_URL") + pathImage + if _, err := os.Stat(companyLogoDir); os.IsNotExist(err) { + + if err := os.MkdirAll(companyLogoDir, os.ModePerm); err != nil { + return "", fmt.Errorf("failed to create directory for company logo: %v", err) + } + } + + allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true} + extension := filepath.Ext(companyLogo.Filename) + if !allowedExtensions[extension] { + return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed") + } + + companyLogoFileName := fmt.Sprintf("%s_companylogo%s", userID, extension) + companyLogoPath := filepath.Join(companyLogoDir, companyLogoFileName) + + src, err := companyLogo.Open() + if err != nil { + return "", fmt.Errorf("failed to open uploaded file: %v", err) + } + defer src.Close() + + dst, err := os.Create(companyLogoPath) + if err != nil { + return "", fmt.Errorf("failed to create company logo: %v", err) + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return "", fmt.Errorf("failed to save company logo: %v", err) + } + + companyLogoURL := fmt.Sprintf("%s%s", pathImage, companyLogoFileName) + + return companyLogoURL, nil +} + +func deleteIdentityCardImage(imagePath string) error { + if imagePath == "" { + return nil + } + + baseDir := "./public/" + os.Getenv("BASE_URL") + absolutePath := baseDir + imagePath + + if _, err := os.Stat(absolutePath); os.IsNotExist(err) { + return fmt.Errorf("image file not found: %v", err) + } + + err := os.Remove(absolutePath) + if err != nil { + return fmt.Errorf("failed to delete image: %v", err) + } + + log.Printf("Image deleted successfully: %s", absolutePath) + return nil +} + +func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID, deviceID string, request *RequestCompanyProfileDTO, companyLogo *multipart.FileHeader) (*authentication.AuthResponse, error) { + + companyLogoPath, err := s.saveCompanyLogo(userID, companyLogo) + if err != nil { + return nil, fmt.Errorf("failed to save company logo: %v", err) + } companyProfile := &model.CompanyProfile{ UserID: userID, @@ -67,7 +138,7 @@ func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID CompanyAddress: request.CompanyAddress, CompanyPhone: request.CompanyPhone, CompanyEmail: request.CompanyEmail, - CompanyLogo: request.CompanyLogo, + CompanyLogo: companyLogoPath, CompanyWebsite: request.CompanyWebsite, TaxID: request.TaxID, FoundedDate: request.FoundedDate, @@ -75,12 +146,72 @@ func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID CompanyDescription: request.CompanyDescription, } - created, err := s.companyRepo.CreateCompanyProfile(ctx, companyProfile) + _, err = s.companyRepo.CreateCompanyProfile(ctx, companyProfile) if err != nil { return nil, err } - return FormatResponseCompanyProfile(created) + user, err := s.authRepo.FindUserByID(ctx, userID) + if err != nil { + return nil, fmt.Errorf("failed to find user: %v", err) + } + + if user.Role.RoleName == "" { + return nil, fmt.Errorf("user role not found") + } + + updates := map[string]interface{}{ + "registration_progress": utils.ProgressDataSubmitted, + "registration_status": utils.RegStatusPending, + } + + err = s.authRepo.PatchUser(ctx, userID, updates) + if err != nil { + return nil, fmt.Errorf("failed to update user: %v", err) + } + + updated, err := s.userRepo.GetByID(ctx, userID) + if err != nil { + if errors.Is(err, userprofile.ErrUserNotFound) { + return nil, fmt.Errorf("user not found") + } + return nil, fmt.Errorf("failed to get updated user: %w", err) + } + + log.Printf("Token Generation Parameters:") + log.Printf("- UserID: '%s'", user.ID) + log.Printf("- Role: '%s'", user.Role.RoleName) + log.Printf("- DeviceID: '%s'", deviceID) + log.Printf("- Registration Status: '%s'", utils.RegStatusPending) + + tokenResponse, err := utils.GenerateTokenPair( + updated.ID, + updated.Role.RoleName, + deviceID, + updated.RegistrationStatus, + int(updated.RegistrationProgress), + ) + if err != nil { + log.Printf("GenerateTokenPair error: %v", err) + return nil, fmt.Errorf("failed to generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep( + updated.Role.RoleName, + int(updated.RegistrationProgress), + updated.RegistrationStatus, + ) + + return &authentication.AuthResponse{ + Message: "data usaha anda berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: updated.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil } func (s *companyProfileService) GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) { diff --git a/internal/requestpickup/pickup_history_repository.go b/internal/requestpickup/pickup_history_repository.go new file mode 100644 index 0000000..d8af037 --- /dev/null +++ b/internal/requestpickup/pickup_history_repository.go @@ -0,0 +1,34 @@ +package requestpickup + +import ( + "context" + "rijig/config" + "rijig/model" +) + +type PickupStatusHistoryRepository interface { + CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error + GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) +} + +type pickupStatusHistoryRepository struct{} + +func NewPickupStatusHistoryRepository() PickupStatusHistoryRepository { + return &pickupStatusHistoryRepository{} +} + +func (r *pickupStatusHistoryRepository) CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error { + return config.DB.WithContext(ctx).Create(&history).Error +} + +func (r *pickupStatusHistoryRepository) GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) { + var histories []model.PickupStatusHistory + err := config.DB.WithContext(ctx). + Where("request_id = ?", requestID). + Order("changed_at asc"). + Find(&histories).Error + if err != nil { + return nil, err + } + return histories, nil +} diff --git a/internal/requestpickup/pickup_maching_service.go b/internal/requestpickup/pickup_maching_service.go new file mode 100644 index 0000000..4ecf57b --- /dev/null +++ b/internal/requestpickup/pickup_maching_service.go @@ -0,0 +1,146 @@ +package requestpickup + +import ( + "context" + "fmt" + "rijig/internal/collector" + "rijig/utils" +) + +type PickupMatchingService interface { + FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]collector.NearbyCollectorDTO, error) + FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]PickupRequestForCollectorDTO, error) +} + +type pickupMatchingService struct { + pickupRepo RequestPickupRepository + collectorRepo collector.CollectorRepository +} + +func NewPickupMatchingService(pickupRepo RequestPickupRepository, + collectorRepo collector.CollectorRepository) PickupMatchingService { + return &pickupMatchingService{ + pickupRepo: pickupRepo, + collectorRepo: collectorRepo, + } +} + +func (s *pickupMatchingService) FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]collector.NearbyCollectorDTO, error) { + pickup, err := s.pickupRepo.GetPickupWithItemsAndAddress(ctx, pickupID) + if err != nil { + return nil, fmt.Errorf("pickup tidak ditemukan: %w", err) + } + + userCoord := utils.Coord{ + Lat: pickup.Address.Latitude, + Lon: pickup.Address.Longitude, + } + + requestedTrash := make(map[string]bool) + for _, item := range pickup.RequestItems { + requestedTrash[item.TrashCategoryId] = true + } + + collectors, err := s.collectorRepo.GetActiveCollectorsWithTrashAndAddress(ctx) + if err != nil { + return nil, fmt.Errorf("gagal mengambil data collector: %w", err) + } + + var result []collector.NearbyCollectorDTO + for _, col := range collectors { + coord := utils.Coord{ + Lat: col.Address.Latitude, + Lon: col.Address.Longitude, + } + + _, km := utils.Distance(userCoord, coord) + if km > 10 { + continue + } + + var matchedTrash []string + for _, item := range col.AvaibleTrashByCollector { + if requestedTrash[item.TrashCategoryID] { + matchedTrash = append(matchedTrash, item.TrashCategoryID) + } + } + + if len(matchedTrash) == 0 { + continue + } + + result = append(result, collector.NearbyCollectorDTO{ + CollectorID: col.ID, + Name: col.User.Name, + Phone: col.User.Phone, + Rating: col.Rating, + Latitude: col.Address.Latitude, + Longitude: col.Address.Longitude, + DistanceKm: km, + MatchedTrash: matchedTrash, + }) + } + + return result, nil +} + +// terdpaat error seperti ini: "undefined: dto.PickupRequestForCollectorDTO" dan seprti ini: s.collectorRepo.GetCollectorWithAddressAndTrash undefined (type repositories.CollectorRepository has no field or method GetCollectorWithAddressAndTrash) pada kode berikut: + +func (s *pickupMatchingService) FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]PickupRequestForCollectorDTO, error) { + collector, err := s.collectorRepo.GetCollectorWithAddressAndTrash(ctx, collectorID) + if err != nil { + return nil, fmt.Errorf("collector tidak ditemukan: %w", err) + } + + pickupList, err := s.pickupRepo.GetAllAutomaticRequestsWithAddress(ctx) + if err != nil { + return nil, fmt.Errorf("gagal mengambil pickup otomatis: %w", err) + } + + collectorCoord := utils.Coord{ + Lat: collector.Address.Latitude, + Lon: collector.Address.Longitude, + } + + // map trash collector + collectorTrash := make(map[string]bool) + for _, t := range collector.AvaibleTrashByCollector { + collectorTrash[t.TrashCategoryID] = true + } + + var results []PickupRequestForCollectorDTO + for _, p := range pickupList { + if p.StatusPickup != "waiting_collector" { + continue + } + coord := utils.Coord{ + Lat: p.Address.Latitude, + Lon: p.Address.Longitude, + } + _, km := utils.Distance(collectorCoord, coord) + if km > 10 { + continue + } + + match := false + var matchedTrash []string + for _, item := range p.RequestItems { + if collectorTrash[item.TrashCategoryId] { + match = true + matchedTrash = append(matchedTrash, item.TrashCategoryId) + } + } + if match { + results = append(results, PickupRequestForCollectorDTO{ + PickupID: p.ID, + UserID: p.UserId, + Latitude: p.Address.Latitude, + Longitude: p.Address.Longitude, + DistanceKm: km, + MatchedTrash: matchedTrash, + }) + } + } + + return results, nil +} diff --git a/internal/requestpickup/pickup_matching_handler.go b/internal/requestpickup/pickup_matching_handler.go new file mode 100644 index 0000000..b3dc2f7 --- /dev/null +++ b/internal/requestpickup/pickup_matching_handler.go @@ -0,0 +1,50 @@ +package requestpickup + +import ( + "context" + "rijig/middleware" + "rijig/utils" + + "github.com/gofiber/fiber/v2" +) + +type PickupMatchingHandler struct { + service PickupMatchingService +} + +func NewPickupMatchingHandler(service PickupMatchingService) *PickupMatchingHandler { + return &PickupMatchingHandler{ + service: service, + + } +} + +func (h *PickupMatchingHandler) GetNearbyCollectorsForPickup(c *fiber.Ctx) error { + pickupID := c.Params("pickupID") + if pickupID == "" { + return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validasi gagal", map[string][]string{ + "pickup_id": {"pickup ID harus disertakan"}, + }) + } + + collectors, err := h.service.FindNearbyCollectorsForPickup(context.Background(), pickupID) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "Data collector terdekat berhasil diambil", collectors) +} + +func (h *PickupMatchingHandler) GetAvailablePickupForCollector(c *fiber.Ctx) error { + claims, err := middleware.GetUserFromContext(c) + if err != nil { + return err + } + + pickups, err := h.service.FindAvailableRequestsForCollector(context.Background(), claims.UserID) + if err != nil { + return utils.InternalServerError(c, err.Error()) + } + + return utils.SuccessWithData(c, "Data request pickup otomatis berhasil diambil", pickups) +} diff --git a/internal/requestpickup/requestpickup_dto.go b/internal/requestpickup/requestpickup_dto.go index df31b39..2ddf72e 100644 --- a/internal/requestpickup/requestpickup_dto.go +++ b/internal/requestpickup/requestpickup_dto.go @@ -1 +1,74 @@ -package requestpickup \ No newline at end of file +package requestpickup + +import ( + "strings" +) + +type SelectCollectorDTO struct { + CollectorID string `json:"collector_id"` +} + +type UpdateRequestPickupItemDTO struct { + ItemID string `json:"item_id"` + Amount float64 `json:"actual_amount"` +} + +type UpdatePickupItemsRequest struct { + Items []UpdateRequestPickupItemDTO `json:"items"` +} + +func (r *SelectCollectorDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CollectorID) == "" { + errors["collector_id"] = append(errors["collector_id"], "collector_id tidak boleh kosong") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} + +type AssignedPickupDTO struct { + PickupID string `json:"pickup_id"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Notes string `json:"notes"` + MatchedTrash []string `json:"matched_trash"` +} + +type PickupRequestForCollectorDTO struct { + PickupID string `json:"pickup_id"` + UserID string `json:"user_id"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + DistanceKm float64 `json:"distance_km"` + MatchedTrash []string `json:"matched_trash"` +} + +type RequestPickupDTO struct { + AddressID string `json:"address_id"` + RequestMethod string `json:"request_method"` + Notes string `json:"notes,omitempty"` +} + +func (r *RequestPickupDTO) Validate() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.AddressID) == "" { + errors["address_id"] = append(errors["address_id"], "alamat harus dipilih") + } + + method := strings.ToLower(strings.TrimSpace(r.RequestMethod)) + if method != "manual" && method != "otomatis" { + errors["request_method"] = append(errors["request_method"], "harus manual atau otomatis") + } + + if len(errors) > 0 { + return errors, false + } + return nil, true +} diff --git a/internal/requestpickup/requestpickup_repository.go b/internal/requestpickup/requestpickup_repository.go index df31b39..7d2861a 100644 --- a/internal/requestpickup/requestpickup_repository.go +++ b/internal/requestpickup/requestpickup_repository.go @@ -1 +1,142 @@ -package requestpickup \ No newline at end of file +package requestpickup + +import ( + "context" + "rijig/config" + "rijig/model" + "time" +) + +type RequestPickupRepository interface { + CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error + GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) + GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) + UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error + GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) + UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error + UpdatePickupStatus(ctx context.Context, pickupID string, status string) error + UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []UpdateRequestPickupItemDTO) error +} + +type requestPickupRepository struct{} + +func NewRequestPickupRepository() RequestPickupRepository { + return &requestPickupRepository{} +} + +func (r *requestPickupRepository) CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error { + return config.DB.WithContext(ctx).Create(pickup).Error +} + +func (r *requestPickupRepository) GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) { + var pickup model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("RequestItems"). + Preload("Address"). + Where("id = ?", id). + First(&pickup).Error + + if err != nil { + return nil, err + } + return &pickup, nil +} + +func (r *requestPickupRepository) UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Update("collector_id", collectorID). + Error +} + +func (r *requestPickupRepository) GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) { + var pickups []model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("RequestItems"). + Preload("Address"). + Where("request_method = ?", "otomatis"). + Find(&pickups).Error + + if err != nil { + return nil, err + } + return pickups, nil +} + +func (r *requestPickupRepository) GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) { + var pickups []model.RequestPickup + err := config.DB.WithContext(ctx). + Preload("User"). + Preload("Address"). + Preload("RequestItems"). + Where("collector_id = ? AND status_pickup = ?", collectorID, "waiting_collector"). + Find(&pickups).Error + + if err != nil { + return nil, err + } + return pickups, nil +} + +func (r *requestPickupRepository) UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Updates(map[string]interface{}{ + "status_pickup": status, + "confirmed_by_collector_at": confirmedAt, + }).Error +} + +func (r *requestPickupRepository) UpdatePickupStatus(ctx context.Context, pickupID string, status string) error { + return config.DB.WithContext(ctx). + Model(&model.RequestPickup{}). + Where("id = ?", pickupID). + Update("status_pickup", status). + Error +} + +func (r *requestPickupRepository) UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []UpdateRequestPickupItemDTO) error { + // ambil collector_id dulu dari pickup + var pickup model.RequestPickup + if err := config.DB.WithContext(ctx). + Select("collector_id"). + Where("id = ?", pickupID). + First(&pickup).Error; err != nil { + return err + } + + for _, item := range items { + var pickupItem model.RequestPickupItem + err := config.DB.WithContext(ctx). + Where("id = ? AND request_pickup_id = ?", item.ItemID, pickupID). + First(&pickupItem).Error + if err != nil { + return err + } + + var price float64 + err = config.DB.WithContext(ctx). + Model(&model.AvaibleTrashByCollector{}). + Where("collector_id = ? AND trash_category_id = ?", pickup.CollectorID, pickupItem.TrashCategoryId). + Select("price"). + Scan(&price).Error + if err != nil { + return err + } + + finalPrice := item.Amount * price + err = config.DB.WithContext(ctx). + Model(&model.RequestPickupItem{}). + Where("id = ?", item.ItemID). + Updates(map[string]interface{}{ + "estimated_amount": item.Amount, + "final_price": finalPrice, + }).Error + if err != nil { + return err + } + } + return nil +} diff --git a/internal/trash/trash_repository.go b/internal/trash/trash_repository.go index 8041af3..638cc2d 100644 --- a/internal/trash/trash_repository.go +++ b/internal/trash/trash_repository.go @@ -32,24 +32,24 @@ type TrashRepositoryInterface interface { GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) } -type TrashRepository struct { +type trashRepository struct { db *gorm.DB } func NewTrashRepository(db *gorm.DB) TrashRepositoryInterface { - return &TrashRepository{ - db: db, + return &trashRepository{ + db, } } -func (r *TrashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error { +func (r *trashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error { if err := r.db.WithContext(ctx).Create(category).Error; err != nil { return fmt.Errorf("failed to create trash category: %w", err) } return nil } -func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error { +func (r *trashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Create(category).Error; err != nil { @@ -75,7 +75,7 @@ func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, ca }) } -func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error { +func (r *trashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error { exists, err := r.CheckTrashCategoryExists(ctx, id) if err != nil { @@ -99,7 +99,7 @@ func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, up return nil } -func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) { +func (r *trashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) { var categories []model.TrashCategory if err := r.db.WithContext(ctx).Find(&categories).Error; err != nil { @@ -109,7 +109,7 @@ func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.Tr return categories, nil } -func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) { +func (r *trashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) { var categories []model.TrashCategory if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB { @@ -121,7 +121,7 @@ func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) return categories, nil } -func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { +func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) { var category model.TrashCategory if err := r.db.WithContext(ctx).Where("id = ?", id).First(&category).Error; err != nil { @@ -134,7 +134,7 @@ func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) ( return &category, nil } -func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) { +func (r *trashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) { var category model.TrashCategory if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB { @@ -149,7 +149,7 @@ func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, i return &category, nil } -func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) error { +func (r *trashRepository) DeleteTrashCategory(ctx context.Context, id string) error { exists, err := r.CheckTrashCategoryExists(ctx, id) if err != nil { @@ -171,7 +171,7 @@ func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) er return nil } -func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error { +func (r *trashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error { exists, err := r.CheckTrashCategoryExists(ctx, detail.TrashCategoryID) if err != nil { @@ -196,7 +196,7 @@ func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.T return nil } -func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error { +func (r *trashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error { exists, err := r.CheckTrashCategoryExists(ctx, categoryID) if err != nil { @@ -223,7 +223,7 @@ func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, category return nil } -func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error { +func (r *trashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error { exists, err := r.CheckTrashDetailExists(ctx, id) if err != nil { @@ -247,7 +247,7 @@ func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, upda return nil } -func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) { +func (r *trashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) { var details []model.TrashDetail if err := r.db.WithContext(ctx).Where("trash_category_id = ?", categoryID).Order("step_order ASC").Find(&details).Error; err != nil { @@ -257,7 +257,7 @@ func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categor return details, nil } -func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) { +func (r *trashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) { var detail model.TrashDetail if err := r.db.WithContext(ctx).Where("id = ?", id).First(&detail).Error; err != nil { @@ -270,7 +270,7 @@ func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*m return &detail, nil } -func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) error { +func (r *trashRepository) DeleteTrashDetail(ctx context.Context, id string) error { exists, err := r.CheckTrashDetailExists(ctx, id) if err != nil { @@ -292,7 +292,7 @@ func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) erro return nil } -func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) { +func (r *trashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) { var count int64 if err := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Count(&count).Error; err != nil { @@ -302,7 +302,7 @@ func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id strin return count > 0, nil } -func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) { +func (r *trashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) { var count int64 if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Count(&count).Error; err != nil { @@ -312,7 +312,7 @@ func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string) return count > 0, nil } -func (r *TrashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) { +func (r *trashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) { var maxOrder int if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}). diff --git a/internal/trash/trash_route.go b/internal/trash/trash_route.go index fb1a11d..f2c201c 100644 --- a/internal/trash/trash_route.go +++ b/internal/trash/trash_route.go @@ -1 +1,84 @@ -package trash \ No newline at end of file +// ===internal/trash/trash_route.go=== +package trash + +import ( + "rijig/config" + "rijig/middleware" + + "github.com/gofiber/fiber/v2" +) + +func TrashRouter(api fiber.Router) { + trashRepo := NewTrashRepository(config.DB) + trashService := NewTrashService(trashRepo) + trashHandler := NewTrashHandler(trashService) + + trashAPI := api.Group("/trash") + trashAPI.Use(middleware.AuthMiddleware()) + + // ============= TRASH CATEGORY ROUTES ============= + + // Create trash category (JSON) + trashAPI.Post("/category", trashHandler.CreateTrashCategory) + + // Create trash category with icon (form-data) + trashAPI.Post("/category/with-icon", trashHandler.CreateTrashCategoryWithIcon) + + // Create trash category with details (JSON) + trashAPI.Post("/category/with-details", trashHandler.CreateTrashCategoryWithDetails) + + // Get all trash categories (with optional query param: ?with_details=true) + trashAPI.Get("/category", trashHandler.GetAllTrashCategories) + + // Get trash category by ID (with optional query param: ?with_details=true) + trashAPI.Get("/category/:id", trashHandler.GetTrashCategoryByID) + + // Update trash category (JSON) + trashAPI.Put("/category/:id", trashHandler.UpdateTrashCategory) + + // Update trash category with icon (form-data) + trashAPI.Put("/category/:id/with-icon", trashHandler.UpdateTrashCategoryWithIcon) + + // Delete trash category + trashAPI.Delete("/category/:id", trashHandler.DeleteTrashCategory) + + // ============= TRASH DETAIL ROUTES ============= + + // Create trash detail (JSON) + trashAPI.Post("/detail", trashHandler.CreateTrashDetail) + + // Create trash detail with icon (form-data) + trashAPI.Post("/detail/with-icon", trashHandler.CreateTrashDetailWithIcon) + + // Add trash detail to specific category (JSON) + trashAPI.Post("/category/:categoryId/detail", trashHandler.AddTrashDetailToCategory) + + // Add trash detail to specific category with icon (form-data) + trashAPI.Post("/category/:categoryId/detail/with-icon", trashHandler.AddTrashDetailToCategoryWithIcon) + + // Get trash details by category ID + trashAPI.Get("/category/:categoryId/details", trashHandler.GetTrashDetailsByCategory) + + // Get trash detail by ID + trashAPI.Get("/detail/:id", trashHandler.GetTrashDetailByID) + + // Update trash detail (JSON) + trashAPI.Put("/detail/:id", trashHandler.UpdateTrashDetail) + + // Update trash detail with icon (form-data) + trashAPI.Put("/detail/:id/with-icon", trashHandler.UpdateTrashDetailWithIcon) + + // Delete trash detail + trashAPI.Delete("/detail/:id", trashHandler.DeleteTrashDetail) + + // ============= BULK OPERATIONS ROUTES ============= + + // Bulk create trash details for specific category + trashAPI.Post("/category/:categoryId/details/bulk", trashHandler.BulkCreateTrashDetails) + + // Bulk delete trash details + trashAPI.Delete("/details/bulk", trashHandler.BulkDeleteTrashDetails) + + // Reorder trash details within a category + trashAPI.Put("/category/:categoryId/details/reorder", trashHandler.ReorderTrashDetails) +} diff --git a/internal/userpin/userpin_service.go b/internal/userpin/userpin_service.go index 1c663b2..56da709 100644 --- a/internal/userpin/userpin_service.go +++ b/internal/userpin/userpin_service.go @@ -8,14 +8,13 @@ import ( "rijig/internal/userprofile" "rijig/model" "rijig/utils" - "strings" "gorm.io/gorm" ) type UserPinService interface { CreateUserPin(ctx context.Context, userID, deviceId string, dto *RequestPinDTO) (*authentication.AuthResponse, error) - VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error) + VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*authentication.AuthResponse, error) } type userPinService struct { @@ -100,7 +99,7 @@ func (s *userPinService) CreateUserPin(ctx context.Context, userID, deviceId str ) return &authentication.AuthResponse{ - Message: "Isi data diri berhasil", + Message: "mantap semuanya completed", AccessToken: tokenResponse.AccessToken, RefreshToken: tokenResponse.RefreshToken, TokenType: string(tokenResponse.TokenType), @@ -111,7 +110,7 @@ func (s *userPinService) CreateUserPin(ctx context.Context, userID, deviceId str }, nil } -func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error) { +func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*authentication.AuthResponse, error) { user, err := s.authRepo.FindUserByID(ctx, userID) if err != nil { return nil, fmt.Errorf("user not found") @@ -126,6 +125,42 @@ func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID str return nil, fmt.Errorf("PIN does not match, %s , %s", userPin.Pin, pin.Pin) } - roleName := strings.ToLower(user.Role.RoleName) - return utils.GenerateTokenPair(user.ID, roleName, deviceID, user.RegistrationStatus, int(user.RegistrationProgress)) + // roleName := strings.ToLower(user.Role.RoleName) + + // updated, err := s.userProfileRepo.GetByID(ctx, userID) + // if err != nil { + // if errors.Is(err, userprofile.ErrUserNotFound) { + // return nil, fmt.Errorf("user not found") + // } + // return nil, fmt.Errorf("failed to get updated user: %w", err) + // } + + tokenResponse, err := utils.GenerateTokenPair( + user.ID, + user.Role.RoleName, + deviceID, + user.RegistrationStatus, + int(user.RegistrationProgress), + ) + + if err != nil { + return nil, fmt.Errorf("gagal generate token: %v", err) + } + + nextStep := utils.GetNextRegistrationStep( + user.Role.RoleName, + int(user.RegistrationProgress), + user.RegistrationStatus, + ) + + return &authentication.AuthResponse{ + Message: "mantap semuanya completed", + AccessToken: tokenResponse.AccessToken, + RefreshToken: tokenResponse.RefreshToken, + TokenType: string(tokenResponse.TokenType), + ExpiresIn: tokenResponse.ExpiresIn, + RegistrationStatus: user.RegistrationStatus, + NextStep: nextStep, + SessionID: tokenResponse.SessionID, + }, nil } diff --git a/internal/whatsapp/whatsapp_handler.go b/internal/whatsapp/whatsapp_handler.go index 216c20b..fe71abb 100644 --- a/internal/whatsapp/whatsapp_handler.go +++ b/internal/whatsapp/whatsapp_handler.go @@ -1,156 +1,311 @@ package whatsapp import ( - "html/template" - "path/filepath" + "regexp" "rijig/config" + "rijig/utils" + "strings" + "time" "github.com/gofiber/fiber/v2" ) -type APIResponse struct { - Meta map[string]interface{} `json:"meta"` - Data interface{} `json:"data,omitempty"` +type QRResponse struct { + QRCode string `json:"qr_code,omitempty"` + Status string `json:"status"` + Message string `json:"message"` + Timestamp int64 `json:"timestamp"` } -func WhatsAppQRPageHandler(c *fiber.Ctx) error { +type StatusResponse struct { + IsConnected bool `json:"is_connected"` + IsLoggedIn bool `json:"is_logged_in"` + Status string `json:"status"` + Message string `json:"message"` + Timestamp int64 `json:"timestamp"` +} + +type SendMessageRequest struct { + PhoneNumber string `json:"phone_number" validate:"required"` + Message string `json:"message" validate:"required"` +} + +type SendMessageResponse struct { + PhoneNumber string `json:"phone_number"` + Timestamp int64 `json:"timestamp"` +} + +func GenerateQRHandler(c *fiber.Ctx) error { wa := config.GetWhatsAppService() if wa == nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "WhatsApp service not initialized", - }, - }) + return utils.InternalServerError(c, "WhatsApp service not initialized") } - // Jika sudah login, tampilkan halaman success if wa.IsLoggedIn() { - templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") - tmpl, err := template.ParseFiles(templatePath) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "Unable to load success template: " + err.Error(), - }, - }) + data := QRResponse{ + Status: "logged_in", + Message: "WhatsApp is already connected and logged in", + Timestamp: time.Now().Unix(), } - - c.Set("Content-Type", "text/html") - return tmpl.Execute(c.Response().BodyWriter(), nil) + return utils.SuccessWithData(c, "Already logged in", data) } qrDataURI, err := wa.GenerateQR() if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "Failed to generate QR code: " + err.Error(), - }, - }) + return utils.InternalServerError(c, "Failed to generate QR code: "+err.Error()) } - if qrDataURI == "success" { - // Login berhasil, tampilkan halaman success - templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") - tmpl, err := template.ParseFiles(templatePath) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "Unable to load success template: " + err.Error(), - }, - }) + switch qrDataURI { + case "success": + data := QRResponse{ + Status: "login_success", + Message: "WhatsApp login successful", + Timestamp: time.Now().Unix(), } + return utils.SuccessWithData(c, "Successfully logged in", data) - c.Set("Content-Type", "text/html") - return tmpl.Execute(c.Response().BodyWriter(), nil) - } - - if qrDataURI == "already_connected" { - // Sudah terhubung, tampilkan halaman success - templatePath := filepath.Join("internal", "whatsapp", "success_scan.html") - tmpl, err := template.ParseFiles(templatePath) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "Unable to load success template: " + err.Error(), - }, - }) + case "already_connected": + data := QRResponse{ + Status: "already_connected", + Message: "WhatsApp is already connected", + Timestamp: time.Now().Unix(), } + return utils.SuccessWithData(c, "Already connected", data) - c.Set("Content-Type", "text/html") - return tmpl.Execute(c.Response().BodyWriter(), nil) + default: + + data := QRResponse{ + QRCode: qrDataURI, + Status: "qr_generated", + Message: "Scan QR code with WhatsApp to login", + Timestamp: time.Now().Unix(), + } + return utils.SuccessWithData(c, "QR code generated successfully", data) + } +} + +func CheckLoginStatusHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return utils.InternalServerError(c, "WhatsApp service not initialized") } - // Tampilkan QR code scanner - templatePath := filepath.Join("internal", "whatsapp", "scanner.html") - tmpl, err := template.ParseFiles(templatePath) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "Unable to load scanner template: " + err.Error(), - }, - }) + // if !wa.IsLoggedIn() { + // return utils.Unauthorized(c, "WhatsApp not logged in") + // } + + isConnected := wa.IsConnected() + isLoggedIn := wa.IsLoggedIn() + + var status string + var message string + + if isLoggedIn && isConnected { + status = "connected_and_logged_in" + message = "WhatsApp is connected and logged in" + } else if isLoggedIn { + status = "logged_in_but_disconnected" + message = "WhatsApp is logged in but disconnected" + } else if isConnected { + status = "connected_but_not_logged_in" + message = "WhatsApp is connected but not logged in" + } else { + status = "disconnected" + message = "WhatsApp is disconnected" } - c.Set("Content-Type", "text/html") - return tmpl.Execute(c.Response().BodyWriter(), template.URL(qrDataURI)) + data := StatusResponse{ + IsConnected: isConnected, + IsLoggedIn: isLoggedIn, + Status: status, + Message: message, + Timestamp: time.Now().Unix(), + } + + return utils.SuccessWithData(c, "Status retrieved successfully", data) } func WhatsAppLogoutHandler(c *fiber.Ctx) error { wa := config.GetWhatsAppService() if wa == nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "WhatsApp service not initialized", - }, - }) + return utils.InternalServerError(c, "WhatsApp service not initialized") + } + + if !wa.IsLoggedIn() { + return utils.BadRequest(c, "No active session to logout") } err := wa.Logout() if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": err.Error(), - }, - }) + return utils.InternalServerError(c, "Failed to logout: "+err.Error()) } - return c.Status(fiber.StatusOK).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "success", - "message": "Successfully logged out and session deleted", - }, - }) + data := map[string]interface{}{ + "timestamp": time.Now().Unix(), + } + + return utils.SuccessWithData(c, "Successfully logged out and session deleted", data) } -func WhatsAppStatusHandler(c *fiber.Ctx) error { +func SendMessageHandler(c *fiber.Ctx) error { wa := config.GetWhatsAppService() if wa == nil { - return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "error", - "message": "WhatsApp service not initialized", - }, - }) + return utils.InternalServerError(c, "WhatsApp service not initialized") } - status := map[string]interface{}{ - "is_connected": wa.IsConnected(), - "is_logged_in": wa.IsLoggedIn(), + if !wa.IsLoggedIn() { + return utils.Unauthorized(c, "WhatsApp not logged in") } - return c.Status(fiber.StatusOK).JSON(APIResponse{ - Meta: map[string]interface{}{ - "status": "success", - "message": "WhatsApp status retrieved successfully", - }, - Data: status, - }) + req := GetValidatedSendMessageRequest(c) + if req == nil { + return utils.BadRequest(c, "Invalid request data") + } + + err := wa.SendMessage(req.PhoneNumber, req.Message) + if err != nil { + return utils.InternalServerError(c, "Failed to send message: "+err.Error()) + } + + data := SendMessageResponse{ + PhoneNumber: req.PhoneNumber, + Timestamp: time.Now().Unix(), + } + + return utils.SuccessWithData(c, "Message sent successfully", data) +} + +func GetDeviceInfoHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return utils.InternalServerError(c, "WhatsApp service not initialized") + } + + if !wa.IsLoggedIn() { + return utils.Unauthorized(c, "WhatsApp not logged in") + } + + var deviceInfo map[string]interface{} + if wa.Client != nil && wa.Client.Store.ID != nil { + deviceInfo = map[string]interface{}{ + "device_id": wa.Client.Store.ID.User, + "device_name": wa.Client.Store.ID.Device, + "is_logged_in": wa.IsLoggedIn(), + "is_connected": wa.IsConnected(), + "timestamp": time.Now().Unix(), + } + } else { + deviceInfo = map[string]interface{}{ + "device_id": nil, + "device_name": nil, + "is_logged_in": false, + "is_connected": false, + "timestamp": time.Now().Unix(), + } + } + + return utils.SuccessWithData(c, "Device info retrieved successfully", deviceInfo) +} + +func HealthCheckHandler(c *fiber.Ctx) error { + wa := config.GetWhatsAppService() + if wa == nil { + return utils.InternalServerError(c, "WhatsApp service not initialized") + } + + healthData := map[string]interface{}{ + "service_status": "running", + "container_status": wa.Container != nil, + "client_status": wa.Client != nil, + "is_connected": wa.IsConnected(), + "is_logged_in": wa.IsLoggedIn(), + "timestamp": time.Now().Unix(), + } + + message := "WhatsApp service is healthy" + if !wa.IsConnected() || !wa.IsLoggedIn() { + message = "WhatsApp service is running but not fully operational" + } + + return utils.SuccessWithData(c, message, healthData) +} + +func validatePhoneNumber(phoneNumber string) error { + + cleaned := strings.ReplaceAll(phoneNumber, " ", "") + cleaned = strings.ReplaceAll(cleaned, "-", "") + cleaned = strings.ReplaceAll(cleaned, "+", "") + + if !regexp.MustCompile(`^\d+$`).MatchString(cleaned) { + return fiber.NewError(fiber.StatusBadRequest, "Phone number must contain only digits") + } + + if len(cleaned) < 10 { + return fiber.NewError(fiber.StatusBadRequest, "Phone number too short. Include country code (e.g., 628123456789)") + } + + if len(cleaned) > 15 { + return fiber.NewError(fiber.StatusBadRequest, "Phone number too long") + } + + return nil +} + +func validateMessage(message string) error { + + if strings.TrimSpace(message) == "" { + return fiber.NewError(fiber.StatusBadRequest, "Message cannot be empty") + } + + if len(message) > 4096 { + return fiber.NewError(fiber.StatusBadRequest, "Message too long. Maximum 4096 characters allowed") + } + + return nil +} + +func ValidateSendMessageRequest(c *fiber.Ctx) error { + var req SendMessageRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid JSON format: "+err.Error()) + } + + if err := validatePhoneNumber(req.PhoneNumber); err != nil { + return utils.BadRequest(c, err.Error()) + } + + if err := validateMessage(req.Message); err != nil { + return utils.BadRequest(c, err.Error()) + } + + req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, " ", "") + req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, "-", "") + req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, "+", "") + + c.Locals("validatedRequest", req) + + return c.Next() +} + +func GetValidatedSendMessageRequest(c *fiber.Ctx) *SendMessageRequest { + if req, ok := c.Locals("validatedRequest").(SendMessageRequest); ok { + return &req + } + return nil +} + +func ValidateContentType() fiber.Handler { + return func(c *fiber.Ctx) error { + + if c.Method() == "GET" { + return c.Next() + } + + contentType := c.Get("Content-Type") + if !strings.Contains(contentType, "application/json") { + return utils.BadRequest(c, "Content-Type must be application/json") + } + + return c.Next() + } } diff --git a/internal/whatsapp/whatsapp_route.go b/internal/whatsapp/whatsapp_route.go index 76e2728..b478d9e 100644 --- a/internal/whatsapp/whatsapp_route.go +++ b/internal/whatsapp/whatsapp_route.go @@ -1,11 +1,32 @@ package whatsapp import ( + "rijig/middleware" + "rijig/utils" + "github.com/gofiber/fiber/v2" ) func WhatsAppRouter(api fiber.Router) { - api.Get("/whatsapp-status", WhatsAppStatusHandler) - api.Get("/whatsapp/pw=admin1234", WhatsAppQRPageHandler) - api.Post("/logout/whastapp", WhatsAppLogoutHandler) + + whatsapp := api.Group("/whatsapp") + + whatsapp.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole()) + + whatsapp.Post("/generate-qr", GenerateQRHandler) + whatsapp.Get("/status", CheckLoginStatusHandler) + whatsapp.Post("/logout", WhatsAppLogoutHandler) + + messaging := whatsapp.Group("/message") + messaging.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole()) + messaging.Post("/send", ValidateSendMessageRequest, SendMessageHandler) + + management := whatsapp.Group("/management") + management.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole()) + management.Get("/device-info", GetDeviceInfoHandler) + management.Get("/health", HealthCheckHandler) + + api.Get("/whatsapp/ping", func(c *fiber.Ctx) error { + return utils.Success(c, "WhatsApp service is running") + }) } diff --git a/internal/worker/cart_worker.go b/internal/worker/cart_worker.go index e6bd367..3e82f7a 100644 --- a/internal/worker/cart_worker.go +++ b/internal/worker/cart_worker.go @@ -1,5 +1,5 @@ package worker -/* + import ( "context" "encoding/json" @@ -8,19 +8,18 @@ import ( "time" "rijig/config" - "rijig/dto" - "rijig/internal/repositories" - "rijig/internal/services" + "rijig/internal/cart" + "rijig/internal/trash" "rijig/model" ) type CartWorker struct { - cartService services.CartService - cartRepo repositories.CartRepository - trashRepo repositories.TrashRepository + cartService cart.CartService + cartRepo cart.CartRepository + trashRepo trash.TrashRepositoryInterface } -func NewCartWorker(cartService services.CartService, cartRepo repositories.CartRepository, trashRepo repositories.TrashRepository) *CartWorker { +func NewCartWorker(cartService cart.CartService, cartRepo cart.CartRepository, trashRepo trash.TrashRepositoryInterface) *CartWorker { return &CartWorker{ cartService: cartService, cartRepo: cartRepo, @@ -32,7 +31,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error { ctx := context.Background() threshold := 1 * time.Minute - keys, err := services.GetExpiringCartKeys(ctx, threshold) + keys, err := cart.GetExpiringCartKeys(ctx, threshold) if err != nil { return err } @@ -59,7 +58,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error { if hasCart { - if err := services.DeleteCartFromRedis(ctx, userID); err != nil { + if err := cart.DeleteCartFromRedis(ctx, userID); err != nil { log.Printf("[CART-WORKER] Failed to delete Redis cache for user %s: %v", userID, err) } else { log.Printf("[CART-WORKER] Deleted Redis cache for user %s (already has DB cart)", userID) @@ -78,7 +77,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error { continue } - if err := services.DeleteCartFromRedis(ctx, userID); err != nil { + if err := cart.DeleteCartFromRedis(ctx, userID); err != nil { log.Printf("[CART-WORKER] Warning: Failed to delete Redis key after commit for user %s: %v", userID, err) } @@ -98,13 +97,13 @@ func (w *CartWorker) extractUserIDFromKey(key string) string { return "" } -func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.RequestCartDTO, error) { +func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*cart.RequestCartDTO, error) { val, err := config.RedisClient.Get(ctx, key).Result() if err != nil { return nil, err } - var cart dto.RequestCartDTO + var cart cart.RequestCartDTO if err := json.Unmarshal([]byte(val), &cart); err != nil { return nil, err } @@ -112,7 +111,7 @@ func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.Req return &cart, nil } -func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *dto.RequestCartDTO) error { +func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *cart.RequestCartDTO) error { if len(cartData.CartItems) == 0 { return nil } @@ -156,4 +155,3 @@ func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData return w.cartRepo.CreateCartWithItems(ctx, newCart) } - */ \ No newline at end of file diff --git a/middleware/additional_middleware.go b/middleware/additional_middleware.go deleted file mode 100644 index 95db86a..0000000 --- a/middleware/additional_middleware.go +++ /dev/null @@ -1,199 +0,0 @@ -package middleware -/* -import ( - "fmt" - "time" - - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func RateLimitByUser(maxRequests int, duration time.Duration) fiber.Handler { - return func(c *fiber.Ctx) error { - claims, err := GetUserFromContext(c) - if err != nil { - return err - } - - key := fmt.Sprintf("rate_limit:%s:%s", claims.UserID, c.Route().Path) - - count, err := utils.IncrementCounter(key, duration) - if err != nil { - - return c.Next() - } - - if count > int64(maxRequests) { - - ttl, _ := utils.GetTTL(key) - return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ - "error": "Rate limit exceeded", - "message": "Terlalu banyak permintaan, silakan coba lagi nanti", - "retry_after": int64(ttl.Seconds()), - "limit": maxRequests, - "remaining": 0, - }) - } - - c.Set("X-RateLimit-Limit", fmt.Sprintf("%d", maxRequests)) - c.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", maxRequests-int(count))) - c.Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(duration).Unix())) - - return c.Next() - } -} - -func SessionValidation() fiber.Handler { - return func(c *fiber.Ctx) error { - claims, err := GetUserFromContext(c) - if err != nil { - return err - } - - if claims.SessionID == "" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Invalid session", - "message": "Session tidak valid", - }) - } - - sessionKey := fmt.Sprintf("session:%s", claims.SessionID) - var sessionData map[string]interface{} - err = utils.GetCache(sessionKey, &sessionData) - if err != nil { - if err.Error() == "ErrCacheMiss" { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Session not found", - "message": "Session tidak ditemukan, silakan login kembali", - }) - } - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Session error", - "message": "Terjadi kesalahan saat validasi session", - }) - } - - if sessionData["user_id"] != claims.UserID { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Session mismatch", - "message": "Session tidak sesuai dengan user", - }) - } - - if expiryInterface, exists := sessionData["expires_at"]; exists { - if expiry, ok := expiryInterface.(float64); ok { - if time.Now().Unix() > int64(expiry) { - - utils.DeleteCache(sessionKey) - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Session expired", - "message": "Session telah berakhir, silakan login kembali", - }) - } - } - } - - return c.Next() - } -} - -func RequireApprovedRegistration() fiber.Handler { - return func(c *fiber.Ctx) error { - claims, err := GetUserFromContext(c) - if err != nil { - return err - } - - if claims.RegistrationStatus == utils.RegStatusRejected { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Registration rejected", - "message": "Registrasi Anda ditolak, silakan hubungi admin", - }) - } - - if claims.RegistrationStatus == utils.RegStatusPending { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Registration pending", - "message": "Registrasi Anda masih menunggu persetujuan admin", - }) - } - - if claims.RegistrationStatus != utils.RegStatusComplete { - progress := utils.GetUserRegistrationProgress(claims.UserID) - nextStep := utils.GetNextRegistrationStep(claims.Role, progress) - - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Registration incomplete", - "message": "Silakan lengkapi registrasi terlebih dahulu", - "registration_status": claims.RegistrationStatus, - "next_step": nextStep, - }) - } - - return c.Next() - } -} - -func ConditionalAuth(condition func(*utils.JWTClaims) bool, errorMessage string) fiber.Handler { - return func(c *fiber.Ctx) error { - claims, err := GetUserFromContext(c) - if err != nil { - return err - } - - if !condition(claims) { - return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ - "error": "Condition not met", - "message": errorMessage, - }) - } - - return c.Next() - } -} - -func RequireSpecificRole(role string) fiber.Handler { - return ConditionalAuth( - func(claims *utils.JWTClaims) bool { - return claims.Role == role - }, - fmt.Sprintf("Akses ini hanya untuk role %s", role), - ) -} - -func RequireCompleteRegistrationAndSpecificRole(role string) fiber.Handler { - return ConditionalAuth( - func(claims *utils.JWTClaims) bool { - return claims.Role == role && utils.IsRegistrationComplete(claims.RegistrationStatus) - }, - fmt.Sprintf("Akses ini hanya untuk role %s dengan registrasi lengkap", role), - ) -} - -func DeviceValidation() fiber.Handler { - return func(c *fiber.Ctx) error { - claims, err := GetUserFromContext(c) - if err != nil { - return err - } - - deviceID := c.Get("X-Device-ID") - if deviceID == "" { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "error": "Device ID required", - "message": "Device ID diperlukan", - }) - } - - if claims.DeviceID != deviceID { - return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ - "error": "Device mismatch", - "message": "Token tidak valid untuk device ini", - }) - } - - return c.Next() - } -} - */ \ No newline at end of file diff --git a/middleware/middleware.go b/middleware/middleware.go index c369de4..9f736c0 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -503,7 +503,7 @@ func DeviceValidation() fiber.Handler { return err } - deviceID := c.Get("X-Device-ID") + deviceID := claims.DeviceID if deviceID == "" { return c.Status(fiber.StatusBadRequest).JSON(&AuthError{ Code: "MISSING_DEVICE_ID", diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index d653c9c..ea43573 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -3,11 +3,13 @@ package router import ( "os" + "rijig/internal/about" "rijig/internal/article" "rijig/internal/authentication" "rijig/internal/company" "rijig/internal/identitycart" "rijig/internal/role" + "rijig/internal/trash" "rijig/internal/userpin" "rijig/internal/userprofile" "rijig/internal/whatsapp" @@ -22,8 +24,8 @@ import ( func SetupRoutes(app *fiber.App) { apa := app.Group(os.Getenv("BASE_URL")) apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads") - a := app.Group(os.Getenv("BASE_URL")) - whatsapp.WhatsAppRouter(a) + // a := app.Group(os.Getenv("BASE_URL")) + // whatsapp.WhatsAppRouter(a) api := app.Group(os.Getenv("BASE_URL")) api.Use(middleware.APIKeyMiddleware) @@ -37,6 +39,9 @@ func SetupRoutes(app *fiber.App) { article.ArticleRouter(api) userprofile.UserProfileRouter(api) wilayahindo.WilayahRouter(api) + trash.TrashRouter(api) + about.AboutRouter(api) + whatsapp.WhatsAppRouter(api) // || auth router || // // presentation.AuthRouter(api) From 8f32944c7bf600dbda553f00a5e41cef41d6afee Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Fri, 27 Jun 2025 19:29:30 +0700 Subject: [PATCH 47/48] update: update readme --- README.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/README.md b/README.md index 8b05eb2..613aec0 100644 --- a/README.md +++ b/README.md @@ -249,26 +249,6 @@ Modul pelaporan dengan kemampuan generate report otomatis, export dalam berbagai - **Middleware Pattern**: Cross-cutting concerns seperti authentication, logging, validation - **Event-Driven Architecture**: Pub/sub pattern untuk real-time notifications -## 🌟 Competitive Advantages - -### **Technical Excellence** -- **High Performance**: Sub-100ms response time untuk critical operations -- **Scalability**: Ready untuk handle growth hingga millions of users -- **Security First**: Multi-layer security dengan encryption dan secure authentication -- **Real-time Capabilities**: Instant updates dan notifications untuk better user experience - -### **Business Value** -- **Cost Efficiency**: Significant reduction dalam operational cost melalui automation -- **Environmental Impact**: Measurable contribution untuk sustainability goals -- **Stakeholder Engagement**: User-friendly platform yang mendorong active participation -- **Data-Driven Decision**: Comprehensive analytics untuk strategic planning - -### **Innovation Features** -- **AI-Ready Architecture**: Prepared untuk integration dengan machine learning models -- **IoT Integration**: Ready untuk connect dengan smart waste bins dan sensors -- **Blockchain Compatibility**: Architecture yang support untuk blockchain integration -- **Multi-tenancy Support**: Scalable untuk multiple cities dan regions - ---
From 992b75e32b54fbd9d966b3e59cbf8ab586c8426d Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Sun, 29 Jun 2025 21:22:49 +0700 Subject: [PATCH 48/48] update: add security for administrator authentication --- cmd/main.go | 2 +- config/database.go | 2 +- go.mod | 5 + go.sum | 4 + internal/authentication/authentication_dto.go | 53 +++ .../authentication/authentication_handler.go | 141 +++++- .../authentication/authentication_route.go | 12 +- .../authentication/authentication_service.go | 436 +++++++++++++++--- middleware/api_key.go | 23 - middleware/middleware.go | 15 + model/user_model.go | 1 + utils/email_utils.go | 179 +++++++ utils/email_verification.go | 208 +++++++++ utils/reset_password.go | 202 ++++++++ utils/response.go | 107 ----- utils/role_permission.go | 17 - utils/token_management.go | 7 + 17 files changed, 1190 insertions(+), 224 deletions(-) delete mode 100644 middleware/api_key.go create mode 100644 utils/email_utils.go create mode 100644 utils/email_verification.go create mode 100644 utils/reset_password.go delete mode 100644 utils/response.go delete mode 100644 utils/role_permission.go diff --git a/cmd/main.go b/cmd/main.go index 7d80cb5..f3a4b92 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -51,4 +51,4 @@ func main() { router.SetupRoutes(app) config.StartServer(app) -} +} \ No newline at end of file diff --git a/config/database.go b/config/database.go index 92a4356..abcb5ae 100644 --- a/config/database.go +++ b/config/database.go @@ -31,4 +31,4 @@ func ConnectDatabase() { if err := RunMigrations(DB); err != nil { log.Fatalf("Error performing auto-migration: %v", err) } -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index 36bd859..552a803 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,11 @@ require ( gorm.io/gorm v1.25.12 ) +require ( + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // direct +) + // require ( // golang.org/x/term v0.30.0 // indirect // rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 58846b8..e6077af 100644 --- a/go.sum +++ b/go.sum @@ -109,7 +109,11 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/authentication/authentication_dto.go b/internal/authentication/authentication_dto.go index b29aee8..801ff71 100644 --- a/internal/authentication/authentication_dto.go +++ b/internal/authentication/authentication_dto.go @@ -256,6 +256,59 @@ type LoginAdminRequest struct { DeviceID string `json:"device_id"` } +type VerifyAdminOTPRequest struct { + Email string `json:"email" validate:"required,email"` + OTP string `json:"otp" validate:"required,len=6,numeric"` + DeviceID string `json:"device_id" validate:"required"` +} + +type ResendAdminOTPRequest struct { + Email string `json:"email" validate:"required,email"` +} + +type OTPAdminResponse struct { + Message string `json:"message"` + Email string `json:"email"` + ExpiresIn time.Duration `json:"expires_in_seconds"` + RemainingTime string `json:"remaining_time"` + CanResend bool `json:"can_resend"` + MaxAttempts int `json:"max_attempts"` +} + +type ForgotPasswordRequest struct { + Email string `json:"email" validate:"required,email"` +} + +type ResetPasswordRequest struct { + Email string `json:"email" validate:"required,email"` + Token string `json:"token" validate:"required"` + NewPassword string `json:"new_password" validate:"required,min=6"` +} + +type ResetPasswordResponse struct { + Message string `json:"message"` + Email string `json:"email"` + ExpiresIn time.Duration `json:"expires_in_seconds"` + RemainingTime string `json:"remaining_time"` +} + +type VerifyEmailRequest struct { + Email string `json:"email" validate:"required,email"` + Token string `json:"token" validate:"required"` +} + +type ResendVerificationRequest struct { + Email string `json:"email" validate:"required,email"` +} + +type EmailVerificationResponse struct { + Message string `json:"message"` + Email string `json:"email"` + ExpiresIn time.Duration `json:"expires_in_seconds"` + RemainingTime string `json:"remaining_time"` +} + + func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) { errors := make(map[string][]string) diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go index 60a6a86..613a4cc 100644 --- a/internal/authentication/authentication_handler.go +++ b/internal/authentication/authentication_handler.go @@ -106,31 +106,144 @@ func (h *AuthenticationHandler) Login(c *fiber.Ctx) error { } -func (h *AuthenticationHandler) Register(c *fiber.Ctx) error { - +func (h *AuthenticationHandler) RegisterAdmin(c *fiber.Ctx) error { var req RegisterAdminRequest if err := c.BodyParser(&req); err != nil { return utils.BadRequest(c, "Invalid request format") } - if errs, ok := req.ValidateRegisterAdminRequest(); !ok { - return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ - "meta": fiber.Map{ - "status": fiber.StatusBadRequest, - "message": "periksa lagi inputan", - }, - "errors": errs, - }) - } + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } - err := h.service.RegisterAdmin(c.Context(), &req) + response, err := h.service.RegisterAdmin(c.Context(), &req) if err != nil { - return utils.InternalServerError(c, err.Error()) + return utils.BadRequest(c, err.Error()) } - return utils.Success(c, "Registration successful, Please login") + return utils.SuccessWithData(c, "Admin registered successfully", response) } +// POST /auth/admin/verify-email - Verify email dari registration +func (h *AuthenticationHandler) VerifyEmail(c *fiber.Ctx) error { + var req VerifyEmailRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } + + err := h.service.VerifyEmail(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "Email berhasil diverifikasi. Sekarang Anda dapat login", nil) +} + +// POST /auth/admin/resend-verification - Resend verification email +func (h *AuthenticationHandler) ResendEmailVerification(c *fiber.Ctx) error { + var req ResendVerificationRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } + + response, err := h.service.ResendEmailVerification(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "Verification email resent", response) +} + + +func (h *AuthenticationHandler) VerifyAdminOTP(c *fiber.Ctx) error { + var req VerifyAdminOTPRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if errs, ok := req.Valida(); !ok { + // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ + // "meta": fiber.Map{ + // "status": fiber.StatusBadRequest, + // "message": "periksa lagi inputan", + // }, + // "errors": errs, + // }) + // } + + response, err := h.service.VerifyAdminOTP(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "OTP resent successfully", response) +} + +// POST /auth/admin/resend-otp - Resend OTP +func (h *AuthenticationHandler) ResendAdminOTP(c *fiber.Ctx) error { + var req ResendAdminOTPRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } + + response, err := h.service.ResendAdminOTP(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "OTP resent successfully", response) +} + +func (h *AuthenticationHandler) ForgotPassword(c *fiber.Ctx) error { + var req ForgotPasswordRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } + + response, err := h.service.ForgotPassword(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "Reset password email sent", response) +} + +// POST /auth/admin/reset-password - Step 2: Reset password dengan token +func (h *AuthenticationHandler) ResetPassword(c *fiber.Ctx) error { + var req ResetPasswordRequest + if err := c.BodyParser(&req); err != nil { + return utils.BadRequest(c, "Invalid request format") + } + + // if err := h.validator.Struct(&req); err != nil { + // return utils.BadRequest(c, "Validation failed: "+err.Error()) + // } + + err := h.service.ResetPassword(c.Context(), &req) + if err != nil { + return utils.BadRequest(c, err.Error()) + } + + return utils.SuccessWithData(c, "Password berhasil direset", nil) +} + + func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error { var req LoginorRegistRequest if err := c.BodyParser(&req); err != nil { diff --git a/internal/authentication/authentication_route.go b/internal/authentication/authentication_route.go index 149a019..bb5058f 100644 --- a/internal/authentication/authentication_route.go +++ b/internal/authentication/authentication_route.go @@ -32,7 +32,17 @@ func AuthenticationRouter(api fiber.Router) { authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus) authRoute.Post("/login/admin", authHandler.Login) - authRoute.Post("/register/admin", authHandler.Register) + authRoute.Post("/register/admin", authHandler.RegisterAdmin) + + authRoute.Post("/verify-email", authHandler.VerifyEmail) + authRoute.Post("/resend-verification", authHandler.ResendEmailVerification) + + authRoute.Post("/verify-otp-admin", authHandler.VerifyAdminOTP) + authRoute.Post("/resend-otp-admin", authHandler.ResendAdminOTP) + + authRoute.Post("/forgot-password", authHandler.ForgotPassword) + authRoute.Post("/reset-password", authHandler.ResetPassword) + authRoute.Post("/request-otp", authHandler.RequestOtpHandler) authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler) authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler) diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go index d984642..980c5c8 100644 --- a/internal/authentication/authentication_service.go +++ b/internal/authentication/authentication_service.go @@ -14,8 +14,17 @@ import ( type AuthenticationService interface { GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error) - LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) - RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error + LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error) + RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error) + + VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error) + ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error) + + ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error) + ResetPassword(ctx context.Context, req *ResetPasswordRequest) error + + VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error + ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) @@ -27,29 +36,34 @@ type AuthenticationService interface { } type authenticationService struct { - authRepo AuthenticationRepository - roleRepo role.RoleRepository + authRepo AuthenticationRepository + roleRepo role.RoleRepository + emailService *utils.EmailService } func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService { - return &authenticationService{authRepo, roleRepo} -} - -func normalizeRoleName(roleName string) string { - switch strings.ToLower(roleName) { - case "administrator", "admin": - return utils.RoleAdministrator - case "pengelola": - return utils.RolePengelola - case "pengepul": - return utils.RolePengepul - case "masyarakat": - return utils.RoleMasyarakat - default: - return strings.ToLower(roleName) + return &authenticationService{ + authRepo: authRepo, + roleRepo: roleRepo, + emailService: utils.NewEmailService(), } } +// func normalizeRoleName(roleName string) string { +// switch strings.ToLower(roleName) { +// case "administrator", "admin": +// return utils.RoleAdministrator +// case "pengelola": +// return utils.RolePengelola +// case "pengepul": +// return utils.RolePengepul +// case "masyarakat": +// return utils.RoleMasyarakat +// default: +// return strings.ToLower(roleName) +// } +// } + type GetRegistrationStatusResponse struct { UserID string `json:"userId"` RegistrationStatus string `json:"registrationStatus"` @@ -122,31 +136,92 @@ func (s *authenticationService) GetRegistrationStatus(ctx context.Context, userI return nil, fmt.Errorf("unsupported registration status: %s", user.RegistrationStatus) } -func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) { +func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error) { + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) if err != nil { - return nil, fmt.Errorf("user not found: %w", err) + return nil, fmt.Errorf("invalid credentials") } if user.Role == nil || user.Role.RoleName != "administrator" { - return nil, fmt.Errorf("user not found: %w", err) + return nil, fmt.Errorf("invalid credentials") } if user.RegistrationStatus != "completed" { - return nil, fmt.Errorf("user not found: %w", err) + return nil, fmt.Errorf("account not activated") } if !utils.CompareHashAndPlainText(user.Password, req.Password) { - return nil, fmt.Errorf("user not found: %w", err) + return nil, fmt.Errorf("invalid credentials") } - token, err := utils.GenerateTokenPair(user.ID, user.Role.RoleName, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress)) + if utils.IsOTPValid(req.Email) { + remaining, _ := utils.GetOTPRemainingTime(req.Email) + return &OTPAdminResponse{ + Message: "OTP sudah dikirim sebelumnya", + Email: req.Email, + ExpiresIn: remaining, + RemainingTime: formatDuration(remaining), + CanResend: false, + MaxAttempts: utils.MAX_OTP_ATTEMPTS, + }, nil + } + + otp, err := utils.GenerateOTP() + if err != nil { + return nil, fmt.Errorf("failed to generate OTP") + } + + if err := utils.StoreOTP(req.Email, otp); err != nil { + return nil, fmt.Errorf("failed to store OTP") + } + + if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil { + log.Printf("Failed to send OTP email: %v", err) + return nil, fmt.Errorf("failed to send OTP email") + } + + return &OTPAdminResponse{ + Message: "Kode OTP berhasil dikirim ke email Anda", + Email: req.Email, + ExpiresIn: utils.OTP_EXPIRY, + RemainingTime: formatDuration(utils.OTP_EXPIRY), + CanResend: false, + MaxAttempts: utils.MAX_OTP_ATTEMPTS, + }, nil +} + +func (s *authenticationService) VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error) { + + if err := utils.ValidateOTP(req.Email, req.OTP); err != nil { + return nil, err + } + + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("user not found") + } + + if !user.EmailVerified { + user.EmailVerified = true + if err := s.authRepo.UpdateUser(ctx, user); err != nil { + log.Printf("Failed to update email verification status: %v", err) + } + } + + token, err := utils.GenerateTokenPair( + user.ID, + user.Role.RoleName, + req.DeviceID, + user.RegistrationStatus, + int(user.RegistrationProgress), + ) if err != nil { return nil, fmt.Errorf("failed to generate token: %w", err) } return &AuthResponse{ - Message: "login berhasil", + Message: "Login berhasil", AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, RegistrationStatus: user.RegistrationStatus, @@ -154,42 +229,283 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR }, nil } -func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error { +func (s *authenticationService) ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error) { - existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email) - if existingUser != nil { - return fmt.Errorf("email already in use") + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("email not found") } - hashedPassword, err := utils.HashingPlainText(req.Password) + if user.Role == nil || user.Role.RoleName != "administrator" { + return nil, fmt.Errorf("not authorized") + } + + if utils.IsOTPValid(req.Email) { + remaining, _ := utils.GetOTPRemainingTime(req.Email) + return nil, fmt.Errorf("OTP masih berlaku. Tunggu %s untuk kirim ulang", formatDuration(remaining)) + } + + otp, err := utils.GenerateOTP() + if err != nil { + return nil, fmt.Errorf("failed to generate OTP") + } + + if err := utils.StoreOTP(req.Email, otp); err != nil { + return nil, fmt.Errorf("failed to store OTP") + } + + if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil { + log.Printf("Failed to send OTP email: %v", err) + return nil, fmt.Errorf("failed to send OTP email") + } + + return &OTPAdminResponse{ + Message: "Kode OTP baru berhasil dikirim", + Email: req.Email, + ExpiresIn: utils.OTP_EXPIRY, + RemainingTime: formatDuration(utils.OTP_EXPIRY), + CanResend: false, + MaxAttempts: utils.MAX_OTP_ATTEMPTS, + }, nil +} + +func (s *authenticationService) VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error { + + verificationData, err := utils.ValidateEmailVerificationToken(req.Email, req.Token) + if err != nil { + return err + } + + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return fmt.Errorf("user not found") + } + + if user.ID != verificationData.UserID { + return fmt.Errorf("invalid verification token") + } + + if user.EmailVerified { + return fmt.Errorf("email sudah terverifikasi sebelumnya") + } + + user.EmailVerified = true + user.RegistrationStatus = utils.RegStatusComplete + // user.RegistrationProgress = 3 + + if err := s.authRepo.UpdateUser(ctx, user); err != nil { + return fmt.Errorf("failed to update user verification status: %w", err) + } + + if err := utils.MarkEmailVerificationTokenAsUsed(req.Email); err != nil { + log.Printf("Failed to mark verification token as used: %v", err) + } + + return nil +} + +func (s *authenticationService) ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error) { + + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return nil, fmt.Errorf("email not found") + } + + if user.Role == nil || user.Role.RoleName != "administrator" { + return nil, fmt.Errorf("not authorized") + } + + if user.EmailVerified { + return nil, fmt.Errorf("email sudah terverifikasi") + } + + if utils.IsEmailVerificationTokenValid(req.Email) { + remaining, _ := utils.GetEmailVerificationTokenRemainingTime(req.Email) + return &EmailVerificationResponse{ + Message: "Email verifikasi sudah dikirim sebelumnya", + Email: req.Email, + ExpiresIn: remaining, + RemainingTime: formatDuration(remaining), + }, nil + } + + token, err := utils.GenerateEmailVerificationToken() + if err != nil { + return nil, fmt.Errorf("failed to generate verification token") + } + + if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil { + return nil, fmt.Errorf("failed to store verification token") + } + + if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil { + log.Printf("Failed to send verification email: %v", err) + return nil, fmt.Errorf("failed to send verification email") + } + + return &EmailVerificationResponse{ + Message: "Email verifikasi berhasil dikirim ulang", + Email: req.Email, + ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY, + RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY), + }, nil +} + +func (s *authenticationService) ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error) { + + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + + return &ResetPasswordResponse{ + Message: "Jika email terdaftar, link reset password akan dikirim", + Email: req.Email, + ExpiresIn: utils.RESET_TOKEN_EXPIRY, + RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY), + }, nil + } + + if user.Role == nil || user.Role.RoleName != "administrator" { + + return &ResetPasswordResponse{ + Message: "Jika email terdaftar, link reset password akan dikirim", + Email: req.Email, + ExpiresIn: utils.RESET_TOKEN_EXPIRY, + RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY), + }, nil + } + + if utils.IsResetTokenValid(req.Email) { + remaining, _ := utils.GetResetTokenRemainingTime(req.Email) + return &ResetPasswordResponse{ + Message: "Link reset password sudah dikirim sebelumnya", + Email: req.Email, + ExpiresIn: remaining, + RemainingTime: formatDuration(remaining), + }, nil + } + + token, err := utils.GenerateResetToken() + if err != nil { + return nil, fmt.Errorf("failed to generate reset token") + } + + if err := utils.StoreResetToken(req.Email, user.ID, token); err != nil { + return nil, fmt.Errorf("failed to store reset token") + } + + if err := s.emailService.SendResetPasswordEmail(req.Email, user.Name, token); err != nil { + log.Printf("Failed to send reset password email: %v", err) + return nil, fmt.Errorf("failed to send reset password email") + } + + return &ResetPasswordResponse{ + Message: "Link reset password berhasil dikirim ke email Anda", + Email: req.Email, + ExpiresIn: utils.RESET_TOKEN_EXPIRY, + RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY), + }, nil +} + +func (s *authenticationService) ResetPassword(ctx context.Context, req *ResetPasswordRequest) error { + + resetData, err := utils.ValidateResetToken(req.Email, req.Token) + if err != nil { + return err + } + + user, err := s.authRepo.FindUserByEmail(ctx, req.Email) + if err != nil { + return fmt.Errorf("user not found") + } + + if user.ID != resetData.UserID { + return fmt.Errorf("invalid reset token") + } + + hashedPassword, err := utils.HashingPlainText(req.NewPassword) if err != nil { return fmt.Errorf("failed to hash password: %w", err) } - role, err := s.roleRepo.FindRoleByName(ctx, "administrator") - if err != nil { - return fmt.Errorf("role name not found: %w", err) + user.Password = hashedPassword + if err := s.authRepo.UpdateUser(ctx, user); err != nil { + return fmt.Errorf("failed to update password: %w", err) } - user := &model.User{ - Name: req.Name, - Phone: req.Phone, - Email: req.Email, - Gender: req.Gender, - Dateofbirth: req.DateOfBirth, - Placeofbirth: req.PlaceOfBirth, - Password: hashedPassword, - RoleID: role.ID, - RegistrationStatus: "completed", + if err := utils.MarkResetTokenAsUsed(req.Email); err != nil { + log.Printf("Failed to mark reset token as used: %v", err) } - if err := s.authRepo.CreateUser(ctx, user); err != nil { - return fmt.Errorf("failed to create user: %w", err) + if err := utils.RevokeAllRefreshTokens(user.ID); err != nil { + log.Printf("Failed to revoke refresh tokens: %v", err) } return nil } +func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error) { + + existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email) + if existingUser != nil { + return nil, fmt.Errorf("email already in use") + } + + hashedPassword, err := utils.HashingPlainText(req.Password) + if err != nil { + return nil, fmt.Errorf("failed to hash password: %w", err) + } + + role, err := s.roleRepo.FindRoleByName(ctx, "administrator") + if err != nil { + return nil, fmt.Errorf("role name not found: %w", err) + } + + user := &model.User{ + Name: req.Name, + Phone: req.Phone, + Email: req.Email, + Gender: req.Gender, + Dateofbirth: req.DateOfBirth, + Placeofbirth: req.PlaceOfBirth, + Password: hashedPassword, + RoleID: role.ID, + RegistrationStatus: "pending_email_verification", + RegistrationProgress: 1, + EmailVerified: false, + } + + if err := s.authRepo.CreateUser(ctx, user); err != nil { + return nil, fmt.Errorf("failed to create user: %w", err) + } + + token, err := utils.GenerateEmailVerificationToken() + if err != nil { + return nil, fmt.Errorf("failed to generate verification token") + } + + if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil { + return nil, fmt.Errorf("failed to store verification token") + } + + if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil { + log.Printf("Failed to send verification email: %v", err) + return nil, fmt.Errorf("failed to send verification email") + } + + return &EmailVerificationResponse{ + Message: "Admin berhasil didaftarkan. Silakan cek email untuk verifikasi", + Email: req.Email, + ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY, + RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY), + }, nil +} + +func formatDuration(d time.Duration) string { + minutes := int(d.Minutes()) + seconds := int(d.Seconds()) % 60 + return fmt.Sprintf("%d:%02d", minutes, seconds) +} + func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) { normalizedRole := strings.ToLower(req.RoleName) @@ -418,7 +734,7 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO var message string if user.RegistrationStatus == utils.RegStatusComplete { message = "verif pin" - nextStep = "verif_pin" + nextStep = "verif_pin" } else { message = "otp berhasil diverifikasi" } @@ -471,19 +787,19 @@ func sendOTP(phone, otp string) error { return nil } -func convertUserToResponse(user *model.User) *UserResponse { - return &UserResponse{ - ID: user.ID, - Name: user.Name, - Phone: user.Phone, - Email: user.Email, - Role: user.Role.RoleName, - RegistrationStatus: user.RegistrationStatus, - RegistrationProgress: user.RegistrationProgress, - PhoneVerified: user.PhoneVerified, - Avatar: user.Avatar, - } -} +// func convertUserToResponse(user *model.User) *UserResponse { +// return &UserResponse{ +// ID: user.ID, +// Name: user.Name, +// Phone: user.Phone, +// Email: user.Email, +// Role: user.Role.RoleName, +// RegistrationStatus: user.RegistrationStatus, +// RegistrationProgress: user.RegistrationProgress, +// PhoneVerified: user.PhoneVerified, +// Avatar: user.Avatar, +// } +// } func IsRegistrationComplete(role string, progress int) bool { switch role { diff --git a/middleware/api_key.go b/middleware/api_key.go deleted file mode 100644 index 1077eb0..0000000 --- a/middleware/api_key.go +++ /dev/null @@ -1,23 +0,0 @@ -package middleware - -import ( - "os" - - "rijig/utils" - - "github.com/gofiber/fiber/v2" -) - -func APIKeyMiddleware(c *fiber.Ctx) error { - apiKey := c.Get("x-api-key") - if apiKey == "" { - return utils.Unauthorized(c, "Unauthorized: API key is required") - } - - validAPIKey := os.Getenv("API_KEY") - if apiKey != validAPIKey { - return utils.Unauthorized(c, "Unauthorized: Invalid API key") - } - - return c.Next() -} diff --git a/middleware/middleware.go b/middleware/middleware.go index 9f736c0..6da7a10 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -3,6 +3,7 @@ package middleware import ( "crypto/subtle" "fmt" + "os" "rijig/utils" "time" @@ -122,6 +123,20 @@ func getStatusCodeForError(errorCode string) int { } } +func APIKeyMiddleware(c *fiber.Ctx) error { + apiKey := c.Get("x-api-key") + if apiKey == "" { + return utils.Unauthorized(c, "Unauthorized: API key is required") + } + + validAPIKey := os.Getenv("API_KEY") + if apiKey != validAPIKey { + return utils.Unauthorized(c, "Unauthorized: Invalid API key") + } + + return c.Next() +} + func AuthMiddleware(config ...AuthConfig) fiber.Handler { cfg := AuthConfig{} if len(config) > 0 { diff --git a/model/user_model.go b/model/user_model.go index faefec9..323c6ac 100644 --- a/model/user_model.go +++ b/model/user_model.go @@ -11,6 +11,7 @@ type User struct { Placeofbirth string `gorm:"not null" json:"placeofbirth"` Phone string `gorm:"not null;index" json:"phone"` Email string `json:"email,omitempty"` + EmailVerified bool `gorm:"default:false" json:"emailVerified"` PhoneVerified bool `gorm:"default:false" json:"phoneVerified"` Password string `json:"password,omitempty"` RoleID string `gorm:"not null" json:"roleId"` diff --git a/utils/email_utils.go b/utils/email_utils.go new file mode 100644 index 0000000..53f0926 --- /dev/null +++ b/utils/email_utils.go @@ -0,0 +1,179 @@ +package utils + +import ( + "fmt" + "os" + "strconv" + "time" + + "gopkg.in/gomail.v2" +) + +type EmailService struct { + host string + port int + username string + password string + from string + fromName string +} + +type OTPData struct { + Code string `json:"code"` + Email string `json:"email"` + ExpiresAt int64 `json:"expires_at"` + Attempts int `json:"attempts"` + CreatedAt int64 `json:"created_at"` +} + +const ( + OTP_LENGTH = 6 + OTP_EXPIRY = 5 * time.Minute + MAX_OTP_ATTEMPTS = 3 +) + +func NewEmailService() *EmailService { + port, _ := strconv.Atoi("587") + + return &EmailService{ + host: "smtp.gmail.com", + port: port, + username: os.Getenv("SMTP_FROM_EMAIL"), + password: os.Getenv("GMAIL_APP_PASSWORD"), + from: os.Getenv("SMTP_FROM_EMAIL"), + fromName: os.Getenv("SMTP_FROM_NAME"), + } +} + +func StoreOTP(email, otp string) error { + key := fmt.Sprintf("otp:admin:%s", email) + + data := OTPData{ + Code: otp, + Email: email, + ExpiresAt: time.Now().Add(OTP_EXPIRY).Unix(), + Attempts: 0, + CreatedAt: time.Now().Unix(), + } + + return SetCache(key, data, OTP_EXPIRY) +} + +func ValidateOTP(email, inputOTP string) error { + key := fmt.Sprintf("otp:admin:%s", email) + + var data OTPData + err := GetCache(key, &data) + if err != nil { + return fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa") + } + + if time.Now().Unix() > data.ExpiresAt { + DeleteCache(key) + return fmt.Errorf("OTP sudah kadaluarsa") + } + + if data.Attempts >= MAX_OTP_ATTEMPTS { + DeleteCache(key) + return fmt.Errorf("OTP diblokir karena terlalu banyak percobaan salah") + } + + if data.Code != inputOTP { + + data.Attempts++ + SetCache(key, data, time.Until(time.Unix(data.ExpiresAt, 0))) + return fmt.Errorf("OTP tidak valid. Sisa percobaan: %d", MAX_OTP_ATTEMPTS-data.Attempts) + } + + DeleteCache(key) + return nil +} + +func (e *EmailService) SendOTPEmail(email, name, otp string) error { + m := gomail.NewMessage() + m.SetHeader("From", m.FormatAddress(e.from, e.fromName)) + m.SetHeader("To", email) + m.SetHeader("Subject", "Kode Verifikasi Login Administrator - Rijig") + + body := fmt.Sprintf(` + + + + + + + +
+
+

🔐 Kode Verifikasi Login

+
+
+

Halo %s,

+

Anda telah meminta untuk login sebagai Administrator. Gunakan kode verifikasi berikut:

+ +
%s
+ +

Penting:

+
    +
  • Kode ini berlaku selama 5 menit
  • +
  • Jangan berikan kode ini kepada siapapun
  • +
  • Maksimal 3 kali percobaan
  • +
+ +

⚠️ Jika Anda tidak melakukan permintaan login ini, abaikan email ini.

+
+ +
+ + + `, name, otp) + + m.SetBody("text/html", body) + + d := gomail.NewDialer(e.host, e.port, e.username, e.password) + + if err := d.DialAndSend(m); err != nil { + return fmt.Errorf("failed to send email: %v", err) + } + + return nil +} + +func IsOTPValid(email string) bool { + key := fmt.Sprintf("otp:admin:%s", email) + + var data OTPData + err := GetCache(key, &data) + if err != nil { + return false + } + + return time.Now().Unix() <= data.ExpiresAt && data.Attempts < MAX_OTP_ATTEMPTS +} + +func GetOTPRemainingTime(email string) (time.Duration, error) { + key := fmt.Sprintf("otp:admin:%s", email) + + var data OTPData + err := GetCache(key, &data) + if err != nil { + return 0, err + } + + remaining := time.Until(time.Unix(data.ExpiresAt, 0)) + if remaining < 0 { + return 0, fmt.Errorf("OTP expired") + } + + return remaining, nil +} diff --git a/utils/email_verification.go b/utils/email_verification.go new file mode 100644 index 0000000..7f6dc02 --- /dev/null +++ b/utils/email_verification.go @@ -0,0 +1,208 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "time" + + "gopkg.in/gomail.v2" +) + +type EmailVerificationData struct { + Token string `json:"token"` + Email string `json:"email"` + UserID string `json:"user_id"` + ExpiresAt int64 `json:"expires_at"` + Used bool `json:"used"` + CreatedAt int64 `json:"created_at"` +} + +const ( + EMAIL_VERIFICATION_TOKEN_EXPIRY = 24 * time.Hour + EMAIL_VERIFICATION_TOKEN_LENGTH = 32 +) + +func GenerateEmailVerificationToken() (string, error) { + bytes := make([]byte, EMAIL_VERIFICATION_TOKEN_LENGTH) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate email verification token: %v", err) + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +func StoreEmailVerificationToken(email, userID, token string) error { + key := fmt.Sprintf("email_verification:%s", email) + + DeleteCache(key) + + data := EmailVerificationData{ + Token: token, + Email: email, + UserID: userID, + ExpiresAt: time.Now().Add(EMAIL_VERIFICATION_TOKEN_EXPIRY).Unix(), + Used: false, + CreatedAt: time.Now().Unix(), + } + + return SetCache(key, data, EMAIL_VERIFICATION_TOKEN_EXPIRY) +} + +func ValidateEmailVerificationToken(email, inputToken string) (*EmailVerificationData, error) { + key := fmt.Sprintf("email_verification:%s", email) + + var data EmailVerificationData + err := GetCache(key, &data) + if err != nil { + return nil, fmt.Errorf("token verifikasi tidak ditemukan atau sudah kadaluarsa") + } + + if time.Now().Unix() > data.ExpiresAt { + DeleteCache(key) + return nil, fmt.Errorf("token verifikasi sudah kadaluarsa") + } + + if data.Used { + return nil, fmt.Errorf("token verifikasi sudah digunakan") + } + + // Validate token + if !ConstantTimeCompare(data.Token, inputToken) { + return nil, fmt.Errorf("token verifikasi tidak valid") + } + + return &data, nil +} + +// Mark email verification token as used +func MarkEmailVerificationTokenAsUsed(email string) error { + key := fmt.Sprintf("email_verification:%s", email) + + var data EmailVerificationData + err := GetCache(key, &data) + if err != nil { + return err + } + + data.Used = true + remaining := time.Until(time.Unix(data.ExpiresAt, 0)) + + return SetCache(key, data, remaining) +} + +// Check if email verification token exists and still valid +func IsEmailVerificationTokenValid(email string) bool { + key := fmt.Sprintf("email_verification:%s", email) + + var data EmailVerificationData + err := GetCache(key, &data) + if err != nil { + return false + } + + return time.Now().Unix() <= data.ExpiresAt && !data.Used +} + +// Get remaining email verification token time +func GetEmailVerificationTokenRemainingTime(email string) (time.Duration, error) { + key := fmt.Sprintf("email_verification:%s", email) + + var data EmailVerificationData + err := GetCache(key, &data) + if err != nil { + return 0, err + } + + remaining := time.Until(time.Unix(data.ExpiresAt, 0)) + if remaining < 0 { + return 0, fmt.Errorf("token expired") + } + + return remaining, nil +} + +// Send email verification email +func (e *EmailService) SendEmailVerificationEmail(email, name, token string) error { + // Create verification URL - in real app this would be frontend URL + verificationURL := fmt.Sprintf("http://localhost:3000/verify-email?token=%s&email=%s", token, email) + + m := gomail.NewMessage() + m.SetHeader("From", m.FormatAddress(e.from, e.fromName)) + m.SetHeader("To", email) + m.SetHeader("Subject", "Verifikasi Email Administrator - Rijig") + + // Email template + body := fmt.Sprintf(` + + + + + + + +
+
+

✅ Verifikasi Email

+
+
+
🎉
+ +

Selamat %s!

+

Akun Administrator Anda telah berhasil dibuat. Untuk mengaktifkan akun dan mulai menggunakan sistem Rijig, silakan verifikasi email Anda dengan mengklik tombol di bawah ini:

+ + + +

Atau copy paste link berikut ke browser Anda:

+
%s
+ +
+

Informasi Penting:

+
    +
  • Link verifikasi berlaku selama 24 jam
  • +
  • Setelah verifikasi, Anda dapat login ke sistem
  • +
  • Link hanya dapat digunakan sekali
  • +
  • Jangan bagikan link ini kepada siapapun
  • +
+
+ +

Langkah selanjutnya setelah verifikasi:

+
    +
  1. Login menggunakan email dan password
  2. +
  3. Masukkan kode OTP yang dikirim ke email
  4. +
  5. Mulai menggunakan sistem Rijig
  6. +
+ +

Jika Anda tidak membuat akun ini, abaikan email ini.

+
+ +
+ + + `, name, verificationURL, verificationURL) + + m.SetBody("text/html", body) + + d := gomail.NewDialer(e.host, e.port, e.username, e.password) + + if err := d.DialAndSend(m); err != nil { + return fmt.Errorf("failed to send email verification email: %v", err) + } + + return nil +} diff --git a/utils/reset_password.go b/utils/reset_password.go new file mode 100644 index 0000000..431987e --- /dev/null +++ b/utils/reset_password.go @@ -0,0 +1,202 @@ +package utils + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "time" + + "gopkg.in/gomail.v2" +) + +type ResetPasswordData struct { + Token string `json:"token"` + Email string `json:"email"` + UserID string `json:"user_id"` + ExpiresAt int64 `json:"expires_at"` + Used bool `json:"used"` + CreatedAt int64 `json:"created_at"` +} + +const ( + RESET_TOKEN_EXPIRY = 30 * time.Minute + RESET_TOKEN_LENGTH = 32 +) + +// Generate secure reset token +func GenerateResetToken() (string, error) { + bytes := make([]byte, RESET_TOKEN_LENGTH) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate reset token: %v", err) + } + return base64.URLEncoding.EncodeToString(bytes), nil +} + +// Store reset password token di Redis +func StoreResetToken(email, userID, token string) error { + key := fmt.Sprintf("reset_password:%s", email) + + // Delete any existing reset token for this email + DeleteCache(key) + + data := ResetPasswordData{ + Token: token, + Email: email, + UserID: userID, + ExpiresAt: time.Now().Add(RESET_TOKEN_EXPIRY).Unix(), + Used: false, + CreatedAt: time.Now().Unix(), + } + + return SetCache(key, data, RESET_TOKEN_EXPIRY) +} + +// Validate reset password token +func ValidateResetToken(email, inputToken string) (*ResetPasswordData, error) { + key := fmt.Sprintf("reset_password:%s", email) + + var data ResetPasswordData + err := GetCache(key, &data) + if err != nil { + return nil, fmt.Errorf("token reset tidak ditemukan atau sudah kadaluarsa") + } + + // Check if token is expired + if time.Now().Unix() > data.ExpiresAt { + DeleteCache(key) + return nil, fmt.Errorf("token reset sudah kadaluarsa") + } + + // Check if token is already used + if data.Used { + return nil, fmt.Errorf("token reset sudah digunakan") + } + + // Validate token + if !ConstantTimeCompare(data.Token, inputToken) { + return nil, fmt.Errorf("token reset tidak valid") + } + + return &data, nil +} + +// Mark reset token as used +func MarkResetTokenAsUsed(email string) error { + key := fmt.Sprintf("reset_password:%s", email) + + var data ResetPasswordData + err := GetCache(key, &data) + if err != nil { + return err + } + + data.Used = true + remaining := time.Until(time.Unix(data.ExpiresAt, 0)) + + return SetCache(key, data, remaining) +} + +// Check if reset token exists and still valid +func IsResetTokenValid(email string) bool { + key := fmt.Sprintf("reset_password:%s", email) + + var data ResetPasswordData + err := GetCache(key, &data) + if err != nil { + return false + } + + return time.Now().Unix() <= data.ExpiresAt && !data.Used +} + +// Get remaining reset token time +func GetResetTokenRemainingTime(email string) (time.Duration, error) { + key := fmt.Sprintf("reset_password:%s", email) + + var data ResetPasswordData + err := GetCache(key, &data) + if err != nil { + return 0, err + } + + remaining := time.Until(time.Unix(data.ExpiresAt, 0)) + if remaining < 0 { + return 0, fmt.Errorf("token expired") + } + + return remaining, nil +} + +// Send reset password email +func (e *EmailService) SendResetPasswordEmail(email, name, token string) error { + // Create reset URL - in real app this would be frontend URL + resetURL := fmt.Sprintf("http://localhost:3000/reset-password?token=%s&email=%s", token, email) + + m := gomail.NewMessage() + m.SetHeader("From", m.FormatAddress(e.from, e.fromName)) + m.SetHeader("To", email) + m.SetHeader("Subject", "Reset Password Administrator - Rijig") + + // Email template + body := fmt.Sprintf(` + + + + + + + +
+
+

🔐 Reset Password

+
+
+

Halo %s,

+

Kami menerima permintaan untuk reset password akun Administrator Anda.

+ +

Klik tombol di bawah ini untuk reset password:

+ + +

Atau copy paste link berikut ke browser Anda:

+
%s
+ +

Penting:

+
    +
  • Link ini berlaku selama 30 menit
  • +
  • Link hanya dapat digunakan sekali
  • +
  • Jangan bagikan link ini kepada siapapun
  • +
+ +

⚠️ Jika Anda tidak melakukan permintaan reset password, abaikan email ini dan password Anda tidak akan berubah.

+
+ +
+ + + `, name, resetURL, resetURL) + + m.SetBody("text/html", body) + + d := gomail.NewDialer(e.host, e.port, e.username, e.password) + + if err := d.DialAndSend(m); err != nil { + return fmt.Errorf("failed to send reset password email: %v", err) + } + + return nil +} diff --git a/utils/response.go b/utils/response.go deleted file mode 100644 index a36fb64..0000000 --- a/utils/response.go +++ /dev/null @@ -1,107 +0,0 @@ -package utils - -// import ( -// "github.com/gofiber/fiber/v2" -// ) - -// type MetaData struct { -// Status int `json:"status"` -// Page int `json:"page,omitempty"` -// Limit int `json:"limit,omitempty"` -// Total int `json:"total,omitempty"` -// Message string `json:"message"` -// } - -// type APIResponse struct { -// Meta MetaData `json:"meta"` -// Data interface{} `json:"data,omitempty"` -// } - -// func PaginatedResponse(c *fiber.Ctx, data interface{}, page, limit, total int, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusOK, -// Page: page, -// Limit: limit, -// Total: total, -// Message: message, -// }, -// Data: data, -// } -// return c.Status(fiber.StatusOK).JSON(response) -// } - -// func NonPaginatedResponse(c *fiber.Ctx, data interface{}, total int, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusOK, -// Total: total, -// Message: message, -// }, -// Data: data, -// } -// return c.Status(fiber.StatusOK).JSON(response) -// } - -// func ErrorResponse(c *fiber.Ctx, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusNotFound, -// Message: message, -// }, -// } -// return c.Status(fiber.StatusNotFound).JSON(response) -// } - -// func ValidationErrorResponse(c *fiber.Ctx, errors map[string][]string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusBadRequest, -// Message: "invalid user request", -// }, -// Data: errors, -// } -// return c.Status(fiber.StatusBadRequest).JSON(response) -// } - -// func InternalServerErrorResponse(c *fiber.Ctx, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusInternalServerError, -// Message: message, -// }, -// } -// return c.Status(fiber.StatusInternalServerError).JSON(response) -// } - -// func GenericResponse(c *fiber.Ctx, status int, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: status, -// Message: message, -// }, -// } -// return c.Status(status).JSON(response) -// } - -// func SuccessResponse(c *fiber.Ctx, data interface{}, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusOK, -// Message: message, -// }, -// Data: data, -// } -// return c.Status(fiber.StatusOK).JSON(response) -// } - -// func CreateResponse(c *fiber.Ctx, data interface{}, message string) error { -// response := APIResponse{ -// Meta: MetaData{ -// Status: fiber.StatusCreated, -// Message: message, -// }, -// Data: data, -// } -// return c.Status(fiber.StatusOK).JSON(response) -// } diff --git a/utils/role_permission.go b/utils/role_permission.go deleted file mode 100644 index a805ea9..0000000 --- a/utils/role_permission.go +++ /dev/null @@ -1,17 +0,0 @@ -package utils - -// RoleID based -/* const ( - RoleAdministrator = "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7" - RolePengelola = "0bf86966-7042-410a-a88c-d01f70832348" - RolePengepul = "d7245535-0e9e-4d35-ab39-baece5c10b3c" - RoleMasyarakat = "60e5684e4-b214-4bd0-972f-3be80c4649a0" -) */ - -// RoleName based -const ( - RoleAdministrator = "administrator" - RolePengelola = "pengelola" - RolePengepul = "pengepul" - RoleMasyarakat = "masyarakat" -) diff --git a/utils/token_management.go b/utils/token_management.go index 95ca2c8..fa1eb5c 100644 --- a/utils/token_management.go +++ b/utils/token_management.go @@ -15,6 +15,13 @@ import ( type TokenType string +const ( + RoleAdministrator = "administrator" + RolePengelola = "pengelola" + RolePengepul = "pengepul" + RoleMasyarakat = "masyarakat" +) + const ( TokenTypePartial TokenType = "partial" TokenTypeFull TokenType = "full"