From 6192a64f0a926668f6b2ce69b241b54fe821752a Mon Sep 17 00:00:00 2001 From: Meninjar Date: Mon, 18 Aug 2025 08:45:49 +0700 Subject: [PATCH] Perbaikan pembacaan data base --- README.md | 4 +- cmd/api/main.go | 7 + diagnostic/main.go | 130 +++ example.env | 16 +- go.mod | 33 +- go.sum | 153 ++- internal/config/config.go | 80 +- internal/database/database.go | 43 +- internal/database/database_test.go | 124 -- internal/handlers/component/example.go | 57 - internal/handlers/component/health.go | 55 - internal/handlers/component/product.go | 124 -- internal/handlers/retribusi/retribusi.go | 1015 +++++++++++++++++ internal/middleware/error_handler.go | 3 +- internal/models/example.go | 18 - internal/models/health.go | 42 - internal/models/product.go | 50 - internal/models/product/product.go | 42 - internal/models/retribusi/retribusi.go | 281 +++++ .../repository/product/product_repository.go | 131 --- internal/routes/v1/routes.go | 30 +- internal/server/server.go | 8 +- tools/diagnostic.go | 130 +++ 23 files changed, 1833 insertions(+), 743 deletions(-) create mode 100644 diagnostic/main.go delete mode 100644 internal/database/database_test.go delete mode 100644 internal/handlers/component/example.go delete mode 100644 internal/handlers/component/health.go delete mode 100644 internal/handlers/component/product.go create mode 100644 internal/handlers/retribusi/retribusi.go delete mode 100644 internal/models/example.go delete mode 100644 internal/models/health.go delete mode 100644 internal/models/product.go delete mode 100644 internal/models/product/product.go create mode 100644 internal/models/retribusi/retribusi.go delete mode 100644 internal/repository/product/product_repository.go create mode 100644 tools/diagnostic.go diff --git a/README.md b/README.md index f8b1a0a..7348a1a 100644 --- a/README.md +++ b/README.md @@ -230,13 +230,13 @@ After starting the server, visit: ### CLI Command to Generate Swagger Documentation Generate Swagger documentation using the following command: ```bash -swag init -g cmd/api/main.go --output cmd/api/docs +swag init -g cmd/api/main.go --output docs ``` This command will: - Scan your Go source code for Swagger annotations - Generate OpenAPI 3.0 specification files -- Create/update the documentation in `cmd/api/docs/` directory +- Create/update the documentation in `docs/` directory - Include all API endpoints, models, and authentication details Make sure to run this command whenever you add new endpoints or modify existing API documentation. diff --git a/cmd/api/main.go b/cmd/api/main.go index b1718f7..edb860b 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -11,6 +11,8 @@ import ( "api-service/internal/server" + "github.com/joho/godotenv" // Import the godotenv package + _ "api-service/docs" ) @@ -58,6 +60,11 @@ func gracefulShutdown(apiServer *http.Server, done chan bool) { func main() { log.Println("Starting API Service...") + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Fatal("Error loading .env file") + } + server := server.NewServer() // Create a done channel to signal when the shutdown is complete diff --git a/diagnostic/main.go b/diagnostic/main.go new file mode 100644 index 0000000..e1c5a2c --- /dev/null +++ b/diagnostic/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "os" + + _ "github.com/jackc/pgx/v5/stdlib" + "github.com/joho/godotenv" +) + +func main() { + fmt.Println("=== Database Connection Diagnostic Tool ===") + + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Printf("Warning: Error loading .env file: %v", err) + } + + // Get configuration from environment + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + username := os.Getenv("DB_USERNAME") + password := os.Getenv("DB_PASSWORD") + database := os.Getenv("DB_DATABASE") + sslmode := os.Getenv("DB_SSLMODE") + + if sslmode == "" { + sslmode = "disable" + } + + fmt.Printf("Host: %s\n", host) + fmt.Printf("Port: %s\n", port) + fmt.Printf("Username: %s\n", username) + fmt.Printf("Database: %s\n", database) + fmt.Printf("SSL Mode: %s\n", sslmode) + + if host == "" || username == "" || password == "" { + fmt.Println("❌ Missing required environment variables") + return + } + + // Test connection to PostgreSQL server + fmt.Println("\n--- Testing PostgreSQL Server Connection ---") + serverConnStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=%s", + host, port, username, password, sslmode) + + db, err := sql.Open("pgx", serverConnStr) + if err != nil { + fmt.Printf("❌ Failed to connect to PostgreSQL server: %v\n", err) + return + } + defer db.Close() + + err = db.Ping() + if err != nil { + fmt.Printf("❌ Failed to ping PostgreSQL server: %v\n", err) + return + } + + fmt.Println("✅ Successfully connected to PostgreSQL server") + + // Check if database exists + fmt.Println("\n--- Checking Database Existence ---") + var exists bool + err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", database).Scan(&exists) + if err != nil { + fmt.Printf("❌ Failed to check database existence: %v\n", err) + return + } + + if !exists { + fmt.Printf("❌ Database '%s' does not exist\n", database) + + // List available databases + fmt.Println("\n--- Available Databases ---") + rows, err := db.Query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname") + if err != nil { + fmt.Printf("❌ Failed to list databases: %v\n", err) + return + } + defer rows.Close() + + fmt.Println("Available databases:") + for rows.Next() { + var dbName string + if err := rows.Scan(&dbName); err != nil { + continue + } + fmt.Printf(" - %s\n", dbName) + } + return + } + + fmt.Printf("✅ Database '%s' exists\n", database) + + // Test direct connection to the database + fmt.Println("\n--- Testing Direct Database Connection ---") + directConnStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", + host, port, username, password, database, sslmode) + + targetDB, err := sql.Open("pgx", directConnStr) + if err != nil { + fmt.Printf("❌ Failed to connect to database '%s': %v\n", database, err) + return + } + defer targetDB.Close() + + err = targetDB.Ping() + if err != nil { + fmt.Printf("❌ Failed to ping database '%s': %v\n", database, err) + return + } + + fmt.Printf("✅ Successfully connected to database '%s'\n", database) + + // Test basic query + fmt.Println("\n--- Testing Basic Query ---") + var version string + err = targetDB.QueryRow("SELECT version()").Scan(&version) + if err != nil { + fmt.Printf("❌ Failed to execute query: %v\n", err) + return + } + + fmt.Printf("✅ PostgreSQL Version: %s\n", version) + + fmt.Println("\n🎉 All tests passed! Database connection is working correctly.") +} diff --git a/example.env b/example.env index efa0b83..830a9e3 100644 --- a/example.env +++ b/example.env @@ -3,29 +3,15 @@ PORT=8080 GIN_MODE=debug # Primary Database Configuration (PostgreSQL) -DB_USERNAME=stim -DB_PASSWORD=stim*RS54 -DB_HOST=10.10.123.165 -DB_PORT=5432 -DB_DATABASE=satu_db -DB_SSLMODE=disable - SATUDATA_CONNECTION=postgres SATUDATA_USERNAME=stim SATUDATA_PASSWORD=stim*RS54 SATUDATA_HOST=10.10.123.165 SATUDATA_DATABASE=satu_db +SATUDATA_NAME=satu_db SATUDATA_PORT=5000 SATUDATA_SSLMODE=disable -ANTRIAN_CONNECTION=mysqli -ANTRIAN_HOST=10.10.123.160 -ANTRIAN_USERNAME=postgres -ANTRIAN_PASSWORD=zahwa2904 -ANTRIAN_DATABASE=gomed_antrian -ANTRIAN_PORT=5000 -ANTRIAN_SSLMODE=disable - # Keycloak Configuration (optional) KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran diff --git a/go.mod b/go.mod index 0876ed0..80e41d6 100644 --- a/go.mod +++ b/go.mod @@ -6,24 +6,31 @@ require ( github.com/gin-gonic/gin v1.10.1 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 - github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.10.0 - github.com/swaggo/files v1.0.1 - github.com/swaggo/gin-swagger v1.6.0 - github.com/swaggo/swag v1.8.12 + github.com/jackc/pgx/v5 v5.7.2 // Ensure pgx is a direct dependency go.mongodb.org/mongo-driver v1.17.3 golang.org/x/crypto v0.41.0 golang.org/x/sync v0.16.0 + gorm.io/driver/mysql v1.6.0 // GORM MySQL driver + gorm.io/driver/postgres v1.5.11 // Added GORM PostgreSQL driver + gorm.io/driver/sqlserver v1.6.1 // GORM SQL Server driver ) require ( + github.com/go-sql-driver/mysql v1.8.1 + github.com/joho/godotenv v1.5.1 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.6 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -34,22 +41,27 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/snappy v0.0.4 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/microsoft/go-mssqldb v1.8.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect @@ -57,12 +69,13 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect google.golang.org/protobuf v1.36.7 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/gorm v1.30.0 // indirect ) diff --git a/go.sum b/go.sum index f52505a..9db4dd3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,23 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -14,6 +34,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= @@ -40,17 +62,50 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -69,6 +124,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -77,40 +134,51 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= +github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= -github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= -github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -130,46 +198,115 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -180,10 +317,20 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY= +gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= diff --git a/internal/config/config.go b/internal/config/config.go index 8627149..155f170 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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() { diff --git a/internal/database/database.go b/internal/database/database.go index 69824dc..dfec3a3 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -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 - diff --git a/internal/database/database_test.go b/internal/database/database_test.go deleted file mode 100644 index 8331ee6..0000000 --- a/internal/database/database_test.go +++ /dev/null @@ -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") - } -} diff --git a/internal/handlers/component/example.go b/internal/handlers/component/example.go deleted file mode 100644 index 3e8a732..0000000 --- a/internal/handlers/component/example.go +++ /dev/null @@ -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) -} diff --git a/internal/handlers/component/health.go b/internal/handlers/component/health.go deleted file mode 100644 index 50b0d22..0000000 --- a/internal/handlers/component/health.go +++ /dev/null @@ -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) -} diff --git a/internal/handlers/component/product.go b/internal/handlers/component/product.go deleted file mode 100644 index ad5d503..0000000 --- a/internal/handlers/component/product.go +++ /dev/null @@ -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) -} diff --git a/internal/handlers/retribusi/retribusi.go b/internal/handlers/retribusi/retribusi.go new file mode 100644 index 0000000..2aeb858 --- /dev/null +++ b/internal/handlers/retribusi/retribusi.go @@ -0,0 +1,1015 @@ +package handlers + +import ( + "api-service/internal/config" + "api-service/internal/database" + models "api-service/internal/models/retribusi" + "context" + "database/sql" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "github.com/google/uuid" +) + +var ( + db database.Service + once sync.Once + validate *validator.Validate +) + +// Initialize the database connection and validator +func init() { + once.Do(func() { + db = database.New(config.LoadConfig()) + validate = validator.New() + + // Register custom validations if needed + validate.RegisterValidation("retribusi_status", validateRetribusiStatus) + + if db == nil { + log.Fatal("Failed to initialize database connection") + } + }) +} + +// Custom validation for retribusi status +func validateRetribusiStatus(fl validator.FieldLevel) bool { + return models.IsValidStatus(fl.Field().String()) +} + +// RetribusiHandler handles retribusi services +type RetribusiHandler struct { + db database.Service +} + +// NewRetribusiHandler creates a new RetribusiHandler +func NewRetribusiHandler() *RetribusiHandler { + return &RetribusiHandler{ + db: db, + } +} + +// GetRetribusi godoc +// @Summary Get retribusi with pagination and optional aggregation +// @Description Returns a paginated list of retribusis with optional summary statistics +// @Tags retribusi +// @Accept json +// @Produce json +// @Param limit query int false "Limit (max 100)" default(10) +// @Param offset query int false "Offset" default(0) +// @Param include_summary query bool false "Include aggregation summary" default(false) +// @Param status query string false "Filter by status" +// @Param jenis query string false "Filter by jenis" +// @Param dinas query string false "Filter by dinas" +// @Param search query string false "Search in multiple fields" +// @Success 200 {object} models.RetribusiGetResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Bad request" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis [get] +func (h *RetribusiHandler) GetRetribusi(c *gin.Context) { + // Parse pagination parameters + limit, offset, err := h.parsePaginationParams(c) + if err != nil { + h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) + return + } + + // Parse filter parameters + filter := h.parseFilterParams(c) + includeAggregation := c.Query("include_summary") == "true" + + // Get database connection + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Execute concurrent operations + var ( + retribusis []models.Retribusi + total int + aggregateData *models.AggregateData + wg sync.WaitGroup + errChan = make(chan error, 3) + mu sync.Mutex + ) + + // Fetch total count + wg.Add(1) + go func() { + defer wg.Done() + if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { + mu.Lock() + errChan <- fmt.Errorf("failed to get total count: %w", err) + mu.Unlock() + } + }() + + // Fetch main data + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.fetchRetribusis(ctx, dbConn, filter, limit, offset) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to fetch data: %w", err) + } else { + retribusis = result + } + mu.Unlock() + }() + + // Fetch aggregation data if requested + if includeAggregation { + wg.Add(1) + go func() { + defer wg.Done() + result, err := h.getAggregateData(ctx, dbConn, filter) + mu.Lock() + if err != nil { + errChan <- fmt.Errorf("failed to get aggregate data: %w", err) + } else { + aggregateData = result + } + mu.Unlock() + }() + } + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) + return + } + } + + // Build response + meta := h.calculateMeta(limit, offset, total) + response := models.RetribusiGetResponse{ + Message: "Data retribusi berhasil diambil", + Data: retribusis, + Meta: meta, + } + + if includeAggregation && aggregateData != nil { + response.Summary = aggregateData + } + + c.JSON(http.StatusOK, response) +} + +// GetRetribusiByID godoc +// @Summary Get Retribusi by ID +// @Description Returns a single retribusi by ID +// @Tags retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Success 200 {object} models.RetribusiGetByIDResponse "Success response" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [get] +func (h *RetribusiHandler) GetRetribusiByID(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + retribusi, err := h.getRetribusiByID(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to get retribusi", err, http.StatusInternalServerError) + } + return + } + + response := models.RetribusiGetByIDResponse{ + Message: "Retribusi details retrieved successfully", + Data: retribusi, + } + + c.JSON(http.StatusOK, response) +} + +// CreateRetribusi godoc +// @Summary Create retribusi +// @Description Creates a new retribusi record +// @Tags retribusi +// @Accept json +// @Produce json +// @Param request body models.RetribusiCreateRequest true "Retribusi creation request" +// @Success 201 {object} models.RetribusiCreateResponse "Retribusi created successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis [post] +func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) { + var req models.RetribusiCreateRequest + + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + retribusi, err := h.createRetribusi(ctx, dbConn, &req) + if err != nil { + h.logAndRespondError(c, "Failed to create retribusi", err, http.StatusInternalServerError) + return + } + + response := models.RetribusiCreateResponse{ + Message: "Retribusi berhasil dibuat", + Data: retribusi, + } + + c.JSON(http.StatusCreated, response) +} + +// UpdateRetribusi godoc +// @Summary Update retribusi +// @Description Updates an existing retribusi record +// @Tags retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Param request body models.RetribusiUpdateRequest true "Retribusi update request" +// @Success 200 {object} models.RetribusiUpdateResponse "Retribusi updated successfully" +// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [put] +func (h *RetribusiHandler) UpdateRetribusi(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + var req models.RetribusiUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + h.respondError(c, "Invalid request body", err, http.StatusBadRequest) + return + } + + // Set ID from path parameter + req.ID = id + + // Validate request + if err := validate.Struct(&req); err != nil { + h.respondError(c, "Validation failed", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + retribusi, err := h.updateRetribusi(ctx, dbConn, &req) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to update retribusi", err, http.StatusInternalServerError) + } + return + } + + response := models.RetribusiUpdateResponse{ + Message: "Retribusi berhasil diperbarui", + Data: retribusi, + } + + c.JSON(http.StatusOK, response) +} + +// DeleteRetribusi godoc +// @Summary Delete retribusi +// @Description Soft deletes a retribusi by setting status to 'deleted' +// @Tags retribusi +// @Accept json +// @Produce json +// @Param id path string true "Retribusi ID (UUID)" +// @Success 200 {object} models.RetribusiDeleteResponse "Retribusi deleted successfully" +// @Failure 400 {object} models.ErrorResponse "Invalid ID format" +// @Failure 404 {object} models.ErrorResponse "Retribusi not found" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusi/{id} [delete] +func (h *RetribusiHandler) DeleteRetribusi(c *gin.Context) { + id := c.Param("id") + + // Validate UUID format + if _, err := uuid.Parse(id); err != nil { + h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) + return + } + + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + err = h.deleteRetribusi(ctx, dbConn, id) + if err != nil { + if err == sql.ErrNoRows { + h.respondError(c, "Retribusi not found", err, http.StatusNotFound) + } else { + h.logAndRespondError(c, "Failed to delete retribusi", err, http.StatusInternalServerError) + } + return + } + + response := models.RetribusiDeleteResponse{ + Message: "Retribusi berhasil dihapus", + ID: id, + } + + c.JSON(http.StatusOK, response) +} + +// GetRetribusiStats godoc +// @Summary Get retribusi statistics +// @Description Returns comprehensive statistics about retribusi data +// @Tags retribusi +// @Accept json +// @Produce json +// @Param status query string false "Filter statistics by status" +// @Success 200 {object} models.AggregateData "Statistics data" +// @Failure 500 {object} models.ErrorResponse "Internal server error" +// @Router /api/v1/retribusis/stats [get] +func (h *RetribusiHandler) GetRetribusiStats(c *gin.Context) { + dbConn, err := h.db.GetDB("satudata") + if err != nil { + h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) + return + } + + ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) + defer cancel() + + filter := h.parseFilterParams(c) + aggregateData, err := h.getAggregateData(ctx, dbConn, filter) + if err != nil { + h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Statistik retribusi berhasil diambil", + "data": aggregateData, + }) +} + +// Get retribusi by ID +func (h *RetribusiHandler) getRetribusiByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Retribusi, error) { + query := ` + SELECT + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + FROM data_retribusi + WHERE id = $1 AND status != 'deleted'` + + row := dbConn.QueryRowContext(ctx, query, id) + + var retribusi models.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, err + } + + return &retribusi, nil +} + +// Create retribusi +func (h *RetribusiHandler) createRetribusi(ctx context.Context, dbConn *sql.DB, req *models.RetribusiCreateRequest) (*models.Retribusi, error) { + id := uuid.New().String() + now := time.Now() + + query := ` + INSERT INTO data_retribusi ( + id, status, date_created, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) + RETURNING + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"` + + row := dbConn.QueryRowContext(ctx, query, + id, req.Status, now, now, + req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif, + req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime, + req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3, + ) + + var retribusi models.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, fmt.Errorf("failed to create retribusi: %w", err) + } + + return &retribusi, nil +} + +// Update retribusi +func (h *RetribusiHandler) updateRetribusi(ctx context.Context, dbConn *sql.DB, req *models.RetribusiUpdateRequest) (*models.Retribusi, error) { + now := time.Now() + + query := ` + UPDATE data_retribusi SET + status = $2, date_updated = $3, + "Jenis" = $4, "Pelayanan" = $5, "Dinas" = $6, "Kelompok_obyek" = $7, "Kode_tarif" = $8, + "Tarif" = $9, "Satuan" = $10, "Tarif_overtime" = $11, "Satuan_overtime" = $12, + "Rekening_pokok" = $13, "Rekening_denda" = $14, "Uraian_1" = $15, "Uraian_2" = $16, "Uraian_3" = $17 + WHERE id = $1 AND status != 'deleted' + RETURNING + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"` + + row := dbConn.QueryRowContext(ctx, query, + req.ID, req.Status, now, + req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif, + req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime, + req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3, + ) + + var retribusi models.Retribusi + err := row.Scan( + &retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated, + &retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated, + &retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek, + &retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda, + &retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3, + ) + + if err != nil { + return nil, fmt.Errorf("failed to update retribusi: %w", err) + } + + return &retribusi, nil +} + +// Soft delete retribusi +func (h *RetribusiHandler) deleteRetribusi(ctx context.Context, dbConn *sql.DB, id string) error { + now := time.Now() + + query := `UPDATE data_retribusi SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'` + + result, err := dbConn.ExecContext(ctx, query, id, now) + if err != nil { + return fmt.Errorf("failed to delete retribusi: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to get affected rows: %w", err) + } + + if rowsAffected == 0 { + return sql.ErrNoRows + } + + return nil +} + +// Enhanced error handling +func (h *RetribusiHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { + log.Printf("[ERROR] %s: %v", message, err) + h.respondError(c, message, err, statusCode) +} + +func (h *RetribusiHandler) respondError(c *gin.Context, message string, err error, statusCode int) { + errorMessage := message + if gin.Mode() == gin.ReleaseMode { + errorMessage = "Internal server error" + } + + c.JSON(statusCode, models.ErrorResponse{ + Error: errorMessage, + Code: statusCode, + Message: err.Error(), + Timestamp: time.Now(), + }) +} + +// Parse pagination parameters dengan validation yang lebih ketat +func (h *RetribusiHandler) parsePaginationParams(c *gin.Context) (int, int, error) { + limit := 10 // Default limit + offset := 0 // Default offset + + if limitStr := c.Query("limit"); limitStr != "" { + parsedLimit, err := strconv.Atoi(limitStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) + } + if parsedLimit <= 0 { + return 0, 0, fmt.Errorf("limit must be greater than 0") + } + if parsedLimit > 100 { + return 0, 0, fmt.Errorf("limit cannot exceed 100") + } + limit = parsedLimit + } + + if offsetStr := c.Query("offset"); offsetStr != "" { + parsedOffset, err := strconv.Atoi(offsetStr) + if err != nil { + return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) + } + if parsedOffset < 0 { + return 0, 0, fmt.Errorf("offset cannot be negative") + } + offset = parsedOffset + } + + log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset) + return limit, offset, nil +} + +// Build WHERE clause dengan filter parameters +func (h *RetribusiHandler) buildWhereClause(filter models.RetribusiFilter) (string, []interface{}) { + conditions := []string{"status != 'deleted'"} + args := []interface{}{} + paramCount := 1 + + if filter.Status != nil { + conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) + args = append(args, *filter.Status) + paramCount++ + } + + if filter.Jenis != nil { + conditions = append(conditions, fmt.Sprintf(`"Jenis" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.Jenis+"%") + paramCount++ + } + + if filter.Dinas != nil { + conditions = append(conditions, fmt.Sprintf(`"Dinas" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.Dinas+"%") + paramCount++ + } + + if filter.KelompokObyek != nil { + conditions = append(conditions, fmt.Sprintf(`"Kelompok_obyek" ILIKE $%d`, paramCount)) + args = append(args, "%"+*filter.KelompokObyek+"%") + paramCount++ + } + + if filter.Search != nil { + searchCondition := fmt.Sprintf(`( + "Jenis" ILIKE $%d OR + "Pelayanan" ILIKE $%d OR + "Dinas" ILIKE $%d OR + "Kode_tarif" ILIKE $%d OR + "Uraian_1" ILIKE $%d OR + "Uraian_2" ILIKE $%d OR + "Uraian_3" ILIKE $%d + )`, paramCount, paramCount, paramCount, paramCount, paramCount, paramCount,paramCount) + conditions = append(conditions, searchCondition) + searchTerm := "%" + *filter.Search + "%" + args = append(args, searchTerm) + paramCount++ + } + + if filter.DateFrom != nil { + conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) + args = append(args, *filter.DateFrom) + paramCount++ + } + + if filter.DateTo != nil { + conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) + args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) // End of day + paramCount++ + } + + return strings.Join(conditions, " AND "), args +} + +// Optimized scanning function yang menggunakan sql.Null* types langsung +func (h *RetribusiHandler) scanRetribusi(rows *sql.Rows) (models.Retribusi, error) { + var retribusi models.Retribusi + + return retribusi, rows.Scan( + &retribusi.ID, + &retribusi.Status, + &retribusi.Sort, + &retribusi.UserCreated, + &retribusi.DateCreated, + &retribusi.UserUpdated, + &retribusi.DateUpdated, + &retribusi.Jenis, + &retribusi.Pelayanan, + &retribusi.Dinas, + &retribusi.KelompokObyek, + &retribusi.KodeTarif, + &retribusi.Tarif, + &retribusi.Satuan, + &retribusi.TarifOvertime, + &retribusi.SatuanOvertime, + &retribusi.RekeningPokok, + &retribusi.RekeningDenda, + &retribusi.Uraian1, + &retribusi.Uraian2, + &retribusi.Uraian3, + ) +} + +// Parse filter parameters dari query string +func (h *RetribusiHandler) parseFilterParams(c *gin.Context) models.RetribusiFilter { + filter := models.RetribusiFilter{} + + if status := c.Query("status"); status != "" { + if models.IsValidStatus(status) { + filter.Status = &status + } + } + + if jenis := c.Query("jenis"); jenis != "" { + filter.Jenis = &jenis + } + + if dinas := c.Query("dinas"); dinas != "" { + filter.Dinas = &dinas + } + + if kelompokObyek := c.Query("kelompok_obyek"); kelompokObyek != "" { + filter.KelompokObyek = &kelompokObyek + } + + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse date filters + if dateFromStr := c.Query("date_from"); dateFromStr != "" { + if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { + filter.DateFrom = &dateFrom + } + } + + if dateToStr := c.Query("date_to"); dateToStr != "" { + if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { + filter.DateTo = &dateTo + } + } + + return filter +} +// Get comprehensive aggregate data dengan filter support +func (h *RetribusiHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.RetribusiFilter) (*models.AggregateData, error) { + aggregate := &models.AggregateData{ + ByStatus: make(map[string]int), + ByDinas: make(map[string]int), + ByJenis: make(map[string]int), + } + + // Build where clause untuk filter + whereClause, args := h.buildWhereClause(filter) + + // Use concurrent execution untuk performance + var wg sync.WaitGroup + var mu sync.Mutex + errChan := make(chan error, 4) + + // 1. Count by status + wg.Add(1) + go func() { + defer wg.Done() + statusQuery := fmt.Sprintf(` + SELECT status, COUNT(*) + FROM data_retribusi + WHERE %s + GROUP BY status + ORDER BY status`, whereClause) + + rows, err := dbConn.QueryContext(ctx, statusQuery, args...) + if err != nil { + errChan <- fmt.Errorf("status query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var status string + var count int + if err := rows.Scan(&status, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("status scan failed: %w", err) + return + } + aggregate.ByStatus[status] = count + switch status { + case "active": + aggregate.TotalActive = count + case "draft": + aggregate.TotalDraft = count + case "inactive": + aggregate.TotalInactive = count + } + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("status iteration error: %w", err) + } + }() + + // 2. Count by Dinas + wg.Add(1) + go func() { + defer wg.Done() + dinasQuery := fmt.Sprintf(` + SELECT COALESCE("Dinas", 'Unknown') as dinas, COUNT(*) + FROM data_retribusi + WHERE %s AND "Dinas" IS NOT NULL AND TRIM("Dinas") != '' + GROUP BY "Dinas" + ORDER BY COUNT(*) DESC + LIMIT 10`, whereClause) + + rows, err := dbConn.QueryContext(ctx, dinasQuery, args...) + if err != nil { + errChan <- fmt.Errorf("dinas query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var dinas string + var count int + if err := rows.Scan(&dinas, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("dinas scan failed: %w", err) + return + } + aggregate.ByDinas[dinas] = count + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("dinas iteration error: %w", err) + } + }() + + // 3. Count by Jenis + wg.Add(1) + go func() { + defer wg.Done() + jenisQuery := fmt.Sprintf(` + SELECT COALESCE("Jenis", 'Unknown') as jenis, COUNT(*) + FROM data_retribusi + WHERE %s AND "Jenis" IS NOT NULL AND TRIM("Jenis") != '' + GROUP BY "Jenis" + ORDER BY COUNT(*) DESC + LIMIT 10`, whereClause) + + rows, err := dbConn.QueryContext(ctx, jenisQuery, args...) + if err != nil { + errChan <- fmt.Errorf("jenis query failed: %w", err) + return + } + defer rows.Close() + + mu.Lock() + for rows.Next() { + var jenis string + var count int + if err := rows.Scan(&jenis, &count); err != nil { + mu.Unlock() + errChan <- fmt.Errorf("jenis scan failed: %w", err) + return + } + aggregate.ByJenis[jenis] = count + } + mu.Unlock() + + if err := rows.Err(); err != nil { + errChan <- fmt.Errorf("jenis iteration error: %w", err) + } + }() + + // 4. Get last updated time dan today statistics + wg.Add(1) + go func() { + defer wg.Done() + + // Last updated + lastUpdatedQuery := fmt.Sprintf(` + SELECT MAX(date_updated) + FROM data_retribusi + WHERE %s AND date_updated IS NOT NULL`, whereClause) + + var lastUpdated sql.NullTime + if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { + errChan <- fmt.Errorf("last updated query failed: %w", err) + return + } + + // Today statistics + today := time.Now().Format("2006-01-02") + todayStatsQuery := fmt.Sprintf(` + SELECT + SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, + SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today + FROM data_retribusi + WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause) + + todayArgs := append(args, today) + var createdToday, updatedToday int + if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { + errChan <- fmt.Errorf("today stats query failed: %w", err) + return + } + + mu.Lock() + if lastUpdated.Valid { + aggregate.LastUpdated = &lastUpdated.Time + } + aggregate.CreatedToday = createdToday + aggregate.UpdatedToday = updatedToday + mu.Unlock() + }() + + // Wait for all goroutines + wg.Wait() + close(errChan) + + // Check for errors + for err := range errChan { + if err != nil { + return nil, err + } + } + + return aggregate, nil +} +// Get total count dengan filter support +func (h *RetribusiHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.RetribusiFilter, total *int) error { + whereClause, args := h.buildWhereClause(filter) + countQuery := fmt.Sprintf(`SELECT COUNT(*) FROM data_retribusi WHERE %s`, whereClause) + + if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { + return fmt.Errorf("total count query failed: %w", err) + } + + return nil +} +// Enhanced fetchRetribusis dengan filter support +func (h *RetribusiHandler) fetchRetribusis(ctx context.Context, dbConn *sql.DB, filter models.RetribusiFilter, limit, offset int) ([]models.Retribusi, error) { + whereClause, args := h.buildWhereClause(filter) + + // Build the main query with pagination + query := fmt.Sprintf(` + SELECT + id, status, sort, user_created, date_created, user_updated, date_updated, + "Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif", + "Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime", + "Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3" + FROM data_retribusi + WHERE %s + ORDER BY date_created DESC NULLS LAST + LIMIT $%d OFFSET $%d`, + whereClause, len(args)+1, len(args)+2) + + // Add pagination parameters + args = append(args, limit, offset) + + rows, err := dbConn.QueryContext(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("fetch retribusis query failed: %w", err) + } + defer rows.Close() + + // Pre-allocate slice dengan kapasitas yang tepat + retribusis := make([]models.Retribusi, 0, limit) + + for rows.Next() { + retribusi, err := h.scanRetribusi(rows) + if err != nil { + return nil, fmt.Errorf("scan retribusi failed: %w", err) + } + retribusis = append(retribusis, retribusi) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows iteration error: %w", err) + } + + log.Printf("Successfully fetched %d retribusis with filters applied", len(retribusis)) + return retribusis, nil +} +// Calculate pagination metadata +func (h *RetribusiHandler) calculateMeta(limit, offset, total int) models.MetaResponse { + totalPages := 0 + currentPage := 1 + + if limit > 0 { + totalPages = (total + limit - 1) / limit // Ceiling division + currentPage = (offset / limit) + 1 + } + + return models.MetaResponse{ + Limit: limit, + Offset: offset, + Total: total, + TotalPages: totalPages, + CurrentPage: currentPage, + HasNext: offset+limit < total, + HasPrev: offset > 0, + } +} +// models/retribusi.go - pastikan struct ini memiliki semua field +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"` +} diff --git a/internal/middleware/error_handler.go b/internal/middleware/error_handler.go index 7278def..fd75714 100644 --- a/internal/middleware/error_handler.go +++ b/internal/middleware/error_handler.go @@ -1,10 +1,9 @@ package middleware import ( + models "api-service/internal/models/retribusi" "net/http" - "api-service/internal/models" - "github.com/gin-gonic/gin" ) diff --git a/internal/models/example.go b/internal/models/example.go deleted file mode 100644 index 0ea58a9..0000000 --- a/internal/models/example.go +++ /dev/null @@ -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"` -} diff --git a/internal/models/health.go b/internal/models/health.go deleted file mode 100644 index 5f26e4e..0000000 --- a/internal/models/health.go +++ /dev/null @@ -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"` -} diff --git a/internal/models/product.go b/internal/models/product.go deleted file mode 100644 index 9721084..0000000 --- a/internal/models/product.go +++ /dev/null @@ -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"` -// } diff --git a/internal/models/product/product.go b/internal/models/product/product.go deleted file mode 100644 index 9e0c2c0..0000000 --- a/internal/models/product/product.go +++ /dev/null @@ -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"` -} diff --git a/internal/models/retribusi/retribusi.go b/internal/models/retribusi/retribusi.go new file mode 100644 index 0000000..3693deb --- /dev/null +++ b/internal/models/retribusi/retribusi.go @@ -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 +} diff --git a/internal/repository/product/product_repository.go b/internal/repository/product/product_repository.go deleted file mode 100644 index 755a1e4..0000000 --- a/internal/repository/product/product_repository.go +++ /dev/null @@ -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 -} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index ba4d0ba..4420192 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -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) diff --git a/internal/server/server.go b/internal/server/server.go index cef71ca..98ef90c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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 diff --git a/tools/diagnostic.go b/tools/diagnostic.go new file mode 100644 index 0000000..5c0caa8 --- /dev/null +++ b/tools/diagnostic.go @@ -0,0 +1,130 @@ +package main + +import ( + "database/sql" + "fmt" + "log" + "os" + + _ "github.com/jackc/pgx/v5" + "github.com/joho/godotenv" +) + +func main() { + fmt.Println("=== Database Connection Diagnostic Tool ===") + + // Load environment variables from .env file + if err := godotenv.Load(); err != nil { + log.Printf("Warning: Error loading .env file: %v", err) + } + + // Get configuration from environment + host := os.Getenv("DB_HOST") + port := os.Getenv("DB_PORT") + username := os.Getenv("DB_USERNAME") + password := os.Getenv("DB_PASSWORD") + database := os.Getenv("DB_DATABASE") + sslmode := os.Getenv("DB_SSLMODE") + + if sslmode == "" { + sslmode = "disable" + } + + fmt.Printf("Host: %s\n", host) + fmt.Printf("Port: %s\n", port) + fmt.Printf("Username: %s\n", username) + fmt.Printf("Database: %s\n", database) + fmt.Printf("SSL Mode: %s\n", sslmode) + + if host == "" || username == "" || password == "" { + fmt.Println("❌ Missing required environment variables") + return + } + + // Test connection to PostgreSQL server + fmt.Println("\n--- Testing PostgreSQL Server Connection ---") + serverConnStr := fmt.Sprintf("postgres://%s:%s@%s:%s/postgres?sslmode=%s", + username, password, host, port, sslmode) + + db, err := sql.Open("pgx", serverConnStr) + if err != nil { + fmt.Printf("❌ Failed to connect to PostgreSQL server: %v\n", err) + return + } + defer db.Close() + + err = db.Ping() + if err != nil { + fmt.Printf("❌ Failed to ping PostgreSQL server: %v\n", err) + return + } + + fmt.Println("✅ Successfully connected to PostgreSQL server") + + // Check if database exists + fmt.Println("\n--- Checking Database Existence ---") + var exists bool + err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", database).Scan(&exists) + if err != nil { + fmt.Printf("❌ Failed to check database existence: %v\n", err) + return + } + + if !exists { + fmt.Printf("❌ Database '%s' does not exist\n", database) + + // List available databases + fmt.Println("\n--- Available Databases ---") + rows, err := db.Query("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname") + if err != nil { + fmt.Printf("❌ Failed to list databases: %v\n", err) + return + } + defer rows.Close() + + fmt.Println("Available databases:") + for rows.Next() { + var dbName string + if err := rows.Scan(&dbName); err != nil { + continue + } + fmt.Printf(" - %s\n", dbName) + } + return + } + + fmt.Printf("✅ Database '%s' exists\n", database) + + // Test direct connection to the database + fmt.Println("\n--- Testing Direct Database Connection ---") + directConnStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", + username, password, host, port, database, sslmode) + + targetDB, err := sql.Open("pgx", directConnStr) + if err != nil { + fmt.Printf("❌ Failed to connect to database '%s': %v\n", database, err) + return + } + defer targetDB.Close() + + err = targetDB.Ping() + if err != nil { + fmt.Printf("❌ Failed to ping database '%s': %v\n", database, err) + return + } + + fmt.Printf("✅ Successfully connected to database '%s'\n", database) + + // Test basic query + fmt.Println("\n--- Testing Basic Query ---") + var version string + err = targetDB.QueryRow("SELECT version()").Scan(&version) + if err != nil { + fmt.Printf("❌ Failed to execute query: %v\n", err) + return + } + + fmt.Printf("✅ PostgreSQL Version: %s\n", version) + + fmt.Println("\n🎉 All tests passed! Database connection is working correctly.") +}