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 +}