diff --git a/config/database.go b/config/database.go index e0fc994..375ac1d 100644 --- a/config/database.go +++ b/config/database.go @@ -42,6 +42,7 @@ func ConnectDatabase() { &model.User{}, &model.Role{}, &model.UserPin{}, + &model.Address{}, // ==main feature== ) if err != nil { diff --git a/dto/address_dto.go b/dto/address_dto.go new file mode 100644 index 0000000..eef67f0 --- /dev/null +++ b/dto/address_dto.go @@ -0,0 +1,61 @@ +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"` + Geography string `json:"geography"` + 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"` + Geography string `json:"geography"` +} + +func (r *CreateAddressDTO) Validate() (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["village_id"], "PostalCode ID is required") + } else if len(r.PostalCode) < 5 { + errors["postalCode"] = append(errors["postalCode"], "kode pos belum sesuai") + } + 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 len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/address_handler.go b/internal/handler/address_handler.go new file mode 100644 index 0000000..7dad668 --- /dev/null +++ b/internal/handler/address_handler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +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.Validate() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + addressResponse, err := h.AddressService.CreateAddress(c.Locals("userID").(string), requestAddressDTO) + if err != nil { + return utils.GenericErrorResponse(c, fiber.StatusBadRequest, err.Error()) + } + + return utils.CreateResponse(c, addressResponse, "user address created successfully") +} diff --git a/internal/handler/auth_handler.go b/internal/handler/auth_handler.go index d6622f2..082fc29 100644 --- a/internal/handler/auth_handler.go +++ b/internal/handler/auth_handler.go @@ -33,7 +33,7 @@ func (h *UserHandler) Login(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, err.Error()) } - return utils.LogResponse(c, user, "Login successful") + return utils.SuccessResponse(c, user, "Login successful") } func (h *UserHandler) Register(c *fiber.Ctx) error { @@ -67,7 +67,7 @@ func (h *UserHandler) Register(c *fiber.Ctx) error { UpdatedAt: updatedAt, } - return utils.LogResponse(c, userResponse, "Registration successful") + return utils.CreateResponse(c, userResponse, "Registration successful") } func (h *UserHandler) Logout(c *fiber.Ctx) error { diff --git a/internal/handler/role_handler.go b/internal/handler/role_handler.go index a4cf541..d9f84fa 100644 --- a/internal/handler/role_handler.go +++ b/internal/handler/role_handler.go @@ -26,7 +26,7 @@ func (h *RoleHandler) GetRoles(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.LogResponse(c, roles, "Roles fetched successfully") + return utils.SuccessResponse(c, roles, "Roles fetched successfully") } func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { @@ -42,5 +42,5 @@ func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusNotFound, "role id tidak ditemukan") } - return utils.LogResponse(c, role, "Role fetched successfully") + return utils.SuccessResponse(c, role, "Role fetched successfully") } diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 51f1d83..7a29af3 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -27,7 +27,7 @@ func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusNotFound, err.Error()) } - return utils.LogResponse(c, userProfile, "User profile retrieved successfully") + return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully") } func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { @@ -51,7 +51,7 @@ func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) } - return utils.LogResponse(c, userResponse, "User profile updated successfully") + return utils.SuccessResponse(c, userResponse, "User profile updated successfully") } func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { @@ -75,7 +75,7 @@ func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusBadRequest, err.Error()) } - return utils.LogResponse(c, userResponse, "Password updated successfully") + return utils.SuccessResponse(c, userResponse, "Password updated successfully") } func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { @@ -94,5 +94,5 @@ func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.LogResponse(c, userResponse, "Avatar updated successfully") + return utils.SuccessResponse(c, userResponse, "Avatar updated successfully") } diff --git a/internal/handler/userpin_handler.go b/internal/handler/userpin_handler.go index aee4e16..3b61fcd 100644 --- a/internal/handler/userpin_handler.go +++ b/internal/handler/userpin_handler.go @@ -36,7 +36,7 @@ func (h *UserPinHandler) VerifyUserPin(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusUnauthorized, "pin yang anda masukkan salah") } - return utils.LogResponse(c, map[string]string{"data": "pin yang anda masukkan benar"}, "Pin verification successful") + return utils.SuccessResponse(c, map[string]string{"data": "pin yang anda masukkan benar"}, "Pin verification successful") } func (h *UserPinHandler) CheckPinStatus(c *fiber.Ctx) error { @@ -54,7 +54,7 @@ func (h *UserPinHandler) CheckPinStatus(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusBadRequest, "pin belum dibuat") } - return utils.LogResponse(c, map[string]string{"data": "pin sudah dibuat"}, "Pin status retrieved successfully") + return utils.SuccessResponse(c, map[string]string{"data": "pin sudah dibuat"}, "Pin status retrieved successfully") } func (h *UserPinHandler) CreateUserPin(c *fiber.Ctx) error { @@ -75,7 +75,7 @@ func (h *UserPinHandler) CreateUserPin(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusConflict, err.Error()) } - return utils.LogResponse(c, userPinResponse, "User pin created successfully") + return utils.CreateResponse(c, userPinResponse, "User pin created successfully") } func (h *UserPinHandler) UpdateUserPin(c *fiber.Ctx) error { @@ -96,5 +96,5 @@ func (h *UserPinHandler) UpdateUserPin(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusBadRequest, err.Error()) } - return utils.LogResponse(c, userPinResponse, "User pin updated successfully") + return utils.SuccessResponse(c, userPinResponse, "User pin updated successfully") } diff --git a/internal/handler/wilayah_indonesia_handler.go b/internal/handler/wilayah_indonesia_handler.go index 24944f0..51ad2b4 100644 --- a/internal/handler/wilayah_indonesia_handler.go +++ b/internal/handler/wilayah_indonesia_handler.go @@ -23,7 +23,7 @@ func (h *WilayahIndonesiaHandler) ImportWilayahData(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.GenericErrorResponse(c, fiber.StatusCreated, "Data imported successfully") + return utils.SuccessResponse(c, fiber.StatusCreated, "Data imported successfully") } func (h *WilayahIndonesiaHandler) GetProvinces(c *fiber.Ctx) error { @@ -195,5 +195,5 @@ func (h *WilayahIndonesiaHandler) GetVillageByID(c *fiber.Ctx) error { return utils.GenericErrorResponse(c, fiber.StatusInternalServerError, err.Error()) } - return utils.LogResponse(c, village, "Village fetched successfully") + return utils.SuccessResponse(c, village, "Village fetched successfully") } diff --git a/internal/repositories/address_repo.go b/internal/repositories/address_repo.go new file mode 100644 index 0000000..74a1e5c --- /dev/null +++ b/internal/repositories/address_repo.go @@ -0,0 +1,22 @@ +package repositories + +import ( + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type AddressRepository interface { + CreateAddress(address *model.Address) 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 +} diff --git a/internal/services/address_service.go b/internal/services/address_service.go new file mode 100644 index 0000000..1ece7be --- /dev/null +++ b/internal/services/address_service.go @@ -0,0 +1,88 @@ +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 AddressService interface { + CreateAddress(userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, 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, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) { + + errors, valid := request.Validate() + if !valid { + return nil, fmt.Errorf("validation failed: %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") + } + + district, _, err := s.WilayahRepo.FindDistrictByID(request.District, 0, 0) + if err != nil { + return nil, fmt.Errorf("invalid district_id") + } + + village, err := s.WilayahRepo.FindVillageByID(request.Village) + if err != nil { + return nil, fmt.Errorf("invalid village_id") + } + + newAddress := &model.Address{ + UserID: userID, + Province: province.Name, + Regency: regency.Name, + District: district.Name, + Village: village.Name, + PostalCode: request.PostalCode, + Detail: request.Detail, + Geography: request.Geography, + } + + err = s.AddressRepo.CreateAddress(newAddress) + if err != nil { + return nil, fmt.Errorf("failed to create user address: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(newAddress.CreatedAt) + + addressResponse := &dto.AddressResponseDTO{ + UserID: newAddress.UserID, + ID: newAddress.ID, + Province: newAddress.Province, + Regency: newAddress.Regency, + District: newAddress.District, + Village: newAddress.Village, + PostalCode: newAddress.PostalCode, + Detail: newAddress.Detail, + Geography: newAddress.Geography, + CreatedAt: createdAt, + UpdatedAt: createdAt, + } + + return addressResponse, nil +} diff --git a/model/address_model.go b/model/address_model.go index 180e431..1da69ec 100644 --- a/model/address_model.go +++ b/model/address_model.go @@ -3,16 +3,16 @@ package model import "time" type Address 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"` - Province string `gorm:"not null" json:"province"` - District string `gorm:"not null" json:"district"` - Subdistrict string `gorm:"not null" json:"subdistrict"` - PostalCode int `gorm:"not null" json:"postalCode"` - Village string `gorm:"not null" json:"village"` - Detail string `gorm:"not null" json:"detail"` - Geography string `gorm:"not null" json:"geography"` - 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"` + UserID string `gorm:"not null" json:"userId"` + User User `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` + Province string `gorm:"not null" json:"province"` + Regency string `gorm:"not null" json:"regency"` + District string `gorm:"not null" json:"district"` + 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"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` } diff --git a/presentation/address_route.go b/presentation/address_route.go new file mode 100644 index 0000000..7f06767 --- /dev/null +++ b/presentation/address_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" +) + +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) +} diff --git a/presentation/role_route.go b/presentation/role_route.go index ff19b49..d4652a1 100644 --- a/presentation/role_route.go +++ b/presentation/role_route.go @@ -1,4 +1,3 @@ -// presentation/role_route.go package presentation import ( diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index 7d8479d..52f1637 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -9,9 +9,11 @@ import ( func SetupRoutes(app *fiber.App) { api := app.Group("/apirijikid") api.Use(middleware.APIKeyMiddleware) + presentation.AuthRouter(api) presentation.UserProfileRouter(api) presentation.UserPinRouter(api) presentation.RoleRouter(api) presentation.WilayahRouter(api) + presentation.AddressRouter(api) } diff --git a/utils/response.go b/utils/response.go index d26eafe..2f8d59d 100644 --- a/utils/response.go +++ b/utils/response.go @@ -84,7 +84,7 @@ func GenericErrorResponse(c *fiber.Ctx, status int, message string) error { return c.Status(status).JSON(response) } -func LogResponse(c *fiber.Ctx, data interface{}, message string) error { +func SuccessResponse(c *fiber.Ctx, data interface{}, message string) error { response := APIResponse{ Meta: MetaData{ Status: fiber.StatusOK, @@ -94,3 +94,14 @@ func LogResponse(c *fiber.Ctx, data interface{}, message string) error { } 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) +}