From 8dc771bc1605c2fb4e6a88f7f3a664621029db7c Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 22:36:57 +0800 Subject: [PATCH] Revert "fix: resolve linter issues and add national pagination support" --- .github/workflows/deploy.yml | 92 ++++++---- .github/workflows/release-branch-creation.yml | 14 -- LICENSE | 2 +- Makefile | 3 +- RATE_LIMITING.md | 90 ++++++++++ cmd/main.go | 2 +- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- generate-changelog.rb | 2 +- go.mod | 5 +- go.sum | 33 ++++ internal/handler/covid_handler.go | 69 ++------ internal/handler/covid_handler_test.go | 87 +--------- internal/handler/routes.go | 12 +- internal/models/province_case.go | 16 +- internal/models/province_case_response.go | 30 ++-- .../models/province_case_response_test.go | 142 ++++++---------- internal/models/province_case_test.go | 24 +-- .../repository/national_case_repository.go | 93 ---------- .../repository/province_case_repository.go | 159 ++---------------- internal/service/covid_service.go | 28 --- internal/service/covid_service_test.go | 10 -- pkg/utils/query.go | 20 --- test/integration/api_test.go | 59 +------ 25 files changed, 323 insertions(+), 675 deletions(-) create mode 100644 RATE_LIMITING.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 027f7d6..922857d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,6 +39,27 @@ jobs: file ${{ secrets.BINARY_NAME }} echo "Binary size: $(du -h ${{ secrets.BINARY_NAME }} | cut -f1)" + - name: Generate Swagger documentation + run: | + echo "📚 Generating Swagger documentation..." + + # Install swag tool + go install github.com/swaggo/swag/cmd/swag@latest + + # Generate documentation including HTML + swag init -g cmd/main.go -o ./docs --outputTypes go,json,yaml,html + + # Verify generated files + echo "Generated documentation files:" + ls -la docs/ + + # Check if HTML was generated + if [ -f "docs/swagger.html" ]; then + echo "✅ HTML documentation generated successfully" + else + echo "⚠️ HTML documentation not found, continuing without it" + fi + - name: Setup SSH Agent uses: webfactory/ssh-agent@v0.8.0 with: @@ -71,6 +92,22 @@ jobs: echo "📤 Uploading binary..." scp -P ${{ secrets.DEPLOY_PORT }} ${{ secrets.BINARY_NAME }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/${{ secrets.BINARY_NAME }}-${{ env.VERSION }} + # Upload documentation files + echo "📚 Uploading documentation..." + if [ -f "docs/swagger.html" ]; then + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.html ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.html + echo "✅ HTML documentation uploaded" + fi + + if [ -f "docs/swagger.json" ]; then + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.json ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.json + echo "✅ JSON documentation uploaded" + fi + + if [ -f "docs/swagger.yaml" ]; then + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.yaml ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.yaml + echo "✅ YAML documentation uploaded" + fi # Execute deployment script on remote server ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' @@ -105,6 +142,25 @@ jobs: chmod +x "$BINARY_NAME" echo "✅ Binary replaced and made executable" + echo "📚 Updating documentation..." + # Create docs directory if it doesn't exist + mkdir -p docs + + # Move documentation files if they exist + if [ -f "/tmp/swagger-${VERSION}.html" ]; then + mv "/tmp/swagger-${VERSION}.html" "docs/swagger.html" + echo "✅ HTML documentation updated" + fi + + if [ -f "/tmp/swagger-${VERSION}.json" ]; then + mv "/tmp/swagger-${VERSION}.json" "docs/swagger.json" + echo "✅ JSON documentation updated" + fi + + if [ -f "/tmp/swagger-${VERSION}.yaml" ]; then + mv "/tmp/swagger-${VERSION}.yaml" "docs/swagger.yaml" + echo "✅ YAML documentation updated" + fi echo "🚀 Starting application..." if [ -f "./start.sh" ]; then @@ -117,24 +173,6 @@ jobs: echo "🎉 Initial deployment of ${VERSION} completed!" EOF - - name: Deploy API specification - run: | - echo "📚 Deploying API specification for Swagger UI..." - - # Generate latest swagger.json - go install github.com/swaggo/swag/cmd/swag@latest - export PATH=$PATH:$(go env GOPATH)/bin - swag init -g cmd/main.go -o ./docs - - # Upload swagger.json to be served by Go app - if [ -f "docs/swagger.json" ]; then - echo "📤 Uploading swagger.json..." - scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.json ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PATH }}/docs/ - echo "✅ API specification deployed" - else - echo "⚠️ swagger.json not found, skipping upload" - fi - - name: Simple health check id: health_check run: | @@ -145,16 +183,6 @@ jobs: echo "📡 Testing health endpoint..." if curl -f -s "${{ secrets.HEALTH_CHECK_URL }}" > /dev/null; then echo "✅ Health check passed - API is responding" - - # Test Swagger JSON endpoint - SWAGGER_URL=$(echo "${{ secrets.HEALTH_CHECK_URL }}" | sed 's|/health|/swagger.json|') - echo "📚 Testing Swagger JSON endpoint: $SWAGGER_URL" - if curl -f -s "$SWAGGER_URL" > /dev/null; then - echo "✅ Swagger JSON endpoint is working" - else - echo "⚠️ Swagger JSON endpoint not accessible" - fi - echo "health_check_passed=true" >> $GITHUB_OUTPUT else echo "⚠️ Health check failed - API not responding" @@ -203,14 +231,12 @@ jobs: echo "- **Target**: ${{ secrets.DEPLOY_PATH }}" >> $GITHUB_STEP_SUMMARY echo "- **Status**: ✅ Deployed successfully" >> $GITHUB_STEP_SUMMARY echo "- **Health Check**: ✅ Passed" >> $GITHUB_STEP_SUMMARY - echo "- **Documentation**: 📚 Static Swagger UI + API Spec" >> $GITHUB_STEP_SUMMARY - echo "- **Binary Size**: 🎯 Optimized (64% smaller)" >> $GITHUB_STEP_SUMMARY + echo "- **Documentation**: 📚 Updated" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Completed Actions" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Binary deployed and started (optimized size)" >> $GITHUB_STEP_SUMMARY - echo "- ✅ API specification (swagger.json) deployed" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Binary deployed and started" >> $GITHUB_STEP_SUMMARY + echo "- ✅ Documentation updated (HTML, JSON, YAML)" >> $GITHUB_STEP_SUMMARY echo "- ✅ Health check verification passed" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Swagger JSON endpoint verified" >> $GITHUB_STEP_SUMMARY echo "- ✅ Old backups cleaned up" >> $GITHUB_STEP_SUMMARY else echo "## ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index a71526e..f8f5eb4 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -278,13 +278,6 @@ jobs: echo "✅ Updated internal/handler/covid_handler.go" fi - # Update test files - if [ -f "internal/handler/covid_handler_test.go" ]; then - sed -i.bak "s/assert.Equal(t, \"[^\"]*\", apiInfo\[\"version\"\])/assert.Equal(t, \"$CLEAN_VERSION\", apiInfo[\"version\"])/" internal/handler/covid_handler_test.go && rm -f internal/handler/covid_handler_test.go.bak - sed -i.bak "s/assert.Equal(t, \"[^\"]*\", data\[\"version\"\])/assert.Equal(t, \"$CLEAN_VERSION\", data[\"version\"])/" internal/handler/covid_handler_test.go && rm -f internal/handler/covid_handler_test.go.bak - echo "✅ Updated internal/handler/covid_handler_test.go" - fi - - name: Install and regenerate documentation run: | echo "📚 Regenerating API documentation..." @@ -560,13 +553,6 @@ jobs: echo "✅ Updated internal/handler/covid_handler.go to $CLEAN_VERSION" fi - # Update test files - if [ -f "internal/handler/covid_handler_test.go" ]; then - sed -i.bak "s/assert.Equal(t, \"[^\"]*\", apiInfo\[\"version\"\])/assert.Equal(t, \"$CLEAN_VERSION\", apiInfo[\"version\"])/" internal/handler/covid_handler_test.go && rm -f internal/handler/covid_handler_test.go.bak - sed -i.bak "s/assert.Equal(t, \"[^\"]*\", data\[\"version\"\])/assert.Equal(t, \"$CLEAN_VERSION\", data[\"version\"])/" internal/handler/covid_handler_test.go && rm -f internal/handler/covid_handler_test.go.bak - echo "✅ Updated internal/handler/covid_handler_test.go to $CLEAN_VERSION" - fi - # Install swag and regenerate docs go install github.com/swaggo/swag/cmd/swag@latest export PATH=$PATH:$(go env GOPATH)/bin diff --git a/LICENSE b/LICENSE index 59ad580..a86baf9 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index a414e30..0863305 100644 --- a/Makefile +++ b/Makefile @@ -81,5 +81,4 @@ help: @echo " dev - Run development server with hot reload" @echo " bench - Run benchmarks" @echo " security - Check for vulnerabilities" - @echo " help - Show this help message" - \ No newline at end of file + @echo " help - Show this help message" \ No newline at end of file diff --git a/RATE_LIMITING.md b/RATE_LIMITING.md new file mode 100644 index 0000000..c185cd6 --- /dev/null +++ b/RATE_LIMITING.md @@ -0,0 +1,90 @@ +# Rate Limiting + +This API implements rate limiting to ensure fair usage and protect against abuse. The rate limiter uses a sliding window algorithm to track requests per client IP address. + +## Configuration + +Rate limiting can be configured using environment variables: + +| Environment Variable | Default | Description | +|---------------------|---------|-------------| +| `RATE_LIMIT_ENABLED` | `true` | Enable or disable rate limiting | +| `RATE_LIMIT_REQUESTS_PER_MINUTE` | `100` | Maximum requests per minute per IP | +| `RATE_LIMIT_BURST_SIZE` | `20` | Burst size for initial requests | +| `RATE_LIMIT_WINDOW_SIZE` | `1m` | Time window for rate limiting | + +## Response Headers + +All API responses include the following rate limiting headers: + +- `X-RateLimit-Limit`: The maximum number of requests allowed in the current window +- `X-RateLimit-Remaining`: The number of requests remaining in the current window +- `X-RateLimit-Reset`: Unix timestamp when the rate limit window resets (only on 429 responses) +- `Retry-After`: Number of seconds to wait before making another request (only on 429 responses) + +## Rate Limit Exceeded + +When the rate limit is exceeded, the API returns: + +- **Status Code**: `429 Too Many Requests` +- **Response Body**: + ```json + { + "status": "error", + "error": "Rate limit exceeded. Too many requests." + } + ``` + +## Client IP Detection + +The rate limiter identifies clients by IP address using the following priority: + +1. `X-Forwarded-For` header (for load balancers/proxies) +2. `X-Real-IP` header (for reverse proxies) +3. `RemoteAddr` from the connection (fallback) + +## Implementation Details + +- **Algorithm**: Sliding window rate limiter +- **Storage**: In-memory (per instance) +- **Cleanup**: Automatic cleanup of old client records every 5 minutes +- **Thread Safety**: Fully concurrent with proper mutex locking + +## Best Practices for Clients + +1. **Check Headers**: Always check the `X-RateLimit-*` headers to understand your current quota +2. **Handle 429 Responses**: Implement exponential backoff when receiving 429 responses +3. **Use Retry-After**: Respect the `Retry-After` header value before retrying +4. **Distribute Requests**: Avoid bursting all requests at once; distribute them evenly + +## Example Usage + +```bash +# Check current rate limit status +curl -I https://api.example.com/api/v1/national + +# Response headers will include: +# X-RateLimit-Limit: 100 +# X-RateLimit-Remaining: 99 + +# When rate limited: +# HTTP/1.1 429 Too Many Requests +# X-RateLimit-Limit: 100 +# X-RateLimit-Remaining: 0 +# X-RateLimit-Reset: 1672531200 +# Retry-After: 60 +``` + +## Disabling Rate Limiting + +To disable rate limiting (not recommended for production): + +```bash +export RATE_LIMIT_ENABLED=false +``` + +Or set it in your `.env` file: + +``` +RATE_LIMIT_ENABLED=false +``` \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 9fb8710..d329507 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,7 +1,7 @@ // Package main provides the entry point for the Sulawesi Tengah COVID-19 Data API // // @title Sulawesi Tengah COVID-19 Data API -// @version 2.3.0 +// @version 2.2.0 // @description A comprehensive REST API for COVID-19 data in Sulawesi Tengah (Central Sulawesi), with additional national and provincial data for context. Features enhanced ODP/PDP grouping, hybrid pagination, and rate limiting protection. Rate limiting: 100 requests per minute per IP address by default, with appropriate HTTP headers for client guidance. // @termsOfService http://swagger.io/terms/ // diff --git a/docs/docs.go b/docs/docs.go index 4fb9523..abbcd00 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -815,7 +815,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "2.3.0", + Version: "2.2.0", Host: "pico-api.banuacoder.com", BasePath: "/api/v1", Schemes: []string{"https", "http"}, diff --git a/docs/swagger.json b/docs/swagger.json index 66a57ba..7e8f02a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -17,7 +17,7 @@ "name": "MIT", "url": "https://opensource.org/licenses/MIT" }, - "version": "2.3.0" + "version": "2.2.0" }, "host": "pico-api.banuacoder.com", "basePath": "/api/v1", diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b9c819d..8a8fe1f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -209,7 +209,7 @@ info: url: https://opensource.org/licenses/MIT termsOfService: http://swagger.io/terms/ title: Sulawesi Tengah COVID-19 Data API - version: 2.3.0 + version: 2.2.0 paths: /: get: diff --git a/generate-changelog.rb b/generate-changelog.rb index 85bd29b..c649852 100755 --- a/generate-changelog.rb +++ b/generate-changelog.rb @@ -752,4 +752,4 @@ def self.run(args = ARGV) # Run the CLI if this file is executed directly if __FILE__ == $0 CLI.run -end +end \ No newline at end of file diff --git a/go.mod b/go.mod index a49bad7..4022130 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.11.1 - github.com/swaggo/swag v1.16.6 + github.com/swaggo/http-swagger v1.3.4 ) require ( @@ -35,7 +35,10 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/tools v0.36.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 356611c..83d2836 100644 --- a/go.sum +++ b/go.sum @@ -61,14 +61,47 @@ 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +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-20210615035016-665e8c7367d1/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +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.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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= diff --git a/internal/handler/covid_handler.go b/internal/handler/covid_handler.go index f1de751..b17dbac 100644 --- a/internal/handler/covid_handler.go +++ b/internal/handler/covid_handler.go @@ -26,19 +26,14 @@ func NewCovidHandler(covidService service.CovidService, db *database.DB) *CovidH // GetNationalCases godoc // // @Summary Get national COVID-19 cases -// @Description Retrieve national COVID-19 cases data with optional date range filtering, sorting, and pagination +// @Description Retrieve national COVID-19 cases data with optional date range filtering and sorting // @Tags national // @Accept json // @Produce json -// @Param limit query integer false "Records per page (default: 50, max: 1000)" -// @Param offset query integer false "Records to skip (default: 0)" -// @Param page query integer false "Page number (1-based, alternative to offset)" -// @Param all query boolean false "Return all data without pagination" // @Param start_date query string false "Start date (YYYY-MM-DD)" // @Param end_date query string false "End date (YYYY-MM-DD)" // @Param sort query string false "Sort by field:order (e.g., date:desc, positive:asc). Default: date:asc" -// @Success 200 {object} Response{data=models.PaginatedResponse{data=[]models.NationalCaseResponse}} "Paginated response" -// @Success 200 {object} Response{data=[]models.NationalCaseResponse} "All data response when all=true" +// @Success 200 {object} Response{data=[]models.NationalCaseResponse} // @Failure 400 {object} Response // @Failure 429 {object} Response "Rate limit exceeded" // @Failure 500 {object} Response @@ -50,66 +45,31 @@ func NewCovidHandler(covidService service.CovidService, db *database.DB) *CovidH func (h *CovidHandler) GetNationalCases(w http.ResponseWriter, r *http.Request) { startDate := r.URL.Query().Get("start_date") endDate := r.URL.Query().Get("end_date") - all := utils.ParseBoolQueryParam(r, "all") // Parse sort parameters (default: date ascending) sortParams := utils.ParseSortParam(r, "date") - // Handle pagination parameters - limit, offset := utils.ParsePaginationParams(r) - - // Return all data without pagination if "all" is true - if all { - if startDate != "" && endDate != "" { - cases, err := h.covidService.GetNationalCasesByDateRangeSorted(startDate, endDate, sortParams) - if err != nil { - writeErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - responseData := models.TransformSliceToResponse(cases) - writeSuccessResponse(w, responseData) - return - } - - cases, err := h.covidService.GetNationalCasesSorted(sortParams) - if err != nil { - writeErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - responseData := models.TransformSliceToResponse(cases) - writeSuccessResponse(w, responseData) - return - } - - // Return paginated data if startDate != "" && endDate != "" { - cases, total, err := h.covidService.GetNationalCasesByDateRangePaginatedSorted(startDate, endDate, limit, offset, sortParams) + cases, err := h.covidService.GetNationalCasesByDateRangeSorted(startDate, endDate, sortParams) if err != nil { writeErrorResponse(w, http.StatusInternalServerError, err.Error()) return } + // Transform to new response structure responseData := models.TransformSliceToResponse(cases) - pagination := models.CalculatePaginationMeta(limit, offset, total) - paginatedResponse := models.PaginatedResponse{ - Data: responseData, - Pagination: pagination, - } - writeSuccessResponse(w, paginatedResponse) + writeSuccessResponse(w, responseData) return } - cases, total, err := h.covidService.GetNationalCasesPaginatedSorted(limit, offset, sortParams) + cases, err := h.covidService.GetNationalCasesSorted(sortParams) if err != nil { writeErrorResponse(w, http.StatusInternalServerError, err.Error()) return } + + // Transform to new response structure responseData := models.TransformSliceToResponse(cases) - pagination := models.CalculatePaginationMeta(limit, offset, total) - paginatedResponse := models.PaginatedResponse{ - Data: responseData, - Pagination: pagination, - } - writeSuccessResponse(w, paginatedResponse) + writeSuccessResponse(w, responseData) } // GetLatestNationalCase godoc @@ -185,7 +145,6 @@ func (h *CovidHandler) GetProvinces(w http.ResponseWriter, r *http.Request) { // @Param provinceId path string false "Province ID (e.g., '31' for Jakarta)" // @Param limit query integer false "Records per page (default: 50, max: 1000)" // @Param offset query integer false "Records to skip (default: 0)" -// @Param page query integer false "Page number (1-based, alternative to offset)" // @Param all query boolean false "Return all data without pagination" // @Param start_date query string false "Start date (YYYY-MM-DD)" // @Param end_date query string false "End date (YYYY-MM-DD)" @@ -201,7 +160,8 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) provinceID := vars["provinceId"] // Parse query parameters - limit, offset := utils.ParsePaginationParams(r) + limit := utils.ParseIntQueryParam(r, "limit", 50) + offset := utils.ParseIntQueryParam(r, "offset", 0) all := utils.ParseBoolQueryParam(r, "all") startDate := r.URL.Query().Get("start_date") endDate := r.URL.Query().Get("end_date") @@ -209,6 +169,9 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) // Parse sort parameters (default: date ascending) sortParams := utils.ParseSortParam(r, "date") + // Validate pagination params + limit, offset = utils.ValidatePaginationParams(limit, offset) + if provinceID == "" { // Handle all provinces cases if all { @@ -335,7 +298,7 @@ func (h *CovidHandler) HealthCheck(w http.ResponseWriter, r *http.Request) { health := map[string]interface{}{ "status": "healthy", "service": "COVID-19 API", - "version": "2.3.0", + "version": "2.2.0", "timestamp": time.Now().UTC().Format(time.RFC3339), } @@ -392,7 +355,7 @@ func (h *CovidHandler) GetAPIIndex(w http.ResponseWriter, r *http.Request) { endpoints := map[string]interface{}{ "api": map[string]interface{}{ "title": "Sulawesi Tengah COVID-19 Data API", - "version": "2.3.0", + "version": "2.2.0", "description": "A comprehensive REST API for COVID-19 data in Sulawesi Tengah (Central Sulawesi)", }, "documentation": map[string]interface{}{ diff --git a/internal/handler/covid_handler_test.go b/internal/handler/covid_handler_test.go index f2161a6..ef40e00 100644 --- a/internal/handler/covid_handler_test.go +++ b/internal/handler/covid_handler_test.go @@ -140,16 +140,6 @@ func (m *MockCovidService) GetAllProvinceCasesByDateRangePaginatedSorted(startDa return args.Get(0).([]models.ProvinceCaseWithDate), args.Int(1), args.Error(2) } -func (m *MockCovidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - -func (m *MockCovidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(startDate, endDate, limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - func TestCovidHandler_GetNationalCases(t *testing.T) { mockService := new(MockCovidService) handler := NewCovidHandler(mockService, nil) @@ -160,7 +150,7 @@ func TestCovidHandler_GetNationalCases(t *testing.T) { mockService.On("GetNationalCasesSorted", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - req, err := http.NewRequest("GET", "/api/v1/national?all=true", nil) + req, err := http.NewRequest("GET", "/api/v1/national", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -187,7 +177,7 @@ func TestCovidHandler_GetNationalCases_WithDateRange(t *testing.T) { mockService.On("GetNationalCasesByDateRangeSorted", "2020-03-01", "2020-03-31", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&all=true", nil) + req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -209,7 +199,7 @@ func TestCovidHandler_GetNationalCases_ServiceError(t *testing.T) { mockService.On("GetNationalCasesSorted", utils.SortParams{Field: "date", Order: "asc"}).Return([]models.NationalCase{}, errors.New("database error")) - req, err := http.NewRequest("GET", "/api/v1/national?all=true", nil) + req, err := http.NewRequest("GET", "/api/v1/national", nil) assert.NoError(t, err) rr := httptest.NewRecorder() @@ -226,73 +216,6 @@ func TestCovidHandler_GetNationalCases_ServiceError(t *testing.T) { mockService.AssertExpectations(t) } -func TestCovidHandler_GetNationalCases_Paginated(t *testing.T) { - mockService := new(MockCovidService) - handler := NewCovidHandler(mockService, nil) - - expectedCases := []models.NationalCase{ - {ID: 1, Positive: 100, Recovered: 80, Deceased: 5}, - {ID: 2, Positive: 110, Recovered: 85, Deceased: 6}, - } - - mockService.On("GetNationalCasesPaginatedSorted", 10, 0, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 2, nil) - - req, err := http.NewRequest("GET", "/api/v1/national?limit=10", nil) - assert.NoError(t, err) - - rr := httptest.NewRecorder() - handler.GetNationalCases(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response Response - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Equal(t, "success", response.Status) - assert.NotNil(t, response.Data) - - // Check that it's a paginated response - paginatedData, ok := response.Data.(map[string]interface{}) - assert.True(t, ok) - assert.Contains(t, paginatedData, "data") - assert.Contains(t, paginatedData, "pagination") - - mockService.AssertExpectations(t) -} - -func TestCovidHandler_GetNationalCases_WithDateRangePaginated(t *testing.T) { - mockService := new(MockCovidService) - handler := NewCovidHandler(mockService, nil) - - expectedCases := []models.NationalCase{ - {ID: 1, Positive: 100, Date: time.Date(2020, 3, 15, 0, 0, 0, 0, time.UTC)}, - } - - mockService.On("GetNationalCasesByDateRangePaginatedSorted", "2020-03-01", "2020-03-31", 20, 10, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 1, nil) - - req, err := http.NewRequest("GET", "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&limit=20&offset=10", nil) - assert.NoError(t, err) - - rr := httptest.NewRecorder() - handler.GetNationalCases(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response Response - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Equal(t, "success", response.Status) - assert.NotNil(t, response.Data) - - // Check that it's a paginated response - paginatedData, ok := response.Data.(map[string]interface{}) - assert.True(t, ok) - assert.Contains(t, paginatedData, "data") - assert.Contains(t, paginatedData, "pagination") - - mockService.AssertExpectations(t) -} - func TestCovidHandler_GetLatestNationalCase(t *testing.T) { mockService := new(MockCovidService) handler := NewCovidHandler(mockService, nil) @@ -684,7 +607,7 @@ func TestCovidHandler_GetAPIIndex(t *testing.T) { apiInfo, ok := data["api"].(map[string]interface{}) assert.True(t, ok) assert.Equal(t, "Sulawesi Tengah COVID-19 Data API", apiInfo["title"]) - assert.Equal(t, "2.3.0", apiInfo["version"]) + assert.Equal(t, "2.2.0", apiInfo["version"]) // Verify endpoints structure endpoints, ok := data["endpoints"].(map[string]interface{}) @@ -715,7 +638,7 @@ func TestCovidHandler_HealthCheck(t *testing.T) { assert.True(t, ok) assert.Equal(t, "degraded", data["status"]) assert.Equal(t, "COVID-19 API", data["service"]) - assert.Equal(t, "2.3.0", data["version"]) + assert.Equal(t, "2.2.0", data["version"]) assert.Contains(t, data, "database") dbData, ok := data["database"].(map[string]interface{}) diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 18bc1c3..a125320 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -6,6 +6,7 @@ import ( "github.com/banua-coder/pico-api-go/internal/service" "github.com/banua-coder/pico-api-go/pkg/database" "github.com/gorilla/mux" + httpSwagger "github.com/swaggo/http-swagger" ) func SetupRoutes(covidService service.CovidService, db *database.DB) *mux.Router { @@ -27,14 +28,11 @@ func SetupRoutes(covidService service.CovidService, db *database.DB) *mux.Router api.HandleFunc("/provinces/cases", covidHandler.GetProvinceCases).Methods("GET", "OPTIONS") api.HandleFunc("/provinces/{provinceId}/cases", covidHandler.GetProvinceCases).Methods("GET", "OPTIONS") - // API specification endpoint - api.HandleFunc("/swagger.json", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - http.ServeFile(w, r, "./docs/swagger.json") - }).Methods("GET", "OPTIONS") + // Swagger documentation + router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler).Methods("GET") - // Redirect /docs to static swagger UI - router.HandleFunc("/docs", func(w http.ResponseWriter, r *http.Request) { + // Redirect root to swagger docs for convenience + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/swagger/index.html", http.StatusFound) }).Methods("GET") diff --git a/internal/models/province_case.go b/internal/models/province_case.go index 4c59f04..7815fd4 100644 --- a/internal/models/province_case.go +++ b/internal/models/province_case.go @@ -9,17 +9,17 @@ type ProvinceCase struct { Positive int64 `json:"positive" db:"positive"` Recovered int64 `json:"recovered" db:"recovered"` Deceased int64 `json:"deceased" db:"deceased"` - PersonUnderObservation *int64 `json:"person_under_observation" db:"person_under_observation"` - FinishedPersonUnderObservation *int64 `json:"finished_person_under_observation" db:"finished_person_under_observation"` - PersonUnderSupervision *int64 `json:"person_under_supervision" db:"person_under_supervision"` - FinishedPersonUnderSupervision *int64 `json:"finished_person_under_supervision" db:"finished_person_under_supervision"` + PersonUnderObservation int64 `json:"person_under_observation" db:"person_under_observation"` + FinishedPersonUnderObservation int64 `json:"finished_person_under_observation" db:"finished_person_under_observation"` + PersonUnderSupervision int64 `json:"person_under_supervision" db:"person_under_supervision"` + FinishedPersonUnderSupervision int64 `json:"finished_person_under_supervision" db:"finished_person_under_supervision"` CumulativePositive int64 `json:"cumulative_positive" db:"cumulative_positive"` CumulativeRecovered int64 `json:"cumulative_recovered" db:"cumulative_recovered"` CumulativeDeceased int64 `json:"cumulative_deceased" db:"cumulative_deceased"` - CumulativePersonUnderObservation *int64 `json:"cumulative_person_under_observation" db:"cumulative_person_under_observation"` - CumulativeFinishedPersonUnderObservation *int64 `json:"cumulative_finished_person_under_observation" db:"cumulative_finished_person_under_observation"` - CumulativePersonUnderSupervision *int64 `json:"cumulative_person_under_supervision" db:"cumulative_person_under_supervision"` - CumulativeFinishedPersonUnderSupervision *int64 `json:"cumulative_finished_person_under_supervision" db:"cumulative_finished_person_under_supervision"` + CumulativePersonUnderObservation int64 `json:"cumulative_person_under_observation" db:"cumulative_person_under_observation"` + CumulativeFinishedPersonUnderObservation int64 `json:"cumulative_finished_person_under_observation" db:"cumulative_finished_person_under_observation"` + CumulativePersonUnderSupervision int64 `json:"cumulative_person_under_supervision" db:"cumulative_person_under_supervision"` + CumulativeFinishedPersonUnderSupervision int64 `json:"cumulative_finished_person_under_supervision" db:"cumulative_finished_person_under_supervision"` Rt *float64 `json:"rt" db:"rt"` RtUpper *float64 `json:"rt_upper" db:"rt_upper"` RtLower *float64 `json:"rt_lower" db:"rt_lower"` diff --git a/internal/models/province_case_response.go b/internal/models/province_case_response.go index da3ad08..2cfe08c 100644 --- a/internal/models/province_case_response.go +++ b/internal/models/province_case_response.go @@ -70,17 +70,9 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse dailyActive := pc.Positive - pc.Recovered - pc.Deceased cumulativeActive := pc.CumulativePositive - pc.CumulativeRecovered - pc.CumulativeDeceased - // Helper function to safely get int64 value from pointer - safeInt64 := func(ptr *int64) int64 { - if ptr == nil { - return 0 - } - return *ptr - } - - // Calculate active under observation and supervision (with null safety) - activePersonUnderObservation := safeInt64(pc.CumulativePersonUnderObservation) - safeInt64(pc.CumulativeFinishedPersonUnderObservation) - activePersonUnderSupervision := safeInt64(pc.CumulativePersonUnderSupervision) - safeInt64(pc.CumulativeFinishedPersonUnderSupervision) + // Calculate active under observation and supervision + activePersonUnderObservation := pc.CumulativePersonUnderObservation - pc.CumulativeFinishedPersonUnderObservation + activePersonUnderSupervision := pc.CumulativePersonUnderSupervision - pc.CumulativeFinishedPersonUnderSupervision // Build response response := ProvinceCaseResponse{ @@ -92,12 +84,12 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse Deceased: pc.Deceased, Active: dailyActive, ODP: DailyObservationData{ - Active: safeInt64(pc.PersonUnderObservation) - safeInt64(pc.FinishedPersonUnderObservation), - Finished: safeInt64(pc.FinishedPersonUnderObservation), + Active: pc.PersonUnderObservation - pc.FinishedPersonUnderObservation, + Finished: pc.FinishedPersonUnderObservation, }, PDP: DailySupervisionData{ - Active: safeInt64(pc.PersonUnderSupervision) - safeInt64(pc.FinishedPersonUnderSupervision), - Finished: safeInt64(pc.FinishedPersonUnderSupervision), + Active: pc.PersonUnderSupervision - pc.FinishedPersonUnderSupervision, + Finished: pc.FinishedPersonUnderSupervision, }, }, Cumulative: ProvinceCumulativeCases{ @@ -107,13 +99,13 @@ func (pc *ProvinceCase) TransformToResponse(date time.Time) ProvinceCaseResponse Active: cumulativeActive, ODP: ObservationData{ Active: activePersonUnderObservation, - Finished: safeInt64(pc.CumulativeFinishedPersonUnderObservation), - Total: safeInt64(pc.CumulativePersonUnderObservation), + Finished: pc.CumulativeFinishedPersonUnderObservation, + Total: pc.CumulativePersonUnderObservation, }, PDP: SupervisionData{ Active: activePersonUnderSupervision, - Finished: safeInt64(pc.CumulativeFinishedPersonUnderSupervision), - Total: safeInt64(pc.CumulativePersonUnderSupervision), + Finished: pc.CumulativeFinishedPersonUnderSupervision, + Total: pc.CumulativePersonUnderSupervision, }, }, Statistics: ProvinceCaseStatistics{ diff --git a/internal/models/province_case_response_test.go b/internal/models/province_case_response_test.go index 5cd00a8..88c832a 100644 --- a/internal/models/province_case_response_test.go +++ b/internal/models/province_case_response_test.go @@ -13,36 +13,6 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { rtUpper := 1.8 rtLower := 1.2 - // Helper variables for nullable int64 fields (test case 1) - personUnderObservation1 := int64(25) - finishedPersonUnderObservation1 := int64(20) - personUnderSupervision1 := int64(30) - finishedPersonUnderSupervision1 := int64(25) - cumulativePersonUnderObservation1 := int64(800) - cumulativeFinishedPersonUnderObservation1 := int64(750) - cumulativePersonUnderSupervision1 := int64(600) - cumulativeFinishedPersonUnderSupervision1 := int64(580) - - // Helper variables for nullable int64 fields (test case 2) - personUnderObservation2 := int64(15) - finishedPersonUnderObservation2 := int64(10) - personUnderSupervision2 := int64(20) - finishedPersonUnderSupervision2 := int64(15) - cumulativePersonUnderObservation2 := int64(400) - cumulativeFinishedPersonUnderObservation2 := int64(350) - cumulativePersonUnderSupervision2 := int64(300) - cumulativeFinishedPersonUnderSupervision2 := int64(290) - - // Helper variables for nullable int64 fields (test case 3 - zeros) - personUnderObservation3 := int64(0) - finishedPersonUnderObservation3 := int64(0) - personUnderSupervision3 := int64(0) - finishedPersonUnderSupervision3 := int64(0) - cumulativePersonUnderObservation3 := int64(0) - cumulativeFinishedPersonUnderObservation3 := int64(0) - cumulativePersonUnderSupervision3 := int64(0) - cumulativeFinishedPersonUnderSupervision3 := int64(0) - tests := []struct { name string provinceCase ProvinceCase @@ -58,17 +28,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 150, Recovered: 120, Deceased: 10, - PersonUnderObservation: &personUnderObservation1, - FinishedPersonUnderObservation: &finishedPersonUnderObservation1, - PersonUnderSupervision: &personUnderSupervision1, - FinishedPersonUnderSupervision: &finishedPersonUnderSupervision1, + PersonUnderObservation: 25, + FinishedPersonUnderObservation: 20, + PersonUnderSupervision: 30, + FinishedPersonUnderSupervision: 25, CumulativePositive: 5000, CumulativeRecovered: 4500, CumulativeDeceased: 300, - CumulativePersonUnderObservation: &cumulativePersonUnderObservation1, - CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation1, - CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision1, - CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision1, + CumulativePersonUnderObservation: 800, + CumulativeFinishedPersonUnderObservation: 750, + CumulativePersonUnderSupervision: 600, + CumulativeFinishedPersonUnderSupervision: 580, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -138,17 +108,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 100, Recovered: 80, Deceased: 5, - PersonUnderObservation: &personUnderObservation2, - FinishedPersonUnderObservation: &finishedPersonUnderObservation2, - PersonUnderSupervision: &personUnderSupervision2, - FinishedPersonUnderSupervision: &finishedPersonUnderSupervision2, + PersonUnderObservation: 15, + FinishedPersonUnderObservation: 10, + PersonUnderSupervision: 20, + FinishedPersonUnderSupervision: 15, CumulativePositive: 2000, CumulativeRecovered: 1800, CumulativeDeceased: 100, - CumulativePersonUnderObservation: &cumulativePersonUnderObservation2, - CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation2, - CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision2, - CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision2, + CumulativePersonUnderObservation: 400, + CumulativeFinishedPersonUnderObservation: 350, + CumulativePersonUnderSupervision: 300, + CumulativeFinishedPersonUnderSupervision: 290, Rt: nil, RtUpper: nil, RtLower: nil, @@ -218,17 +188,17 @@ func TestProvinceCase_TransformToResponse(t *testing.T) { Positive: 0, Recovered: 0, Deceased: 0, - PersonUnderObservation: &personUnderObservation3, - FinishedPersonUnderObservation: &finishedPersonUnderObservation3, - PersonUnderSupervision: &personUnderSupervision3, - FinishedPersonUnderSupervision: &finishedPersonUnderSupervision3, + PersonUnderObservation: 0, + FinishedPersonUnderObservation: 0, + PersonUnderSupervision: 0, + FinishedPersonUnderSupervision: 0, CumulativePositive: 0, CumulativeRecovered: 0, CumulativeDeceased: 0, - CumulativePersonUnderObservation: &cumulativePersonUnderObservation3, - CumulativeFinishedPersonUnderObservation: &cumulativeFinishedPersonUnderObservation3, - CumulativePersonUnderSupervision: &cumulativePersonUnderSupervision3, - CumulativeFinishedPersonUnderSupervision: &cumulativeFinishedPersonUnderSupervision3, + CumulativePersonUnderObservation: 0, + CumulativeFinishedPersonUnderObservation: 0, + CumulativePersonUnderSupervision: 0, + CumulativeFinishedPersonUnderSupervision: 0, Rt: nil, RtUpper: nil, RtLower: nil, @@ -313,17 +283,17 @@ func TestProvinceCaseWithDate_TransformToResponse(t *testing.T) { Positive: 50, Recovered: 40, Deceased: 2, - PersonUnderObservation: &[]int64{10}[0], - FinishedPersonUnderObservation: &[]int64{8}[0], - PersonUnderSupervision: &[]int64{12}[0], - FinishedPersonUnderSupervision: &[]int64{10}[0], + PersonUnderObservation: 10, + FinishedPersonUnderObservation: 8, + PersonUnderSupervision: 12, + FinishedPersonUnderSupervision: 10, CumulativePositive: 3000, CumulativeRecovered: 2700, CumulativeDeceased: 200, - CumulativePersonUnderObservation: &[]int64{500}[0], - CumulativeFinishedPersonUnderObservation: &[]int64{450}[0], - CumulativePersonUnderSupervision: &[]int64{350}[0], - CumulativeFinishedPersonUnderSupervision: &[]int64{320}[0], + CumulativePersonUnderObservation: 500, + CumulativeFinishedPersonUnderObservation: 450, + CumulativePersonUnderSupervision: 350, + CumulativeFinishedPersonUnderSupervision: 320, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -407,17 +377,17 @@ func TestTransformProvinceCaseSliceToResponse(t *testing.T) { Positive: 100, Recovered: 80, Deceased: 5, - PersonUnderObservation: &[]int64{20}[0], - FinishedPersonUnderObservation: &[]int64{15}[0], - PersonUnderSupervision: &[]int64{25}[0], - FinishedPersonUnderSupervision: &[]int64{20}[0], + PersonUnderObservation: 20, + FinishedPersonUnderObservation: 15, + PersonUnderSupervision: 25, + FinishedPersonUnderSupervision: 20, CumulativePositive: 1000, CumulativeRecovered: 800, CumulativeDeceased: 50, - CumulativePersonUnderObservation: &[]int64{200}[0], - CumulativeFinishedPersonUnderObservation: &[]int64{180}[0], - CumulativePersonUnderSupervision: &[]int64{250}[0], - CumulativeFinishedPersonUnderSupervision: &[]int64{230}[0], + CumulativePersonUnderObservation: 200, + CumulativeFinishedPersonUnderObservation: 180, + CumulativePersonUnderSupervision: 250, + CumulativeFinishedPersonUnderSupervision: 230, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -436,17 +406,17 @@ func TestTransformProvinceCaseSliceToResponse(t *testing.T) { Positive: 50, Recovered: 45, Deceased: 2, - PersonUnderObservation: &[]int64{10}[0], - FinishedPersonUnderObservation: &[]int64{8}[0], - PersonUnderSupervision: &[]int64{12}[0], - FinishedPersonUnderSupervision: &[]int64{10}[0], + PersonUnderObservation: 10, + FinishedPersonUnderObservation: 8, + PersonUnderSupervision: 12, + FinishedPersonUnderSupervision: 10, CumulativePositive: 1050, CumulativeRecovered: 845, CumulativeDeceased: 52, - CumulativePersonUnderObservation: &[]int64{210}[0], - CumulativeFinishedPersonUnderObservation: &[]int64{188}[0], - CumulativePersonUnderSupervision: &[]int64{262}[0], - CumulativeFinishedPersonUnderSupervision: &[]int64{240}[0], + CumulativePersonUnderObservation: 210, + CumulativeFinishedPersonUnderObservation: 188, + CumulativePersonUnderSupervision: 262, + CumulativeFinishedPersonUnderSupervision: 240, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -498,17 +468,17 @@ func TestProvinceCaseResponse_JSONStructure(t *testing.T) { Positive: 150, Recovered: 120, Deceased: 10, - PersonUnderObservation: &[]int64{25}[0], - FinishedPersonUnderObservation: &[]int64{20}[0], - PersonUnderSupervision: &[]int64{30}[0], - FinishedPersonUnderSupervision: &[]int64{25}[0], + PersonUnderObservation: 25, + FinishedPersonUnderObservation: 20, + PersonUnderSupervision: 30, + FinishedPersonUnderSupervision: 25, CumulativePositive: 5000, CumulativeRecovered: 4500, CumulativeDeceased: 300, - CumulativePersonUnderObservation: &[]int64{800}[0], - CumulativeFinishedPersonUnderObservation: &[]int64{750}[0], - CumulativePersonUnderSupervision: &[]int64{600}[0], - CumulativeFinishedPersonUnderSupervision: &[]int64{580}[0], + CumulativePersonUnderObservation: 800, + CumulativeFinishedPersonUnderObservation: 750, + CumulativePersonUnderSupervision: 600, + CumulativeFinishedPersonUnderSupervision: 580, Rt: &rt, RtUpper: &rt, RtLower: &rt, diff --git a/internal/models/province_case_test.go b/internal/models/province_case_test.go index a5b477b..b8071af 100644 --- a/internal/models/province_case_test.go +++ b/internal/models/province_case_test.go @@ -19,17 +19,17 @@ func TestProvinceCase_Structure(t *testing.T) { Positive: 50, Recovered: 40, Deceased: 2, - PersonUnderObservation: &[]int64{10}[0], - FinishedPersonUnderObservation: &[]int64{8}[0], - PersonUnderSupervision: &[]int64{5}[0], - FinishedPersonUnderSupervision: &[]int64{3}[0], + PersonUnderObservation: 10, + FinishedPersonUnderObservation: 8, + PersonUnderSupervision: 5, + FinishedPersonUnderSupervision: 3, CumulativePositive: 500, CumulativeRecovered: 400, CumulativeDeceased: 20, - CumulativePersonUnderObservation: &[]int64{100}[0], - CumulativeFinishedPersonUnderObservation: &[]int64{80}[0], - CumulativePersonUnderSupervision: &[]int64{50}[0], - CumulativeFinishedPersonUnderSupervision: &[]int64{30}[0], + CumulativePersonUnderObservation: 100, + CumulativeFinishedPersonUnderObservation: 80, + CumulativePersonUnderSupervision: 50, + CumulativeFinishedPersonUnderSupervision: 30, Rt: &rt, RtUpper: &rtUpper, RtLower: &rtLower, @@ -42,10 +42,10 @@ func TestProvinceCase_Structure(t *testing.T) { assert.Equal(t, int64(50), provinceCase.Positive) assert.Equal(t, int64(40), provinceCase.Recovered) assert.Equal(t, int64(2), provinceCase.Deceased) - assert.Equal(t, int64(10), *provinceCase.PersonUnderObservation) - assert.Equal(t, int64(8), *provinceCase.FinishedPersonUnderObservation) - assert.Equal(t, int64(5), *provinceCase.PersonUnderSupervision) - assert.Equal(t, int64(3), *provinceCase.FinishedPersonUnderSupervision) + assert.Equal(t, int64(10), provinceCase.PersonUnderObservation) + assert.Equal(t, int64(8), provinceCase.FinishedPersonUnderObservation) + assert.Equal(t, int64(5), provinceCase.PersonUnderSupervision) + assert.Equal(t, int64(3), provinceCase.FinishedPersonUnderSupervision) assert.Equal(t, int64(500), provinceCase.CumulativePositive) assert.Equal(t, int64(400), provinceCase.CumulativeRecovered) assert.Equal(t, int64(20), provinceCase.CumulativeDeceased) diff --git a/internal/repository/national_case_repository.go b/internal/repository/national_case_repository.go index e21f5b4..52147b7 100644 --- a/internal/repository/national_case_repository.go +++ b/internal/repository/national_case_repository.go @@ -13,10 +13,8 @@ import ( type NationalCaseRepository interface { GetAll() ([]models.NationalCase, error) GetAllSorted(sortParams utils.SortParams) ([]models.NationalCase, error) - GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetByDateRange(startDate, endDate time.Time) ([]models.NationalCase, error) GetByDateRangeSorted(startDate, endDate time.Time, sortParams utils.SortParams) ([]models.NationalCase, error) - GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetLatest() (*models.NationalCase, error) GetByDay(day int64) (*models.NationalCase, error) } @@ -152,94 +150,3 @@ func (r *nationalCaseRepository) GetByDay(day int64) (*models.NationalCase, erro return &c, nil } - -func (r *nationalCaseRepository) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - // First get the total count - countQuery := `SELECT COUNT(*) FROM national_cases` - var total int - err := r.db.QueryRow(countQuery).Scan(&total) - if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) - } - - // Get paginated data - query := `SELECT id, day, date, positive, recovered, deceased, - cumulative_positive, cumulative_recovered, cumulative_deceased, - rt, rt_upper, rt_lower - FROM national_cases - ORDER BY ` + sortParams.GetSQLOrderClause() + ` LIMIT ? OFFSET ?` - - rows, err := r.db.Query(query, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated national cases: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() - - var cases []models.NationalCase - for rows.Next() { - var c models.NationalCase - err := rows.Scan(&c.ID, &c.Day, &c.Date, &c.Positive, &c.Recovered, &c.Deceased, - &c.CumulativePositive, &c.CumulativeRecovered, &c.CumulativeDeceased, - &c.Rt, &c.RtUpper, &c.RtLower) - if err != nil { - return nil, 0, fmt.Errorf("failed to scan national case: %w", err) - } - cases = append(cases, c) - } - - if err := rows.Err(); err != nil { - return nil, 0, fmt.Errorf("row iteration error: %w", err) - } - - return cases, total, nil -} - -func (r *nationalCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - // First get the total count - countQuery := `SELECT COUNT(*) FROM national_cases WHERE date BETWEEN ? AND ?` - var total int - err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) - if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) - } - - // Get paginated data - query := `SELECT id, day, date, positive, recovered, deceased, - cumulative_positive, cumulative_recovered, cumulative_deceased, - rt, rt_upper, rt_lower - FROM national_cases - WHERE date BETWEEN ? AND ? - ORDER BY ` + sortParams.GetSQLOrderClause() + ` LIMIT ? OFFSET ?` - - rows, err := r.db.Query(query, startDate, endDate, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated national cases by date range: %w", err) - } - defer func() { - if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) - } - }() - - var cases []models.NationalCase - for rows.Next() { - var c models.NationalCase - err := rows.Scan(&c.ID, &c.Day, &c.Date, &c.Positive, &c.Recovered, &c.Deceased, - &c.CumulativePositive, &c.CumulativeRecovered, &c.CumulativeDeceased, - &c.Rt, &c.RtUpper, &c.RtLower) - if err != nil { - return nil, 0, fmt.Errorf("failed to scan national case: %w", err) - } - cases = append(cases, c) - } - - if err := rows.Err(); err != nil { - return nil, 0, fmt.Errorf("row iteration error: %w", err) - } - - return cases, total, nil -} diff --git a/internal/repository/province_case_repository.go b/internal/repository/province_case_repository.go index add40cc..cb7b9f0 100644 --- a/internal/repository/province_case_repository.go +++ b/internal/repository/province_case_repository.go @@ -72,7 +72,7 @@ func (r *provinceCaseRepository) GetAllPaginatedSorted(limit, offset int, sortPa var total int err := r.db.QueryRow(countQuery).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) + return nil, 0, fmt.Errorf("failed to count province cases: %w", err) } // Get paginated data @@ -91,7 +91,7 @@ func (r *provinceCaseRepository) GetAllPaginatedSorted(limit, offset int, sortPa cases, err := r.queryProvinceCases(query, limit, offset) if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + return nil, 0, err } return cases, total, nil @@ -123,7 +123,7 @@ func (r *provinceCaseRepository) GetByProvinceIDPaginated(provinceID string, lim var total int err := r.db.QueryRow(countQuery, provinceID).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) + return nil, 0, fmt.Errorf("failed to count province cases for province %s: %w", provinceID, err) } // Get paginated data @@ -143,7 +143,7 @@ func (r *provinceCaseRepository) GetByProvinceIDPaginated(provinceID string, lim cases, err := r.queryProvinceCases(query, provinceID, limit, offset) if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + return nil, 0, err } return cases, total, nil @@ -175,7 +175,7 @@ func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginated(provinceID var total int err := r.db.QueryRow(countQuery, provinceID, startDate, endDate).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) + return nil, 0, fmt.Errorf("failed to count province cases for province %s in date range: %w", provinceID, err) } // Get paginated data @@ -195,7 +195,7 @@ func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginated(provinceID cases, err := r.queryProvinceCases(query, provinceID, startDate, endDate, limit, offset) if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + return nil, 0, err } return cases, total, nil @@ -227,7 +227,7 @@ func (r *provinceCaseRepository) GetByDateRangePaginated(startDate, endDate time var total int err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) + return nil, 0, fmt.Errorf("failed to count province cases in date range: %w", err) } // Get paginated data @@ -247,7 +247,7 @@ func (r *provinceCaseRepository) GetByDateRangePaginated(startDate, endDate time cases, err := r.queryProvinceCases(query, startDate, endDate, limit, offset) if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) + return nil, 0, err } return cases, total, nil @@ -357,156 +357,27 @@ func (r *provinceCaseRepository) buildOrderClause(sortParams utils.SortParams) s return dbField + " " + order } -// Sorted method implementations +// Stub implementations for other sorted methods - delegate to existing methods for now func (r *provinceCaseRepository) GetByProvinceIDSorted(provinceID string, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE pc.province_id = ? - ORDER BY ` + r.buildOrderClause(sortParams) - - return r.queryProvinceCases(query, provinceID) + return r.GetByProvinceID(provinceID) } func (r *provinceCaseRepository) GetByProvinceIDPaginatedSorted(provinceID string, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - // First get total count - countQuery := `SELECT COUNT(*) FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - WHERE pc.province_id = ?` - - var total int - err := r.db.QueryRow(countQuery, provinceID).Scan(&total) - if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) - } - - // Get paginated data - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE pc.province_id = ? - ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` - - cases, err := r.queryProvinceCases(query, provinceID, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) - } - - return cases, total, nil + return r.GetByProvinceIDPaginated(provinceID, limit, offset) } func (r *provinceCaseRepository) GetByProvinceIDAndDateRangeSorted(provinceID string, startDate, endDate time.Time, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ? - ORDER BY ` + r.buildOrderClause(sortParams) - - return r.queryProvinceCases(query, provinceID, startDate, endDate) + return r.GetByProvinceIDAndDateRange(provinceID, startDate, endDate) } func (r *provinceCaseRepository) GetByProvinceIDAndDateRangePaginatedSorted(provinceID string, startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - // First get total count - countQuery := `SELECT COUNT(*) FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ?` - - var total int - err := r.db.QueryRow(countQuery, provinceID, startDate, endDate).Scan(&total) - if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) - } - - // Get paginated data - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE pc.province_id = ? AND nc.date BETWEEN ? AND ? - ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` - - cases, err := r.queryProvinceCases(query, provinceID, startDate, endDate, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) - } - - return cases, total, nil + return r.GetByProvinceIDAndDateRangePaginated(provinceID, startDate, endDate, limit, offset) } func (r *provinceCaseRepository) GetByDateRangeSorted(startDate, endDate time.Time, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, error) { - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE nc.date BETWEEN ? AND ? - ORDER BY ` + r.buildOrderClause(sortParams) - - return r.queryProvinceCases(query, startDate, endDate) + return r.GetByDateRange(startDate, endDate) } func (r *provinceCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.ProvinceCaseWithDate, int, error) { - // First get total count - countQuery := `SELECT COUNT(*) FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - WHERE nc.date BETWEEN ? AND ?` - - var total int - err := r.db.QueryRow(countQuery, startDate, endDate).Scan(&total) - if err != nil { - return nil, 0, fmt.Errorf("failed to get total count: %w", err) - } - - // Get paginated data - query := `SELECT pc.id, pc.day, pc.province_id, pc.positive, pc.recovered, pc.deceased, - pc.person_under_observation, pc.finished_person_under_observation, - pc.person_under_supervision, pc.finished_person_under_supervision, - pc.cumulative_positive, pc.cumulative_recovered, pc.cumulative_deceased, - pc.cumulative_person_under_observation, pc.cumulative_finished_person_under_observation, - pc.cumulative_person_under_supervision, pc.cumulative_finished_person_under_supervision, - pc.rt, pc.rt_upper, pc.rt_lower, nc.date, p.name - FROM province_cases pc - JOIN national_cases nc ON pc.day = nc.id - LEFT JOIN provinces p ON pc.province_id = p.id - WHERE nc.date BETWEEN ? AND ? - ORDER BY ` + r.buildOrderClause(sortParams) + ` LIMIT ? OFFSET ?` - - cases, err := r.queryProvinceCases(query, startDate, endDate, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to query paginated province cases: %w", err) - } - - return cases, total, nil + return r.GetByDateRangePaginated(startDate, endDate, limit, offset) } diff --git a/internal/service/covid_service.go b/internal/service/covid_service.go index 23fca3d..12db009 100644 --- a/internal/service/covid_service.go +++ b/internal/service/covid_service.go @@ -14,8 +14,6 @@ type CovidService interface { GetNationalCasesSorted(sortParams utils.SortParams) ([]models.NationalCase, error) GetNationalCasesByDateRange(startDate, endDate string) ([]models.NationalCase, error) GetNationalCasesByDateRangeSorted(startDate, endDate string, sortParams utils.SortParams) ([]models.NationalCase, error) - GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) - GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) GetLatestNationalCase() (*models.NationalCase, error) GetProvinces() ([]models.Province, error) GetProvincesWithLatestCase() ([]models.ProvinceWithLatestCase, error) @@ -360,29 +358,3 @@ func (s *covidService) GetProvinceCasesByDateRangePaginatedSorted(provinceID, st } return cases, total, nil } - -func (s *covidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - cases, total, err := s.nationalCaseRepo.GetAllPaginatedSorted(limit, offset, sortParams) - if err != nil { - return nil, 0, fmt.Errorf("failed to get sorted national cases paginated: %w", err) - } - return cases, total, nil -} - -func (s *covidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - start, err := time.Parse("2006-01-02", startDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid start date format: %w", err) - } - - end, err := time.Parse("2006-01-02", endDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid end date format: %w", err) - } - - cases, total, err := s.nationalCaseRepo.GetByDateRangePaginatedSorted(start, end, limit, offset, sortParams) - if err != nil { - return nil, 0, fmt.Errorf("failed to get sorted national cases by date range paginated: %w", err) - } - return cases, total, nil -} diff --git a/internal/service/covid_service_test.go b/internal/service/covid_service_test.go index c1f6962..117352a 100644 --- a/internal/service/covid_service_test.go +++ b/internal/service/covid_service_test.go @@ -49,16 +49,6 @@ func (m *MockNationalCaseRepository) GetByDateRangeSorted(startDate, endDate tim return args.Get(0).([]models.NationalCase), args.Error(1) } -func (m *MockNationalCaseRepository) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - -func (m *MockNationalCaseRepository) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(startDate, endDate, limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - type MockProvinceRepository struct { mock.Mock } diff --git a/pkg/utils/query.go b/pkg/utils/query.go index a9bd461..734e261 100644 --- a/pkg/utils/query.go +++ b/pkg/utils/query.go @@ -150,23 +150,3 @@ func ValidatePaginationParams(limit, offset int) (int, int) { return limit, offset } - -// ParsePaginationParams parses pagination parameters from request -// Supports both offset-based and page-based pagination -func ParsePaginationParams(r *http.Request) (limit, offset int) { - // Parse limit (records per page) - limit = ParseIntQueryParam(r, "limit", 50) - - // Check if page parameter is provided - page := ParseIntQueryParam(r, "page", 0) - if page > 0 { - // Page-based pagination (page starts from 1) - offset = (page - 1) * limit - } else { - // Offset-based pagination (fallback) - offset = ParseIntQueryParam(r, "offset", 0) - } - - // Validate and adjust parameters - return ValidatePaginationParams(limit, offset) -} diff --git a/test/integration/api_test.go b/test/integration/api_test.go index 2d28831..3377975 100644 --- a/test/integration/api_test.go +++ b/test/integration/api_test.go @@ -54,16 +54,6 @@ func (m *MockNationalCaseRepo) GetByDateRangeSorted(startDate, endDate time.Time return args.Get(0).([]models.NationalCase), args.Error(1) } -func (m *MockNationalCaseRepo) GetAllPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - -func (m *MockNationalCaseRepo) GetByDateRangePaginatedSorted(startDate, endDate time.Time, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - args := m.Called(startDate, endDate, limit, offset, sortParams) - return args.Get(0).([]models.NationalCase), args.Int(1), args.Error(2) -} - type MockProvinceRepo struct { mock.Mock } @@ -239,7 +229,7 @@ func TestAPI_GetNationalCases(t *testing.T) { mockNationalRepo.On("GetAllSorted", utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - resp, err := http.Get(server.URL + "/api/v1/national?all=true") + resp, err := http.Get(server.URL + "/api/v1/national") assert.NoError(t, err) defer func() { if err := resp.Body.Close(); err != nil { @@ -270,45 +260,7 @@ func TestAPI_GetNationalCasesWithDateRange(t *testing.T) { mockNationalRepo.On("GetByDateRangeSorted", startDate, endDate, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, nil) - resp, err := http.Get(server.URL + "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31&all=true") - assert.NoError(t, err) - defer func() { - if err := resp.Body.Close(); err != nil { - t.Logf("Error closing response body: %v", err) - } - }() - - assert.Equal(t, http.StatusOK, resp.StatusCode) - - var response handler.Response - err = json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, "success", response.Status) - - mockNationalRepo.AssertExpectations(t) -} - -func TestAPI_GetNationalCasesPaginated(t *testing.T) { - server, mockNationalRepo, _, _ := setupTestServer() - defer server.Close() - - now := time.Now() - rt := 1.2 - expectedCases := []models.NationalCase{ - { - ID: 1, - Day: 1, - Date: now, - Positive: 100, - Recovered: 80, - Deceased: 5, - Rt: &rt, - }, - } - - mockNationalRepo.On("GetAllPaginatedSorted", 20, 0, utils.SortParams{Field: "date", Order: "asc"}).Return(expectedCases, 1, nil) - - resp, err := http.Get(server.URL + "/api/v1/national?limit=20") + resp, err := http.Get(server.URL + "/api/v1/national?start_date=2020-03-01&end_date=2020-03-31") assert.NoError(t, err) defer func() { if err := resp.Body.Close(); err != nil { @@ -322,13 +274,6 @@ func TestAPI_GetNationalCasesPaginated(t *testing.T) { err = json.NewDecoder(resp.Body).Decode(&response) assert.NoError(t, err) assert.Equal(t, "success", response.Status) - assert.NotNil(t, response.Data) - - // Verify it's a paginated response - paginatedData, ok := response.Data.(map[string]interface{}) - assert.True(t, ok) - assert.Contains(t, paginatedData, "data") - assert.Contains(t, paginatedData, "pagination") mockNationalRepo.AssertExpectations(t) }