refact: refact code for optimization

This commit is contained in:
pahmiudahgede 2025-01-24 20:17:15 +07:00
parent dd7fca697d
commit 83d6a09600
12 changed files with 474 additions and 363 deletions

View File

@ -1,112 +0,0 @@
package config
import (
"context"
"fmt"
"log"
"os"
"github.com/go-redis/redis/v8"
"github.com/pahmiudahgede/senggoldong/domain"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var (
DB *gorm.DB
DBHost string
DBPort string
DBName string
DBUser string
DBPassword string
APIKey string
ServerHost string
ServerPort string
RedisClient *redis.Client
RedisHost string
RedisPort string
RedisPassword string
RedisDB int
)
func Context() context.Context {
return context.Background()
}
func InitConfig() {
ServerHost = os.Getenv("SERVER_HOST")
ServerPort = os.Getenv("SERVER_PORT")
DBHost = os.Getenv("DB_HOST")
DBPort = os.Getenv("DB_PORT")
DBName = os.Getenv("DB_NAME")
DBUser = os.Getenv("DB_USER")
DBPassword = os.Getenv("DB_PASSWORD")
APIKey = os.Getenv("API_KEY")
RedisHost = os.Getenv("REDIS_HOST")
RedisPort = os.Getenv("REDIS_PORT")
RedisPassword = os.Getenv("REDIS_PASSWORD")
RedisDB = 0
if ServerHost == "" || ServerPort == "" || DBHost == "" || DBPort == "" || DBName == "" || DBUser == "" || DBPassword == "" || APIKey == "" {
log.Fatal("Error: environment variables yang dibutuhkan tidak ada")
}
}
func InitDatabase() {
InitConfig()
dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
DBHost, DBPort, DBUser, DBName, DBPassword)
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("gagal terhubung ke database: ", err)
}
err = DB.AutoMigrate(
&domain.Point{},
&domain.User{},
&domain.UserRole{},
&domain.UserPin{},
&domain.MenuAccess{},
&domain.PlatformHandle{},
&domain.Address{},
&domain.Article{},
&domain.TrashCategory{},
&domain.TrashDetail{},
&domain.Banner{},
&domain.CoverageArea{},
&domain.CoverageDistric{},
&domain.CoverageSubdistrict{},
&domain.RequestPickup{},
&domain.RequestItem{},
&domain.Product{},
&domain.ProductImage{},
&domain.Store{},
)
if err != nil {
log.Fatal("Error: Failed to auto migrate domain:", err)
}
fmt.Println("Koneksi ke database berhasil dan migrasi dilakukan")
}
func InitRedis() {
InitConfig()
RedisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", RedisHost, RedisPort),
Password: RedisPassword,
DB: RedisDB,
})
_, err := RedisClient.Ping(context.Background()).Result()
if err != nil {
log.Fatal("Gagal terhubung ke Redis:", err)
}
fmt.Println("Koneksi ke Redis berhasil")
}

56
config/database.go Normal file
View File

@ -0,0 +1,56 @@
package config
import (
"fmt"
"log"
"github.com/pahmiudahgede/senggoldong/domain"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func InitDatabase() {
InitConfig()
dsn := fmt.Sprintf(
"host=%s port=%s user=%s dbname=%s password=%s sslmode=disable",
DBHost, DBPort, DBUser, DBName, DBPassword,
)
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Error: Gagal terhubung ke database: %v", err)
}
err = DB.AutoMigrate(
&domain.Point{},
&domain.User{},
&domain.UserRole{},
&domain.UserPin{},
&domain.MenuAccess{},
&domain.PlatformHandle{},
&domain.Address{},
&domain.Article{},
&domain.TrashCategory{},
&domain.TrashDetail{},
&domain.Banner{},
&domain.CoverageArea{},
&domain.CoverageDistric{},
&domain.CoverageSubdistrict{},
&domain.RequestPickup{},
&domain.RequestItem{},
&domain.Product{},
&domain.ProductImage{},
&domain.Store{},
)
if err != nil {
log.Fatalf("Error: Gagal melakukan migrasi schema: %v", err)
}
log.Println("Koneksi ke database berhasil dan migrasi schema juga berhasil")
}

44
config/loader.go Normal file
View File

@ -0,0 +1,44 @@
package config
import (
"log"
"os"
)
var (
DBHost string
DBPort string
DBName string
DBUser string
DBPassword string
RedisHost string
RedisPort string
RedisPassword string
RedisDB int
ServerHost string
ServerPort string
APIKey string
)
func InitConfig() {
ServerHost = os.Getenv("SERVER_HOST")
ServerPort = os.Getenv("SERVER_PORT")
DBHost = os.Getenv("DB_HOST")
DBPort = os.Getenv("DB_PORT")
DBName = os.Getenv("DB_NAME")
DBUser = os.Getenv("DB_USER")
DBPassword = os.Getenv("DB_PASSWORD")
APIKey = os.Getenv("API_KEY")
RedisHost = os.Getenv("REDIS_HOST")
RedisPort = os.Getenv("REDIS_PORT")
RedisPassword = os.Getenv("REDIS_PASSWORD")
RedisDB = 0
if ServerHost == "" || ServerPort == "" || DBHost == "" || DBPort == "" || DBName == "" || DBUser == "" || DBPassword == "" || APIKey == "" {
log.Fatal("Error: Beberapa environment variables tidak ditemukan.")
}
}

61
config/redis.go Normal file
View File

@ -0,0 +1,61 @@
package config
import (
"context"
"fmt"
"log"
"time"
"github.com/go-redis/redis/v8"
)
var RedisClient *redis.Client
func Context() context.Context {
return context.Background()
}
func InitRedis() {
InitConfig()
RedisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", RedisHost, RedisPort),
Password: RedisPassword,
DB: RedisDB,
})
_, err := RedisClient.Ping(context.Background()).Result()
if err != nil {
log.Fatalf("Error: Gagal terhubung ke Redis: %v", err)
}
log.Println("Koneksi ke Redis berhasil.")
}
func GetFromCache(key string) (string, error) {
val, err := RedisClient.Get(Context(), key).Result()
if err == redis.Nil {
return "", nil
} else if err != nil {
return "", err
}
return val, nil
}
func SetToCache(key string, value string, ttl time.Duration) error {
err := RedisClient.Set(Context(), key, value, ttl).Err()
if err != nil {
return err
}
return nil
}
func DeleteFromCache(key string) error {
err := RedisClient.Del(Context(), key).Err()
if err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,5 @@
package dto
import "github.com/go-playground/validator/v10"
type PointResponse struct {
ID string `json:"id"`
CoinName string `json:"coin_name"`
@ -10,39 +8,12 @@ type PointResponse struct {
UpdatedAt string `json:"updatedAt"`
}
func NewPointResponse(id, coinName string, valuePerUnit float64, createdAt, updatedAt string) PointResponse {
return PointResponse{
ID: id,
CoinName: coinName,
ValuePerUnit: valuePerUnit,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}
type PointRequest struct {
type PointCreateRequest struct {
CoinName string `json:"coin_name" validate:"required"`
ValuePerUnit float64 `json:"value_perunit" validate:"required,gt=0"`
}
func NewPointRequest(coinName string, valuePerUnit float64) PointRequest {
return PointRequest{
CoinName: coinName,
ValuePerUnit: valuePerUnit,
}
}
func (p *PointRequest) Validate() error {
validate := validator.New()
return validate.Struct(p)
}
type PointUpdateDTO struct {
CoinName string `json:"coin_name" validate:"required"`
ValuePerUnit float64 `json:"value_perunit" validate:"required,gt=0"`
}
func (p *PointUpdateDTO) Validate() error {
validate := validator.New()
return validate.Struct(p)
type PointUpdateRequest struct {
CoinName string `json:"coin_name,omitempty"`
ValuePerUnit float64 `json:"value_perunit,omitempty"`
}

View File

@ -4,22 +4,30 @@ import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/internal/controllers"
"github.com/pahmiudahgede/senggoldong/internal/middleware"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
func AppRouter(app *fiber.App) {
// # init
pointRepo := repositories.NewPointRepository()
pointService := services.NewPointService(pointRepo)
pointController := controllers.NewPointController(pointService)
// # api group domain endpoint #
api := app.Group("/apirijikid")
// # API Secure #
api.Use(middleware.APIKeyMiddleware)
api.Use(middleware.RateLimitMiddleware)
// # user initial coint #
api.Get("/user/initial-coint", controllers.GetUserInitialCoint)
api.Get("/user/initial-coint/:id", controllers.GetUserInitialCointById)
api.Post("/user/initial-coint", middleware.RoleRequired(utils.RoleAdministrator), controllers.CreatePoint)
api.Put("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), controllers.UpdatePoint)
api.Delete("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), controllers.DeletePoint)
api.Get("/user/initial-coint", pointController.GetAllPoints)
api.Get("/user/initial-coint/:id", pointController.GetPointByID)
api.Post("/user/initial-coint", middleware.RoleRequired(utils.RoleAdministrator), pointController.CreatePoint)
api.Put("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), pointController.UpdatePoint)
api.Delete("/user/initial-coint/:id", middleware.RoleRequired(utils.RoleAdministrator), pointController.DeletePoint)
//# coverage area #
api.Get("/coverage-areas", controllers.GetCoverageAreas)

View File

@ -7,183 +7,116 @@ import (
"github.com/pahmiudahgede/senggoldong/utils"
)
func GetUserInitialCoint(c *fiber.Ctx) error {
points, err := services.GetPoints()
type PointController struct {
service *services.PointService
}
func NewPointController(service *services.PointService) *PointController {
return &PointController{service: service}
}
func (pc *PointController) GetAllPoints(c *fiber.Ctx) error {
points, err := pc.service.GetAllPoints()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse(
return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse(
fiber.StatusInternalServerError,
"Failed to fetch points",
nil,
))
}
var pointResponses []dto.PointResponse
for _, point := range points {
pointResponses = append(pointResponses, dto.PointResponse{
ID: point.ID,
CoinName: point.CoinName,
ValuePerUnit: point.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt),
})
}
if len(pointResponses) == 0 {
return c.Status(fiber.StatusOK).JSON(utils.FormatResponse(
fiber.StatusOK,
"Points successfully displayed but no data",
nil,
))
}
return c.Status(fiber.StatusOK).JSON(utils.FormatResponse(
fiber.StatusOK,
"Points fetched successfully",
pointResponses,
points,
))
}
func GetUserInitialCointById(c *fiber.Ctx) error {
func (pc *PointController) GetPointByID(c *fiber.Ctx) error {
id := c.Params("id")
point, err := services.GetPointByID(id)
point, err := pc.service.GetPointByID(id)
if err != nil {
if err.Error() == "point not found" {
return c.Status(fiber.StatusNotFound).JSON(utils.FormatResponse(
fiber.StatusNotFound,
"Point not found",
nil,
))
}
return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse(
fiber.StatusInternalServerError,
"Failed to fetch point",
nil,
return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse(
fiber.StatusNotFound,
"Point not found",
))
}
pointResponse := dto.PointResponse{
ID: point.ID,
CoinName: point.CoinName,
ValuePerUnit: point.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt),
}
return c.Status(fiber.StatusOK).JSON(utils.FormatResponse(
fiber.StatusOK,
"Point fetched successfully",
pointResponse,
point,
))
}
func CreatePoint(c *fiber.Ctx) error {
var pointInput dto.PointRequest
func (pc *PointController) CreatePoint(c *fiber.Ctx) error {
var request dto.PointCreateRequest
if err := c.BodyParser(&pointInput); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse(
if err := c.BodyParser(&request); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.ErrorResponse(
fiber.StatusBadRequest,
"Invalid input data",
nil,
"Invalid request body",
))
}
if err := pointInput.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse(
fiber.StatusBadRequest,
"Validation failed: "+err.Error(),
nil,
))
}
newPoint, err := services.CreatePoint(pointInput.CoinName, pointInput.ValuePerUnit)
point, err := pc.service.CreatePoint(&request)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse(
return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse(
fiber.StatusInternalServerError,
"Failed to create point",
nil,
err.Error(),
))
}
pointResponse := dto.NewPointResponse(
newPoint.ID,
newPoint.CoinName,
newPoint.ValuePerUnit,
utils.FormatDateToIndonesianFormat(newPoint.CreatedAt),
utils.FormatDateToIndonesianFormat(newPoint.UpdatedAt),
)
return c.Status(fiber.StatusCreated).JSON(utils.FormatResponse(
fiber.StatusCreated,
"Point created successfully",
struct {
Point dto.PointResponse `json:"point"`
}{
Point: pointResponse,
},
point,
))
}
func UpdatePoint(c *fiber.Ctx) error {
func (pc *PointController) UpdatePoint(c *fiber.Ctx) error {
id := c.Params("id")
var request dto.PointUpdateRequest
var pointInput dto.PointUpdateDTO
if err := c.BodyParser(&pointInput); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse(
if err := c.BodyParser(&request); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.ErrorResponse(
fiber.StatusBadRequest,
"Invalid input data",
nil,
"Invalid request body",
))
}
if err := pointInput.Validate(); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(utils.FormatResponse(
fiber.StatusBadRequest,
"Validation failed: "+err.Error(),
nil,
))
}
updatedPoint, err := services.UpdatePoint(id, pointInput.CoinName, pointInput.ValuePerUnit)
point, err := pc.service.UpdatePoint(id, &request)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse(
if err.Error() == "point not found" {
return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse(
fiber.StatusNotFound,
"Point not found",
))
}
return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse(
fiber.StatusInternalServerError,
"Failed to update point",
nil,
err.Error(),
))
}
pointResponse := dto.NewPointResponse(
updatedPoint.ID,
updatedPoint.CoinName,
updatedPoint.ValuePerUnit,
utils.FormatDateToIndonesianFormat(updatedPoint.CreatedAt),
utils.FormatDateToIndonesianFormat(updatedPoint.UpdatedAt),
)
return c.Status(fiber.StatusOK).JSON(utils.FormatResponse(
fiber.StatusOK,
"Point updated successfully",
struct {
Point dto.PointResponse `json:"point"`
}{
Point: pointResponse,
},
point,
))
}
func DeletePoint(c *fiber.Ctx) error {
func (pc *PointController) DeletePoint(c *fiber.Ctx) error {
id := c.Params("id")
err := services.DeletePoint(id)
err := pc.service.DeletePoint(id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(utils.FormatResponse(
if err.Error() == "point not found" {
return c.Status(fiber.StatusNotFound).JSON(utils.ErrorResponse(
fiber.StatusNotFound,
"Point not found",
))
}
return c.Status(fiber.StatusInternalServerError).JSON(utils.ErrorResponse(
fiber.StatusInternalServerError,
"Failed to delete point",
nil,
err.Error(),
))
}
@ -192,4 +125,4 @@ func DeletePoint(c *fiber.Ctx) error {
"Point deleted successfully",
nil,
))
}
}

View File

@ -1,28 +1,54 @@
package middleware
import (
"context"
"fmt"
"os"
"time"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/config"
"github.com/pahmiudahgede/senggoldong/utils"
)
func APIKeyMiddleware(c *fiber.Ctx) error {
apiKey := c.Get("x-api-key")
expectedAPIKey := os.Getenv("API_KEY")
if apiKey != expectedAPIKey {
response := utils.FormatResponse(
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid API Key",
nil,
)
return c.Status(fiber.StatusUnauthorized).JSON(response)
))
}
return c.Next()
}
func RateLimitMiddleware(c *fiber.Ctx) error {
apiKey := c.Get("x-api-key")
if apiKey == "" {
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"API Key is missing",
nil,
))
}
ctx := context.Background()
rateLimitKey := fmt.Sprintf("rate_limit:%s", apiKey)
count, _ := config.RedisClient.Incr(ctx, rateLimitKey).Result()
if count > 100 {
return c.Status(fiber.StatusTooManyRequests).JSON(utils.FormatResponse(
fiber.StatusTooManyRequests,
"Rate limit exceeded",
nil,
))
}
config.RedisClient.Expire(ctx, rateLimitKey, time.Minute)
return c.Next()
}

View File

@ -13,9 +13,18 @@ import (
"github.com/pahmiudahgede/senggoldong/utils"
)
func containsRole(roles []string, role string) bool {
for _, r := range roles {
if r == role {
return true
}
}
return false
}
func RoleRequired(roles ...string) fiber.Handler {
return func(c *fiber.Ctx) error {
tokenString := c.Get("Authorization")
tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ")
if tokenString == "" {
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
@ -24,8 +33,6 @@ func RoleRequired(roles ...string) fiber.Handler {
))
}
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
ctx := context.Background()
cachedToken, err := config.RedisClient.Get(ctx, "auth_token:"+tokenString).Result()
if err != nil || cachedToken == "" {
@ -63,7 +70,7 @@ func RoleRequired(roles ...string) fiber.Handler {
if !ok || userID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid or missing user ID in token",
"Missing or invalid user ID in token",
nil,
))
}
@ -72,7 +79,7 @@ func RoleRequired(roles ...string) fiber.Handler {
if !ok || role == "" {
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid or missing role in token",
"Missing or invalid role in token",
nil,
))
}
@ -80,59 +87,62 @@ func RoleRequired(roles ...string) fiber.Handler {
c.Locals("userID", userID)
c.Locals("role", role)
for _, r := range roles {
if r == role {
return c.Next()
}
if !containsRole(roles, role) {
return c.Status(fiber.StatusForbidden).JSON(utils.FormatResponse(
fiber.StatusForbidden,
"You do not have permission to access this resource",
nil,
))
}
return c.Status(fiber.StatusForbidden).JSON(utils.FormatResponse(
fiber.StatusForbidden,
"You do not have permission to access this resource",
nil,
))
return c.Next()
}
}
func AuthMiddleware(c *fiber.Ctx) error {
tokenString := c.Get("Authorization")
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
tokenString := strings.TrimPrefix(c.Get("Authorization"), "Bearer ")
if tokenString == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"message": "Missing or invalid token",
})
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Missing or invalid token",
nil,
))
}
ctx := context.Background()
cachedToken, err := config.RedisClient.Get(ctx, "auth_token:"+tokenString).Result()
if err != nil || cachedToken == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"message": "Invalid or expired token",
})
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid or expired token",
nil,
))
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("API_KEY")), nil
})
if err != nil || !token.Valid {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"message": "Invalid or expired token",
})
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid or expired token",
nil,
))
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"message": "Invalid token claims",
})
return c.Status(fiber.StatusUnauthorized).JSON(utils.FormatResponse(
fiber.StatusUnauthorized,
"Invalid token claims",
nil,
))
}
userID := claims["sub"].(string)
c.Locals("userID", userID)
config.RedisClient.Expire(ctx, "auth_token:"+tokenString, time.Hour*24).Err()
config.RedisClient.Expire(ctx, "auth_token:"+tokenString, time.Hour*24)
return c.Next()
}

View File

@ -1,44 +1,44 @@
package repositories
import (
"errors"
"github.com/pahmiudahgede/senggoldong/config"
"github.com/pahmiudahgede/senggoldong/domain"
)
func GetPoints() ([]domain.Point, error) {
type PointRepository struct{}
func NewPointRepository() *PointRepository {
return &PointRepository{}
}
func (r *PointRepository) GetAll() ([]domain.Point, error) {
var points []domain.Point
if err := config.DB.Find(&points).Error; err != nil {
return nil, err
err := config.DB.Find(&points).Error
if err != nil {
return nil, errors.New("failed to fetch points from database")
}
return points, nil
}
func GetPointByID(id string) (domain.Point, error) {
func (r *PointRepository) GetByID(id string) (*domain.Point, error) {
var point domain.Point
if err := config.DB.Where("id = ?", id).First(&point).Error; err != nil {
return point, err
err := config.DB.First(&point, "id = ?", id).Error
if err != nil {
return nil, errors.New("point not found")
}
return point, nil
return &point, nil
}
func CreatePoint(point *domain.Point) error {
if err := config.DB.Create(point).Error; err != nil {
return err
}
return nil
func (r *PointRepository) Create(point *domain.Point) error {
return config.DB.Create(point).Error
}
func UpdatePoint(point *domain.Point) error {
if err := config.DB.Save(point).Error; err != nil {
return err
}
return nil
func (r *PointRepository) Update(point *domain.Point) error {
return config.DB.Save(point).Error
}
func DeletePoint(id string) error {
if err := config.DB.Where("id = ?", id).Delete(&domain.Point{}).Error; err != nil {
return err
}
return nil
func (r *PointRepository) Delete(point *domain.Point) error {
return config.DB.Delete(point).Error
}

View File

@ -1,65 +1,170 @@
package services
import (
"encoding/json"
"errors"
"time"
"github.com/pahmiudahgede/senggoldong/config"
"github.com/pahmiudahgede/senggoldong/domain"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/utils"
)
func GetPoints() ([]domain.Point, error) {
return repositories.GetPoints()
type PointService struct {
repo *repositories.PointRepository
}
func GetPointByID(id string) (domain.Point, error) {
point, err := repositories.GetPointByID(id)
func NewPointService(repo *repositories.PointRepository) *PointService {
return &PointService{repo: repo}
}
func (s *PointService) GetAllPoints() ([]dto.PointResponse, error) {
ctx := config.Context()
cacheKey := "points:all"
cachedData, err := config.RedisClient.Get(ctx, cacheKey).Result()
if err == nil && cachedData != "" {
var cachedPoints []dto.PointResponse
if err := json.Unmarshal([]byte(cachedData), &cachedPoints); err == nil {
return cachedPoints, nil
}
}
points, err := s.repo.GetAll()
if err != nil {
return domain.Point{}, errors.New("point not found")
return nil, err
}
return point, nil
var result []dto.PointResponse
for _, point := range points {
result = append(result, dto.PointResponse{
ID: point.ID,
CoinName: point.CoinName,
ValuePerUnit: point.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt),
})
}
cacheData, _ := json.Marshal(result)
config.RedisClient.Set(ctx, cacheKey, cacheData, time.Minute*5)
return result, nil
}
func CreatePoint(coinName string, valuePerUnit float64) (domain.Point, error) {
func (s *PointService) GetPointByID(id string) (*dto.PointResponse, error) {
ctx := config.Context()
newPoint := domain.Point{
CoinName: coinName,
ValuePerUnit: valuePerUnit,
cacheKey := "points:" + id
cachedData, err := config.RedisClient.Get(ctx, cacheKey).Result()
if err == nil && cachedData != "" {
var cachedPoint dto.PointResponse
if err := json.Unmarshal([]byte(cachedData), &cachedPoint); err == nil {
return &cachedPoint, nil
}
}
if err := repositories.CreatePoint(&newPoint); err != nil {
return domain.Point{}, err
}
return newPoint, nil
}
func UpdatePoint(id, coinName string, valuePerUnit float64) (domain.Point, error) {
point, err := repositories.GetPointByID(id)
point, err := s.repo.GetByID(id)
if err != nil {
return domain.Point{}, errors.New("point not found")
return nil, err
}
point.CoinName = coinName
point.ValuePerUnit = valuePerUnit
if err := repositories.UpdatePoint(&point); err != nil {
return domain.Point{}, err
result := &dto.PointResponse{
ID: point.ID,
CoinName: point.CoinName,
ValuePerUnit: point.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt),
}
return point, nil
cacheData, _ := json.Marshal(result)
config.RedisClient.Set(ctx, cacheKey, cacheData, time.Minute*5)
return result, nil
}
func DeletePoint(id string) error {
func (s *PointService) CreatePoint(request *dto.PointCreateRequest) (*dto.PointResponse, error) {
_, err := repositories.GetPointByID(id)
if request.CoinName == "" || request.ValuePerUnit <= 0 {
return nil, errors.New("invalid input data")
}
newPoint := &domain.Point{
CoinName: request.CoinName,
ValuePerUnit: request.ValuePerUnit,
}
err := s.repo.Create(newPoint)
if err != nil {
return nil, err
}
ctx := config.Context()
config.RedisClient.Del(ctx, "points:all")
response := &dto.PointResponse{
ID: newPoint.ID,
CoinName: newPoint.CoinName,
ValuePerUnit: newPoint.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(newPoint.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(newPoint.UpdatedAt),
}
return response, nil
}
func (s *PointService) UpdatePoint(id string, request *dto.PointUpdateRequest) (*dto.PointResponse, error) {
point, err := s.repo.GetByID(id)
if err != nil {
return nil, errors.New("point not found")
}
if request.CoinName != "" {
point.CoinName = request.CoinName
}
if request.ValuePerUnit > 0 {
point.ValuePerUnit = request.ValuePerUnit
}
point.UpdatedAt = time.Now()
err = s.repo.Update(point)
if err != nil {
return nil, errors.New("failed to update point")
}
ctx := config.Context()
config.RedisClient.Del(ctx, "points:all")
config.RedisClient.Del(ctx, "points:"+id)
response := &dto.PointResponse{
ID: point.ID,
CoinName: point.CoinName,
ValuePerUnit: point.ValuePerUnit,
CreatedAt: utils.FormatDateToIndonesianFormat(point.CreatedAt),
UpdatedAt: utils.FormatDateToIndonesianFormat(point.UpdatedAt),
}
return response, nil
}
func (s *PointService) DeletePoint(id string) error {
point, err := s.repo.GetByID(id)
if err != nil {
return errors.New("point not found")
}
if err := repositories.DeletePoint(id); err != nil {
return err
err = s.repo.Delete(point)
if err != nil {
return errors.New("failed to delete point")
}
ctx := config.Context()
config.RedisClient.Del(ctx, "points:all")
config.RedisClient.Del(ctx, "points:"+id)
return nil
}

View File

@ -7,7 +7,7 @@ type Meta struct {
type ApiResponse struct {
Meta Meta `json:"meta"`
Data interface{} `json:"data"`
Data interface{} `json:"data,omitempty"`
}
func FormatResponse(statusCode int, message string, data interface{}) ApiResponse {
@ -19,3 +19,12 @@ func FormatResponse(statusCode int, message string, data interface{}) ApiRespons
Data: data,
}
}
func ErrorResponse(statusCode int, message string) ApiResponse {
return ApiResponse{
Meta: Meta{
StatusCode: statusCode,
Message: message,
},
}
}