{
  "markdown": "[![CI](https://github.com/fairyhunter13/community-waste-collection-system/actions/workflows/ci.yml/badge.svg)](https://github.com/fairyhunter13/community-waste-collection-system/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/fairyhunter13/community-waste-collection-system/graph/badge.svg)](https://codecov.io/gh/fairyhunter13/community-waste-collection-system)\n[![Coverage](https://img.shields.io/codecov/c/github/fairyhunter13/community-waste-collection-system/main)](https://codecov.io/gh/fairyhunter13/community-waste-collection-system)\n\n# Community Waste Collection API\n\nA RESTful API service for managing community household waste collection,\npickup scheduling, and proof-of-payment processing.\n\nBuilt with Go 1.26, Echo v4, PostgreSQL 17, MinIO, and Docker Compose.\n\n---\n\n## Key Features\n\n- **15 product REST endpoints** across households, pickups, payments, and reports plus 3 operational endpoints (`/health`, `/readyz`, `/metrics`)\n- **6 business rules** enforced in the service layer:\n  - BR-01 — A household with any pending payment cannot create a new pickup\n  - BR-02 — Only pending pickups can be scheduled. Only scheduled can be completed or canceled\n  - BR-03 — Electronic waste pickup requires a `safety_check: true` flag\n  - BR-04 — Organic pickups with no scheduled date for 3 days are auto-cancelled by a background worker\n  - BR-05 — Completing a pickup atomically auto-generates a payment record at the confirmed amount\n  - BR-06 — Payment confirmation requires a multipart proof-of-payment file upload\n- **Per-IP rate limiting** on pickup creation (5 req/s, burst 10) through token bucket\n- **Full-stack observability**: structured JSON logs (slog), distributed tracing (OTel → Jaeger), 21 Prometheus instruments, 3 auto-provisioned Grafana dashboards\n- **Unit test coverage ≥80%** enforced in CI. Integration tests use real PostgreSQL through testcontainers\n- **OpenAPI 3.0 spec** documented in `api/openapi.yaml`\n\n---\n\n## Documentation\n\n| Doc | Contents |\n|---|---|\n| [docs/architecture.md](docs/architecture.md) | Layers, request flow, business processes, data model, decisions |\n| [docs/deployment.md](docs/deployment.md) | Compose topology, observability data flow, graceful shutdown |\n| [docs/operations.md](docs/operations.md) | Failure-mode decision tree, health endpoints, recovery |\n| [api/openapi.yaml](api/openapi.yaml) | The endpoint contract, error envelope and status codes |\n\n---\n\n## Prerequisites\n\n- **Go 1.26+** (`go version` should report `go1.26.x`).\n- **Docker 24+** with the **Compose v2 plugin** (`docker compose version` should report `2.x`). The legacy `docker-compose` shim is not used.\n- **GNU make** (`make --version`). On macOS, ship Xcode Command Line Tools.\n- **Ports free** on the host: `8080` (API), `5432` (Postgres), `9000`/`9001` (MinIO), `3000` (Grafana), `9090` (Prometheus), `16686` (Jaeger UI), `4317`/`4318` (Jaeger OTLP), `3100` (Loki), `2112` (Prometheus scrape target), `6060` (pprof). See [Troubleshooting](#troubleshooting) if any of these collide with a host service.\n- **Optional but recommended for local SQL work:**\n  - [`migrate` CLI](https://github.com/golang-migrate/migrate/releases) — run migrations from the host without entering the API container.\n  - `psql` client — inspect the live DB during development.\n  - [`newman`](https://github.com/postmanlabs/newman) (`npm i -g newman`) — replay the Postman collection against a running stack.\n\n---\n\n## Quick Start\n\n```bash\ncp .env.example .env\nmake docker-up       # start all services (postgres, minio, jaeger, prometheus, grafana, loki, promtail, api)\nmake migrate-up      # apply database migrations\n```\n\nVerify the stack is healthy:\n\n```bash\ncurl http://localhost:8080/health\n# {\"status\":\"ok\"}\n```\n\n| Service | URL | Credentials |\n|---|---|---|\n| API | http://localhost:8080 | — |\n| Grafana Dashboard | http://localhost:3000 | admin / admin |\n| Prometheus | http://localhost:9090 | — |\n| Jaeger UI (traces) | http://localhost:16686 | — |\n| MinIO console | http://localhost:9001 | minioadmin / minioadmin |\n| Prometheus metrics | http://localhost:2112/metrics | — |\n| pprof debug | http://localhost:6060/debug/pprof/ | — |\n\nGrafana auto-provisions three dashboards on startup:\n\n- **Waste Collection API** — 7 rows: API traffic, business events, database performance, background worker, Go runtime, process metrics, S3 storage, and Jaeger traces\n- **Business Operations** — 4 rows: pickup funnel, payment funnel, error breakdown, S3 storage KPIs\n- **Logs & Traces** — Loki log stream with trace-ID correlation links to Jaeger\n\n---\n\n## Troubleshooting\n\nCommon issues encountered when booting the stack for the first time.\n\n### Port already in use\n\n`docker compose up` fails with `bind: address already in use`. The\nservice that owns the conflicting port is reported in the error.\n\n```bash\n# Find the host process holding the port (example: 5432)\nsudo ss -tulpn | grep ':5432 '\n\n# Either stop the offending process or remap the port in\n# deployments/docker-compose.yml under the relevant service's\n# `ports:` section, then `make docker-down && make docker-up`.\n```\n\n### Migrations fail with `connection refused`\n\nPostgres takes a few seconds to accept connections after the container\nstarts. Wait for the `db` service to become healthy before running\n`make migrate-up`:\n\n```bash\ndocker compose -f deployments/docker-compose.yml ps db | grep healthy\n```\n\nIf healthy still fails, confirm the `DATABASE_URL` in `.env` points at\n`localhost:5432` (not `db:5432`, which is the in-network hostname only\nvisible to other compose services).\n\n### MinIO bucket missing on first run\n\nThe application creates the `proofs` bucket on startup if absent. If\n`PUT /api/payments/:id/confirm` returns 500 with `BucketNotFound`, the\nboot-time check ran before MinIO finished initialising. Restart the\nAPI container:\n\n```bash\ndocker compose -f deployments/docker-compose.yml restart api\n```\n\nYou can also create the bucket manually in the MinIO console at\nhttp://localhost:9001 (login `minioadmin` / `minioadmin`).\n\n### `migrate` CLI not found\n\nInstall from the [golang-migrate releases](https://github.com/golang-migrate/migrate/releases).\nAlternatively, run migrations from inside the API container:\n\n```bash\ndocker compose -f deployments/docker-compose.yml exec api \\\n  migrate -path=/migrations -database \"$DATABASE_URL\" up\n```\n\n### Grafana panels are empty\n\nGenerate traffic before checking dashboards — without requests, the\npanels have no data points to render:\n\n```bash\nfor _ in $(seq 1 20); do curl -s http://localhost:8080/health >/dev/null; done\n```\n\nThen refresh Grafana. If panels still show \"No data\", confirm the\nPrometheus datasource is configured at http://localhost:9090 (it is\nauto-provisioned but may report `unreachable` if Prometheus failed to\nboot).\n\n---\n\n## API Walkthrough\n\nA complete end-to-end flow from household registration to payment confirmation:\n\n**1. Register a household**\n\n```bash\nHH=$(curl -s -X POST http://localhost:8080/api/households \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"owner_name\":\"Ahmad Sutrisno\",\"address\":\"Jl. Merdeka 12, Jakarta\"}')\necho $HH | jq .\nHH_ID=$(echo $HH | jq -r '.data.id')\n```\n\n```json\n{ \"success\": true, \"data\": { \"id\": \"uuid-here\", \"owner_name\": \"Ahmad Sutrisno\", \"address\": \"Jl. Merdeka 12, Jakarta\", \"created_at\": \"...\" } }\n```\n\n**2. Request a pickup**\n\n```bash\nPK=$(curl -s -X POST http://localhost:8080/api/pickups \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\\\"household_id\\\":\\\"$HH_ID\\\",\\\"type\\\":\\\"organic\\\"}\")\nPK_ID=$(echo $PK | jq -r '.data.id')\n```\n\n**3. Schedule the pickup**\n\n```bash\ncurl -s -X PUT \"http://localhost:8080/api/pickups/$PK_ID/schedule\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"pickup_date\":\"2026-02-14T09:00:00Z\"}' | jq .\n```\n\n**4. Complete the pickup** (auto-creates a payment record)\n\n```bash\ncurl -s -X PUT \"http://localhost:8080/api/pickups/$PK_ID/complete\" | jq .\n```\n\n**5. Get the auto-created payment**\n\n```bash\nPM=$(curl -s \"http://localhost:8080/api/payments?household_id=$HH_ID\")\nPM_ID=$(echo $PM | jq -r '.data[0].id')\n```\n\n**6. Confirm payment with proof upload**\n\n```bash\ncurl -s -X PUT \"http://localhost:8080/api/payments/$PM_ID/confirm\" \\\n  -F \"proof=@/path/to/receipt.jpg;type=image/jpeg\" | jq .\n```\n\n**7. View household history**\n\n```bash\ncurl -s \"http://localhost:8080/api/reports/households/$HH_ID/history\" | jq .\n```\n\n---\n\n## Error Reference\n\n| HTTP | Code | Triggered by |\n|---|---|---|\n| `400` | `VALIDATION_ERROR` | Missing required field, invalid enum value, past pickup date, malformed UUID |\n| `404` | `NOT_FOUND` | Resource ID does not exist in the database |\n| `409` | `CONFLICT` | BR-01 (household has a pending payment), BR-02 (wrong pickup status for operation) |\n| `422` | `BUSINESS_RULE_VIOLATION` | BR-03 (electronic without safety_check), BR-06 (confirm without proof file) |\n| `429` | `RATE_LIMITED` | More than 5 pickup creation requests per second from the same IP |\n| `500` | `INTERNAL_ERROR` | Unexpected server-side error |\n| `503` | `service unavailable` | Health check: database unreachable |\n\n---\n\n## Environment Variables\n\n| Variable | Default | Description |\n|---|---|---|\n| `APP_PORT` | `8080` | HTTP server port |\n| `APP_ENV` | `development` | Environment name |\n| `DEBUG_PORT` | `6060` | pprof debug server port |\n| `DATABASE_URL` | `postgres://postgres:postgres@localhost:5432/waste_collection?sslmode=disable` | PostgreSQL connection string |\n| `DB_MAX_OPEN_CONNS` | `25` | DB connection pool max open |\n| `DB_MAX_IDLE_CONNS` | `10` | DB connection pool max idle |\n| `DB_CONN_MAX_IDLE_TIME` | `5m` | Max time a connection can remain idle before being closed |\n| `DB_CONN_MAX_LIFETIME` | `30m` | Max lifetime of a DB connection before it is recycled |\n| `DB_APPLICATION_NAME` | `waste-collection-api` | `application_name` sent to Postgres (visible in `pg_stat_activity`) |\n| `S3_ENDPOINT` | `http://localhost:9000` | S3-compatible storage endpoint |\n| `S3_BUCKET` | `waste-proofs` | Bucket for payment proof uploads |\n| `S3_ACCESS_KEY` | `minioadmin` | S3 access key |\n| `S3_SECRET_KEY` | `minioadmin` | S3 secret key |\n| `S3_REGION` | `us-east-1` | S3 region (required by AWS SDK; MinIO ignores it) |\n| `S3_USE_PATH_STYLE` | `true` | Use path-style S3 URLs (required for MinIO) |\n| `MAX_UPLOAD_SIZE_MB` | `10` | Maximum proof file upload size |\n| `HTTP_READ_HEADER_TIMEOUT` | `5s` | Max time to read request headers |\n| `HTTP_READ_TIMEOUT` | `15s` | Max time to read the full request (headers + body) |\n| `HTTP_WRITE_TIMEOUT` | `15s` | Max time to write the response |\n| `HTTP_IDLE_TIMEOUT` | `60s` | Max time a keep-alive connection can be idle |\n| `HTTP_SHUTDOWN_TIMEOUT` | `15s` | Grace period for in-flight requests on shutdown |\n| `RATE_LIMIT_RPS` | `5` | Pickup creation rate limit (req/sec/IP) |\n| `RATE_LIMIT_BURST` | `10` | Rate limit burst capacity |\n| `LOG_LEVEL` | `info` | Log level: `debug`, `info`, `warn`, `error` |\n| `LOG_FORMAT` | `json` | Log format: `json` or `text` |\n| `METRICS_PORT` | `2112` | Prometheus metrics server port |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | OTel OTLP HTTP endpoint |\n| `OTEL_SERVICE_NAME` | `community-waste-collection-api` | OTel service name |\n| `OTEL_SERVICE_VERSION` | `0.1.0` | OTel service version tag on all spans |\n| `WORKER_CANCEL_INTERVAL` | `1h` | How often the organic-canceler worker runs |\n| `WORKER_ORGANIC_CUTOFF_DAYS` | `3` | Days after which a pending organic pickup is auto-cancelled |\n| `WORKER_QUERY_TIMEOUT` | `5s` | Max time for a single DB query inside the worker |\n| `WORKER_SHUTDOWN_TIMEOUT` | `30s` | Grace period for the worker to finish its current cycle on shutdown |\n| `CODECOV_TOKEN` | — | Codecov upload token (CI only, never commit the value) |\n\nSee `.env.example` for a complete reference with comments.\n\n---\n\n## Running Locally (without Docker)\n\n```bash\n# 1. Start infrastructure only\ndocker compose up -d postgres minio jaeger\n\n# 2. Apply migrations\nmake migrate-up\n\n# 3. (Optional) Seed demo data\nmake seed   # or: psql \"$DATABASE_URL\" -f scripts/seed.sql\n\n# 4. Run the API\nmake run\n```\n\n---\n\n## Running Tests\n\n```bash\n# Unit tests (no external dependencies)\nmake test\n\n# Integration tests (spins up Postgres via testcontainers)\nmake test-integration\n\n# E2E tests (requires full stack via docker-compose)\nmake docker-up && make migrate-up\nmake test-e2e\n\n# HTTP performance benchmarks (requires full stack + running app)\nmake docker-up && make migrate-up\nmake perf\n\n# DB-layer micro-benchmarks (requires DATABASE_URL, no docker stack needed)\nmake bench\n```\n\n> The BR-04 organic worker E2E test requires `E2E_DB_URL` pointing at the host-accessible\n> Postgres URL (e.g. `postgres://postgres:postgres@localhost:5432/waste_collection?sslmode=disable`).\n> Without it the worker test skips automatically.\n\n---\n\n## Known Limits\n\n- **DB and MinIO credentials default to development values.** Override `DATABASE_URL`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` through environment variables or a secrets manager before deploying to production.\n- **Jaeger uses in-memory trace storage in docker-compose.** Spans are lost on container restart — intentional for local development. Use a persistent backend (e.g. Elasticsearch) in production.\n- **`DELETE /api/households` performs a hard cascade delete.** The household and all linked pickups/payments are permanently removed. Audit trail preservation is out of scope for v1.\n- **`WORKER_CANCEL_INTERVAL` adds up to one tick of drift to the 3-day organic-cancellation cutoff.** At the default 1-hour interval the worst case is ~73 hours. Decrease `WORKER_CANCEL_INTERVAL` for tighter SLAs.\n- **pprof debug server binds to `127.0.0.1` only.** Not reachable from outside the container. No host port is mapped.\n\n---\n\n",
  "bytes": 13730,
  "sha": "de33741465c856a16d87059ef8767c8bf2b093966dadf6df3567ed95a3dd2567",
  "repo_slug": "fairyhunter13/community-waste-collection-system",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_fairyhunter13_community_waste_collection_256feaf4/readme"
}