Perbaikan pembacaan data base

This commit is contained in:
2025-08-18 08:45:49 +07:00
parent a7d6005649
commit 6192a64f0a
23 changed files with 1833 additions and 743 deletions

View File

@@ -70,6 +70,42 @@ func LoadConfig() *Config {
}
func (c *Config) loadDatabaseConfigs() {
// Simplified approach: Directly load from environment variables
// This ensures we get the exact values specified in .env
// Primary database configuration
c.Databases["primary"] = DatabaseConfig{
Name: "primary",
Type: getEnv("DB_CONNECTION", "postgres"),
Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432),
Username: getEnv("DB_USERNAME", ""),
Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_DATABASE", "satu_db"),
Schema: getEnv("DB_SCHEMA", "public"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
}
// SATUDATA database configuration
c.Databases["satudata"] = DatabaseConfig{
Name: "satudata",
Type: getEnv("SATUDATA_CONNECTION", "postgres"),
Host: getEnv("SATUDATA_HOST", "localhost"),
Port: getEnvAsInt("SATUDATA_PORT", 5432),
Username: getEnv("SATUDATA_USERNAME", ""),
Password: getEnv("SATUDATA_PASSWORD", ""),
Database: getEnv("SATUDATA_DATABASE", "satu_db"),
Schema: getEnv("SATUDATA_SCHEMA", "public"),
SSLMode: getEnv("SATUDATA_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("SATUDATA_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("SATUDATA_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("SATUDATA_CONN_MAX_LIFETIME", "5m")),
}
// Legacy support for backward compatibility
envVars := os.Environ()
dbConfigs := make(map[string]map[string]string)
@@ -100,35 +136,12 @@ func (c *Config) loadDatabaseConfigs() {
dbConfigs[dbName][property] = value
}
}
// Parse DB_ prefixed variables
if strings.HasPrefix(key, "DB_") && !strings.Contains(key, "_REPLICA_") {
segments := strings.Split(key, "_")
if len(segments) >= 3 {
dbName := strings.ToLower(segments[1])
property := strings.ToLower(strings.Join(segments[2:], "_"))
if dbConfigs[dbName] == nil {
dbConfigs[dbName] = make(map[string]string)
}
dbConfigs[dbName][property] = value
}
}
// Parse legacy format (for backward compatibility)
if strings.HasPrefix(key, "BLUEPRINT_DB_") {
if dbConfigs["primary"] == nil {
dbConfigs["primary"] = make(map[string]string)
}
property := strings.ToLower(strings.TrimPrefix(key, "BLUEPRINT_DB_"))
dbConfigs["primary"][property] = value
}
}
// Create DatabaseConfig from parsed configurations
// Create DatabaseConfig from parsed configurations for additional databases
for name, config := range dbConfigs {
// Skip empty configurations or system configurations
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") {
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" || name == "satudata" {
continue
}
@@ -154,25 +167,8 @@ func (c *Config) loadDatabaseConfigs() {
continue
}
// Handle legacy format
if name == "primary" && dbConfig.Type == "postgres" && dbConfig.Host == "localhost" {
dbConfig.Host = getEnv("BLUEPRINT_DB_HOST", "localhost")
dbConfig.Port = getEnvAsInt("BLUEPRINT_DB_PORT", 5432)
dbConfig.Username = getEnv("BLUEPRINT_DB_USERNAME", "postgres")
dbConfig.Password = getEnv("BLUEPRINT_DB_PASSWORD", "postgres")
dbConfig.Database = getEnv("BLUEPRINT_DB_DATABASE", "api_service")
dbConfig.Schema = getEnv("BLUEPRINT_DB_SCHEMA", "public")
}
c.Databases[name] = dbConfig
}
// Add specific databases from .env if not already parsed
c.addSpecificDatabase("db", "postgres")
c.addSpecificDatabase("simrs", "postgres")
c.addSpecificDatabase("antrian", "mysql")
c.addSpecificDatabase("satudata", "postgres")
c.addSpecificDatabase("mongodb_dev", "mongodb")
}
func (c *Config) loadReadReplicaConfigs() {

View File

@@ -4,13 +4,22 @@ import (
"context"
"database/sql"
"fmt"
"log"
"log" // Import runtime package
// Import debug package
"strconv"
"sync"
"time"
"api-service/internal/config"
_ "github.com/jackc/pgx/v5" // Import pgx driver
_ "gorm.io/driver/postgres" // Import GORM PostgreSQL driver
_ "github.com/go-sql-driver/mysql" // MySQL driver for database/sql
_ "gorm.io/driver/mysql" // GORM MySQL driver
_ "gorm.io/driver/sqlserver" // GORM SQL Server driver
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
@@ -64,7 +73,9 @@ func New(cfg *config.Config) Service {
readBalancer: make(map[string]int),
}
// Load configurations from config
log.Println("Initializing database service...") // Log when the initialization starts
// log.Printf("Current Goroutine ID: %d", runtime.NumGoroutine()) // Log the number of goroutines
// log.Printf("Stack Trace: %s", debug.Stack()) // Log the stack trace
dbManager.loadFromConfig(cfg)
// Initialize all databases
@@ -106,6 +117,15 @@ func (s *service) addDatabase(name string, config config.DatabaseConfig) error {
s.mu.Lock()
defer s.mu.Unlock()
log.Printf("=== Database Connection Debug ===")
log.Printf("Database: %s", name)
log.Printf("Type: %s", config.Type)
log.Printf("Host: %s", config.Host)
log.Printf("Port: %d", config.Port)
log.Printf("Database: %s", config.Database)
log.Printf("Username: %s", config.Username)
log.Printf("SSLMode: %s", config.SSLMode)
var db *sql.DB
var err error
@@ -127,9 +147,12 @@ func (s *service) addDatabase(name string, config config.DatabaseConfig) error {
}
if err != nil {
log.Printf("❌ Error connecting to database %s: %v", name, err)
log.Printf(" Database: %s@%s:%d/%s", config.Username, config.Host, config.Port, config.Database)
return err
}
log.Printf("✅ Successfully connected to database: %s", name)
return s.configureSQLDB(name, db, config.MaxOpenConns, config.MaxIdleConns, config.ConnMaxLifetime)
}
@@ -408,14 +431,27 @@ func (s *service) Health() map[string]map[string]string {
// GetDB returns a specific SQL database connection by name
func (s *service) GetDB(name string) (*sql.DB, error) {
log.Printf("Attempting to get database connection for: %s", name)
s.mu.RLock()
defer s.mu.RUnlock()
db, exists := s.sqlDatabases[name]
if !exists {
log.Printf("Error: database %s not found", name) // Log the error
return nil, fmt.Errorf("database %s not found", name)
}
log.Printf("Current connection pool state for %s: Open: %d, In Use: %d, Idle: %d",
name, db.Stats().OpenConnections, db.Stats().InUse, db.Stats().Idle)
s.mu.RLock()
defer s.mu.RUnlock()
// db, exists := s.sqlDatabases[name]
// if !exists {
// log.Printf("Error: database %s not found", name) // Log the error
// return nil, fmt.Errorf("database %s not found", name)
// }
return db, nil
}
@@ -533,6 +569,3 @@ func (s *service) Close() error {
return nil
}
// Import necessary packages

View File

@@ -1,124 +0,0 @@
package database
import (
"database/sql"
"os"
"testing"
"github.com/stretchr/testify/mock"
)
// Mock for the database service
type MockService struct {
mock.Mock
}
func (m *MockService) Health() map[string]map[string]string {
args := m.Called()
return args.Get(0).(map[string]map[string]string)
}
func (m *MockService) GetDB(name string) (*sql.DB, error) {
args := m.Called(name)
return args.Get(0).(*sql.DB), args.Error(1)
}
func (m *MockService) Close() error {
return m.Called().Error(0)
}
func (m *MockService) ListDBs() []string {
args := m.Called()
return args.Get(0).([]string)
}
func (m *MockService) GetDBType(name string) (DatabaseType, error) {
args := m.Called(name)
return args.Get(0).(DatabaseType), args.Error(1)
}
func TestNew(t *testing.T) {
srv := New()
if srv == nil {
t.Fatal("New() returned nil")
}
}
func TestHealth(t *testing.T) {
srv := New()
stats := srv.Health()
// Since we don't have any databases configured in test, we expect empty stats
if len(stats) == 0 {
t.Log("No databases configured, health check returns empty stats")
return
}
// If we have databases, check their health
for dbName, dbStats := range stats {
if dbStats["status"] != "up" {
t.Errorf("database %s status is not up: %s", dbName, dbStats["status"])
}
if err, ok := dbStats["error"]; ok && err != "" {
t.Errorf("database %s has error: %s", dbName, err)
}
}
}
func TestClose(t *testing.T) {
srv := New()
if srv.Close() != nil {
t.Fatalf("expected Close() to return nil")
}
}
// Test for loading database configurations
func TestLoadDatabaseConfigs(t *testing.T) {
// Set environment variables for testing
os.Setenv("DB_TEST_TYPE", "postgres")
os.Setenv("DB_TEST_HOST", "localhost")
os.Setenv("DB_TEST_PORT", "5432")
os.Setenv("DB_TEST_DATABASE", "testdb")
os.Setenv("DB_TEST_USERNAME", "testuser")
os.Setenv("DB_TEST_PASSWORD", "testpass")
configs := loadDatabaseConfigs()
if len(configs) == 0 {
t.Fatal("Expected database configurations to be loaded")
}
if configs[0].Type != "postgres" {
t.Errorf("Expected database type to be postgres, got %s", configs[0].Type)
}
}
// Test for connection pooling settings
func TestConnectionPooling(t *testing.T) {
srv := New()
// Check health after loading configurations
stats := srv.Health()
if len(stats) == 0 {
t.Fatal("Expected databases to be configured, but found none")
}
db, err := srv.GetDB("testdb")
if err != nil {
t.Fatalf("Failed to get database: %v", err)
}
if db.Stats().MaxOpenConnections != 10 {
t.Errorf("Expected max open connections to be 10, got %d", db.Stats().MaxOpenConnections)
}
}
// Test for error handling during connection
func TestErrorHandling(t *testing.T) {
srv := New()
// Check health to see if it handles errors
stats := srv.Health()
if len(stats) > 0 {
t.Fatal("Expected no databases to be configured, but found some")
}
}

View File

@@ -1,57 +0,0 @@
package handlers
import (
"net/http"
"api-service/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ExampleHandler handles example GET and POST services
type ExampleHandler struct{}
// NewExampleHandler creates a new ExampleHandler
func NewExampleHandler() *ExampleHandler {
return &ExampleHandler{}
}
// GetExample godoc
// @Summary Example GET service
// @Description Returns a simple message for GET request
// @Tags example
// @Accept json
// @Produce json
// @Success 200 {object} models.ExampleGetResponse "Example GET response"
// @Router /api/v1/example [get]
func (h *ExampleHandler) GetExample(c *gin.Context) {
response := models.ExampleGetResponse{
Message: "This is a GET example response",
}
c.JSON(http.StatusOK, response)
}
// PostExample godoc
// @Summary Example POST service
// @Description Accepts a JSON payload and returns a response with an ID
// @Tags example
// @Accept json
// @Produce json
// @Param request body models.ExamplePostRequest true "Example POST request"
// @Success 200 {object} models.ExamplePostResponse "Example POST response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Router /api/v1/example [post]
func (h *ExampleHandler) PostExample(c *gin.Context) {
var req models.ExamplePostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response := models.ExamplePostResponse{
ID: uuid.NewString(),
Message: "Received data for " + req.Name,
}
c.JSON(http.StatusOK, response)
}

View File

@@ -1,55 +0,0 @@
package handlers
import (
"net/http"
"time"
"api-service/internal/models"
"github.com/gin-gonic/gin"
)
// HealthHandler handles health check endpoints
type HealthHandler struct{}
// NewHealthHandler creates a new HealthHandler
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
// GetHealth godoc
// @Summary Health check endpoint
// @Description Returns the health status of the API service
// @Tags health
// @Accept json
// @Produce json
// @Success 200 {object} models.HealthResponse "Health status"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /health [get]
func (h *HealthHandler) GetHealth(c *gin.Context) {
health := models.HealthResponse{
Status: "healthy",
Timestamp: time.Now().Format(time.RFC3339),
Details: map[string]string{
"service": "api-service",
"version": "1.0.0",
},
}
c.JSON(http.StatusOK, health)
}
// HelloWorld godoc
// @Summary Hello World endpoint
// @Description Returns a hello world message
// @Tags root
// @Accept json
// @Produce json
// @Success 200 {object} models.HelloWorldResponse "Hello world message"
// @Router / [get]
func (h *HealthHandler) HelloWorld(c *gin.Context) {
response := models.HelloWorldResponse{
Message: "Hello World",
Version: "1.0.0",
}
c.JSON(http.StatusOK, response)
}

View File

@@ -1,124 +0,0 @@
package handlers
import (
"api-service/internal/models"
"net/http"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ProductHandler handles product services
type ProductHandler struct{}
// NewProductHandler creates a new ProductHandler
func NewProductHandler() *ProductHandler {
return &ProductHandler{}
}
// GetProduct godoc
// @Summary Get product
// @Description Returns a list of products
// @Tags product
// @Accept json
// @Produce json
// @Success 200 {object} models.ProductGetResponse "Product GET response"
// @Router /api/v1/products [get]
func (h *ProductHandler) GetProduct(c *gin.Context) {
response := models.ProductGetResponse{
Message: "List of products",
Data: []string{"Product 1", "Product 2"},
}
c.JSON(http.StatusOK, response)
}
// GetProductByID godoc
// @Summary Get product by ID
// @Description Returns a single product by ID
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID"
// @Success 200 {object} models.ProductGetByIDResponse "Product GET by ID response"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Router /api/v1/products/{id} [get]
func (h *ProductHandler) GetProductByID(c *gin.Context) {
id := c.Param("id")
response := models.ProductGetByIDResponse{
ID: id,
Message: "Product details",
}
c.JSON(http.StatusOK, response)
}
// CreateProduct godoc
// @Summary Create product
// @Description Creates a new product
// @Tags product
// @Accept json
// @Produce json
// @Param request body models.ProductCreateRequest true "Product creation request"
// @Success 201 {object} models.ProductCreateResponse "Product created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Router /api/v1/products [post]
func (h *ProductHandler) CreateProduct(c *gin.Context) {
var req models.ProductCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response := models.ProductCreateResponse{
ID: uuid.NewString(),
Message: "Product created successfully",
Data: req,
}
c.JSON(http.StatusCreated, response)
}
// UpdateProduct godoc
// @Summary Update product
// @Description Updates an existing product
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID"
// @Param request body models.ProductUpdateRequest true "Product update request"
// @Success 200 {object} models.ProductUpdateResponse "Product updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Router /api/v1/products/{id} [put]
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
id := c.Param("id")
var req models.ProductUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
response := models.ProductUpdateResponse{
ID: id,
Message: "Product updated successfully",
Data: req,
}
c.JSON(http.StatusOK, response)
}
// DeleteProduct godoc
// @Summary Delete product
// @Description Deletes a product by ID
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID"
// @Success 200 {object} models.ProductDeleteResponse "Product deleted successfully"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Router /api/v1/products/{id} [delete]
func (h *ProductHandler) DeleteProduct(c *gin.Context) {
id := c.Param("id")
response := models.ProductDeleteResponse{
ID: id,
Message: "Product deleted successfully",
}
c.JSON(http.StatusOK, response)
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
package middleware
import (
models "api-service/internal/models/retribusi"
"net/http"
"api-service/internal/models"
"github.com/gin-gonic/gin"
)

View File

@@ -1,18 +0,0 @@
package models
// ExampleGetResponse represents the response for the GET example service
type ExampleGetResponse struct {
Message string `json:"message"`
}
// ExamplePostRequest represents the request body for the POST example service
type ExamplePostRequest struct {
Name string `json:"name" binding:"required"`
Age int `json:"age" binding:"required"`
}
// ExamplePostResponse represents the response for the POST example service
type ExamplePostResponse struct {
ID string `json:"id"`
Message string `json:"message"`
}

View File

@@ -1,42 +0,0 @@
package models
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status"`
Timestamp string `json:"timestamp"`
Details map[string]string `json:"details"`
}
// HelloWorldResponse represents the hello world response
type HelloWorldResponse struct {
Message string `json:"message"`
Version string `json:"version"`
}
// ErrorResponse represents an error response
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code int `json:"code"`
}
// SuccessResponse represents a generic success response
type SuccessResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Pagination represents pagination metadata
type Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
// PaginatedResponse represents a paginated response
type PaginatedResponse struct {
Data interface{} `json:"data"`
Pagination Pagination `json:"pagination"`
}

View File

@@ -1,50 +0,0 @@
package models
// ProductGetResponse represents the response for GET products
type ProductGetResponse struct {
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ProductGetByIDResponse represents the response for GET product by ID
type ProductGetByIDResponse struct {
ID string `json:"id"`
Message string `json:"message"`
}
// ProductCreateRequest represents the request for creating product
type ProductCreateRequest struct {
Name string `json:"name" binding:"required"`
// Add more fields as needed
}
// ProductCreateResponse represents the response for creating product
type ProductCreateResponse struct {
ID string `json:"id"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ProductUpdateRequest represents the request for updating product
type ProductUpdateRequest struct {
Name string `json:"name" binding:"required"`
// Add more fields as needed
}
// ProductUpdateResponse represents the response for updating product
type ProductUpdateResponse struct {
ID string `json:"id"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ProductDeleteResponse represents the response for deleting product
type ProductDeleteResponse struct {
ID string `json:"id"`
Message string `json:"message"`
}
// ErrorResponse represents an error response
// type ErrorResponse struct {
// Error string `json:"error"`
// }

View File

@@ -1,42 +0,0 @@
package model
import "time"
// Product represents the product domain model
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ProductCreateRequest represents the request for creating a product
type ProductCreateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Price float64 `json:"price" binding:"required,gt=0"`
}
// ProductUpdateRequest represents the request for updating a product
type ProductUpdateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Price float64 `json:"price" binding:"required,gt=0"`
}
// ProductResponse represents the response for product operations
type ProductResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ProductsResponse represents the response for listing products
type ProductsResponse struct {
Data []*Product `json:"data"`
}

View File

@@ -0,0 +1,281 @@
package models
import (
"database/sql"
"encoding/json"
"time"
)
// Retribusi represents the data structure for the retribusi table
// with proper null handling and optimized JSON marshaling
type Retribusi struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort sql.NullInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
Jenis sql.NullString `json:"jenis,omitempty" db:"Jenis"`
Pelayanan sql.NullString `json:"pelayanan,omitempty" db:"Pelayanan"`
Dinas sql.NullString `json:"dinas,omitempty" db:"Dinas"`
KelompokObyek sql.NullString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
KodeTarif sql.NullString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
Tarif sql.NullString `json:"tarif,omitempty" db:"Tarif"`
Satuan sql.NullString `json:"satuan,omitempty" db:"Satuan"`
TarifOvertime sql.NullString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
SatuanOvertime sql.NullString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
RekeningPokok sql.NullString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
RekeningDenda sql.NullString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
Uraian1 sql.NullString `json:"uraian_1,omitempty" db:"Uraian_1"`
Uraian2 sql.NullString `json:"uraian_2,omitempty" db:"Uraian_2"`
Uraian3 sql.NullString `json:"uraian_3,omitempty" db:"Uraian_3"`
}
// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response
func (r Retribusi) MarshalJSON() ([]byte, error) {
type Alias Retribusi
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Jenis *string `json:"jenis,omitempty"`
Pelayanan *string `json:"pelayanan,omitempty"`
Dinas *string `json:"dinas,omitempty"`
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
KodeTarif *string `json:"kode_tarif,omitempty"`
Tarif *string `json:"tarif,omitempty"`
Satuan *string `json:"satuan,omitempty"`
TarifOvertime *string `json:"tarif_overtime,omitempty"`
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
RekeningPokok *string `json:"rekening_pokok,omitempty"`
RekeningDenda *string `json:"rekening_denda,omitempty"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
// Convert sql.Null* to pointers
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Jenis.Valid {
aux.Jenis = &r.Jenis.String
}
if r.Pelayanan.Valid {
aux.Pelayanan = &r.Pelayanan.String
}
if r.Dinas.Valid {
aux.Dinas = &r.Dinas.String
}
if r.KelompokObyek.Valid {
aux.KelompokObyek = &r.KelompokObyek.String
}
if r.KodeTarif.Valid {
aux.KodeTarif = &r.KodeTarif.String
}
if r.Tarif.Valid {
aux.Tarif = &r.Tarif.String
}
if r.Satuan.Valid {
aux.Satuan = &r.Satuan.String
}
if r.TarifOvertime.Valid {
aux.TarifOvertime = &r.TarifOvertime.String
}
if r.SatuanOvertime.Valid {
aux.SatuanOvertime = &r.SatuanOvertime.String
}
if r.RekeningPokok.Valid {
aux.RekeningPokok = &r.RekeningPokok.String
}
if r.RekeningDenda.Valid {
aux.RekeningDenda = &r.RekeningDenda.String
}
if r.Uraian1.Valid {
aux.Uraian1 = &r.Uraian1.String
}
if r.Uraian2.Valid {
aux.Uraian2 = &r.Uraian2.String
}
if r.Uraian3.Valid {
aux.Uraian3 = &r.Uraian3.String
}
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *Retribusi) GetJenis() string {
if r.Jenis.Valid {
return r.Jenis.String
}
return ""
}
func (r *Retribusi) GetDinas() string {
if r.Dinas.Valid {
return r.Dinas.String
}
return ""
}
func (r *Retribusi) GetTarif() string {
if r.Tarif.Valid {
return r.Tarif.String
}
return ""
}
// Response struct untuk GET by ID - diperbaiki struktur
type RetribusiGetByIDResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Request struct untuk create - dioptimalkan dengan validasi
type RetribusiCreateRequest struct {
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk create
type RetribusiCreateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Update request - sama seperti create tapi dengan ID
type RetribusiUpdateRequest struct {
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk update
type RetribusiUpdateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Response struct untuk delete
type RetribusiDeleteResponse struct {
Message string `json:"message"`
ID string `json:"id"`
}
// Enhanced GET response dengan pagination dan aggregation
type RetribusiGetResponse struct {
Message string `json:"message"`
Data []Retribusi `json:"data"`
Meta MetaResponse `json:"meta"`
Summary *AggregateData `json:"summary,omitempty"`
}
// Metadata untuk pagination - dioptimalkan
type MetaResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Aggregate data untuk summary
type AggregateData struct {
TotalActive int `json:"total_active"`
TotalDraft int `json:"total_draft"`
TotalInactive int `json:"total_inactive"`
ByStatus map[string]int `json:"by_status"`
ByDinas map[string]int `json:"by_dinas,omitempty"`
ByJenis map[string]int `json:"by_jenis,omitempty"`
LastUpdated *time.Time `json:"last_updated,omitempty"`
CreatedToday int `json:"created_today"`
UpdatedToday int `json:"updated_today"`
}
// Error response yang konsisten
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
// Filter struct untuk query parameters
type RetribusiFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Jenis *string `json:"jenis,omitempty" form:"jenis"`
Dinas *string `json:"dinas,omitempty" form:"dinas"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
}
// Validation constants
const (
StatusDraft = "draft"
StatusActive = "active"
StatusInactive = "inactive"
StatusDeleted = "deleted"
)
// ValidStatuses untuk validasi
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
// IsValidStatus helper function
func IsValidStatus(status string) bool {
for _, validStatus := range ValidStatuses {
if status == validStatus {
return true
}
}
return false
}

View File

@@ -1,131 +0,0 @@
package product
import (
"context"
"database/sql"
model "api-service/internal/models/product"
)
// Repository defines the interface for product data operations
type Repository interface {
Create(ctx context.Context, product *model.Product) error
GetByID(ctx context.Context, id string) (*model.Product, error)
GetAll(ctx context.Context) ([]*model.Product, error)
Update(ctx context.Context, product *model.Product) error
Delete(ctx context.Context, id string) error
}
// repository implements the Repository interface
type repository struct {
db *sql.DB
}
// NewRepository creates a new product repository
func NewRepository(db *sql.DB) Repository {
return &repository{db: db}
}
// Create adds a new product to the database
func (r *repository) Create(ctx context.Context, product *model.Product) error {
query := `
INSERT INTO products (id, name, description, price, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`
_, err := r.db.ExecContext(ctx, query,
product.ID,
product.Name,
product.Description,
product.Price,
product.CreatedAt,
product.UpdatedAt,
)
return err
}
// GetByID retrieves a product by its ID
func (r *repository) GetByID(ctx context.Context, id string) (*model.Product, error) {
query := `
SELECT id, name, description, price, created_at, updated_at
FROM products
WHERE id = ?
`
var product model.Product
err := r.db.QueryRowContext(ctx, query, id).Scan(
&product.ID,
&product.Name,
&product.Description,
&product.Price,
&product.CreatedAt,
&product.UpdatedAt,
)
if err != nil {
return nil, err
}
return &product, nil
}
// GetAll retrieves all products
func (r *repository) GetAll(ctx context.Context) ([]*model.Product, error) {
query := `
SELECT id, name, description, price, created_at, updated_at
FROM products
ORDER BY created_at DESC
`
rows, err := r.db.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var products []*model.Product
for rows.Next() {
var product model.Product
err := rows.Scan(
&product.ID,
&product.Name,
&product.Description,
&product.Price,
&product.CreatedAt,
&product.UpdatedAt,
)
if err != nil {
return nil, err
}
products = append(products, &product)
}
return products, nil
}
// Update updates an existing product
func (r *repository) Update(ctx context.Context, product *model.Product) error {
query := `
UPDATE products
SET name = ?, description = ?, price = ?, updated_at = ?
WHERE id = ?
`
_, err := r.db.ExecContext(ctx, query,
product.Name,
product.Description,
product.Price,
product.UpdatedAt,
product.ID,
)
return err
}
// Delete removes a product from the database
func (r *repository) Delete(ctx context.Context, id string) error {
query := `DELETE FROM products WHERE id = ?`
_, err := r.db.ExecContext(ctx, query, id)
return err
}

View File

@@ -1,6 +1,7 @@
package v1
import (
retribusiHandlers "api-service/internal/handlers/retribusi"
"net/http"
"api-service/internal/config"
@@ -12,7 +13,6 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
authHandlers "api-service/internal/handlers/auth"
componentHandlers "api-service/internal/handlers/component"
)
// RegisterRoutes registers all API routes for version 1
@@ -35,10 +35,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1 := router.Group("/api/v1")
{
// Public routes (no authentication required)
// Health endpoints
healthHandler := componentHandlers.NewHealthHandler()
v1.GET("/health", healthHandler.GetHealth)
v1.GET("/", healthHandler.HelloWorld)
// Authentication routes
authHandler := authHandlers.NewAuthHandler(authService)
@@ -53,25 +49,19 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.POST("/token/generate", tokenHandler.GenerateToken)
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
// Retribusi endpoints
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
v1.GET("/retribusis", retribusiHandler.GetRetribusi)
v1.GET("/retribusi/:id", retribusiHandler.GetRetribusiByID)
v1.POST("/retribusis", retribusiHandler.CreateRetribusi)
v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi)
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
// Protected routes (require authentication)
protected := v1.Group("/")
protected.Use(middleware.JWTAuthMiddleware(authService))
{
// Product endpoints
productHandler := componentHandlers.NewProductHandler()
protected.GET("/products", productHandler.GetProduct)
protected.GET("/products/:id", productHandler.GetProductByID)
protected.POST("/products", productHandler.CreateProduct)
protected.PUT("/products/:id", productHandler.UpdateProduct)
protected.DELETE("/products/:id", productHandler.DeleteProduct)
// Example endpoints
exampleHandler := componentHandlers.NewExampleHandler()
protected.GET("/example", exampleHandler.GetExample)
protected.POST("/example", exampleHandler.PostExample)
// WebSocket endpoint
protected.GET("/websocket", WebSocketHandler)
protected.GET("/webservice", WebServiceHandler)

View File

@@ -14,6 +14,8 @@ import (
v1 "api-service/internal/routes/v1"
)
var dbService database.Service // Global variable to hold the database service instance
type Server struct {
port int
db database.Service
@@ -29,9 +31,13 @@ func NewServer() *http.Server {
port = cfg.Server.Port
}
if dbService == nil { // Check if the database service is already initialized
dbService = database.New(cfg) // Initialize only once
}
NewServer := &Server{
port: port,
db: database.New(cfg),
db: dbService, // Use the global database service instance
}
// Declare Server config