diff --git a/public/docs/index.html b/public/docs/index.html
new file mode 100644
index 0000000..2ed8030
--- /dev/null
+++ b/public/docs/index.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+ API Retribusi - Documentation
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/docs/openapi.json b/public/docs/openapi.json
new file mode 100644
index 0000000..9775579
--- /dev/null
+++ b/public/docs/openapi.json
@@ -0,0 +1,604 @@
+{
+ "openapi": "3.0.0",
+ "info": {
+ "title": "API Retribusi",
+ "description": "Sistem API Retribusi berbasis Slim Framework 4 untuk infrastruktur pemerintah",
+ "version": "1.0.0",
+ "contact": {
+ "name": "BTekno Development Team"
+ }
+ },
+ "servers": [
+ {
+ "url": "https://api.btekno.cloud",
+ "description": "Production Server"
+ },
+ {
+ "url": "http://localhost",
+ "description": "Local Development"
+ }
+ ],
+ "tags": [
+ {
+ "name": "Health",
+ "description": "Health check endpoint"
+ },
+ {
+ "name": "Authentication",
+ "description": "JWT authentication"
+ },
+ {
+ "name": "Ingest",
+ "description": "Event ingestion (mesin YOLO)"
+ },
+ {
+ "name": "Frontend",
+ "description": "Frontend CRUD operations"
+ },
+ {
+ "name": "Summary",
+ "description": "Data summary & aggregation"
+ },
+ {
+ "name": "Dashboard",
+ "description": "Dashboard visualization data"
+ },
+ {
+ "name": "Realtime",
+ "description": "Real-time events (SSE)"
+ }
+ ],
+ "components": {
+ "securitySchemes": {
+ "BearerAuth": {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ "description": "JWT token untuk frontend API"
+ },
+ "ApiKeyAuth": {
+ "type": "apiKey",
+ "in": "header",
+ "name": "X-API-KEY",
+ "description": "API Key untuk ingest endpoint"
+ }
+ },
+ "schemas": {
+ "Error": {
+ "type": "object",
+ "properties": {
+ "error": {
+ "type": "string",
+ "description": "Error code"
+ },
+ "message": {
+ "type": "string",
+ "description": "Error message"
+ },
+ "fields": {
+ "type": "object",
+ "description": "Validation errors (optional)"
+ }
+ }
+ },
+ "Success": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "example": true
+ },
+ "data": {
+ "type": "object"
+ },
+ "timestamp": {
+ "type": "integer",
+ "description": "Unix timestamp"
+ }
+ }
+ }
+ }
+ },
+ "paths": {
+ "/health": {
+ "get": {
+ "tags": ["Health"],
+ "summary": "Health check",
+ "description": "Check API health status",
+ "responses": {
+ "200": {
+ "description": "API is healthy",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "string",
+ "example": "ok"
+ },
+ "time": {
+ "type": "integer",
+ "example": 1735123456
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/v1/login": {
+ "post": {
+ "tags": ["Authentication"],
+ "summary": "Login",
+ "description": "Authenticate user dan dapatkan JWT token",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["username", "password"],
+ "properties": {
+ "username": {
+ "type": "string",
+ "example": "admin"
+ },
+ "password": {
+ "type": "string",
+ "format": "password",
+ "example": "password123"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Login successful",
+ "content": {
+ "application/json": {
+ "schema": {
+ "allOf": [
+ {"$ref": "#/components/schemas/Success"},
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "object",
+ "properties": {
+ "token": {
+ "type": "string"
+ },
+ "expires_in": {
+ "type": "integer"
+ },
+ "user": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "integer"},
+ "username": {"type": "string"},
+ "role": {"type": "string"}
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden (user inactive)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/retribusi/v1/ingest": {
+ "post": {
+ "tags": ["Ingest"],
+ "summary": "Ingest event data",
+ "description": "Masukkan event data dari mesin YOLO",
+ "security": [
+ {
+ "ApiKeyAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["timestamp", "location_code", "gate_code", "category"],
+ "properties": {
+ "timestamp": {
+ "type": "integer",
+ "description": "Unix timestamp",
+ "example": 1735123456
+ },
+ "location_code": {
+ "type": "string",
+ "example": "kerkof_01"
+ },
+ "gate_code": {
+ "type": "string",
+ "example": "gate01"
+ },
+ "category": {
+ "type": "string",
+ "example": "motor"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Event stored successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Success"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized (invalid API key)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found (location/gate/tariff not found)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/retribusi/v1/frontend/locations": {
+ "get": {
+ "tags": ["Frontend"],
+ "summary": "Get locations list",
+ "description": "Mendapatkan daftar lokasi dengan pagination",
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "schema": {
+ "type": "integer",
+ "default": 1,
+ "minimum": 1
+ },
+ "description": "Page number"
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "schema": {
+ "type": "integer",
+ "default": 20,
+ "minimum": 1,
+ "maximum": 100
+ },
+ "description": "Items per page"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Success"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post": {
+ "tags": ["Frontend"],
+ "summary": "Create location",
+ "description": "Membuat lokasi baru (operator/admin only)",
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": ["code", "name", "type", "is_active"],
+ "properties": {
+ "code": {
+ "type": "string",
+ "example": "kerkof_01"
+ },
+ "name": {
+ "type": "string",
+ "example": "Kerkof Garut"
+ },
+ "type": {
+ "type": "string",
+ "example": "parkir"
+ },
+ "is_active": {
+ "type": "integer",
+ "enum": [0, 1],
+ "example": 1
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Location created",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Success"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden (insufficient permissions)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Conflict (code already exists)",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/retribusi/v1/summary/daily": {
+ "get": {
+ "tags": ["Summary"],
+ "summary": "Get daily summary",
+ "description": "Mendapatkan rekap harian",
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "date",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "date",
+ "example": "2025-01-01"
+ },
+ "description": "Date (Y-m-d format)"
+ },
+ {
+ "name": "location_code",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "gate_code",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/retribusi/v1/dashboard/daily": {
+ "get": {
+ "tags": ["Dashboard"],
+ "summary": "Get daily chart data",
+ "description": "Data untuk line chart harian",
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "start_date",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "end_date",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "date"
+ }
+ },
+ {
+ "name": "location_code",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "gate_code",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Success"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/retribusi/v1/realtime/stream": {
+ "get": {
+ "tags": ["Realtime"],
+ "summary": "SSE Stream",
+ "description": "Server-Sent Events stream untuk real-time updates",
+ "security": [
+ {
+ "BearerAuth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "last_id",
+ "in": "query",
+ "schema": {
+ "type": "integer"
+ },
+ "description": "Last event ID received"
+ },
+ {
+ "name": "location_code",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "SSE stream",
+ "content": {
+ "text/event-stream": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Error"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/public/index.php b/public/index.php
index 9d56a31..d85d1bd 100644
--- a/public/index.php
+++ b/public/index.php
@@ -19,6 +19,47 @@ AppConfig::loadEnv(__DIR__ . '/..');
// Bootstrap application
$app = AppBootstrap::create();
+// Root route - redirect to docs
+$app->get('/', function ($request, $response) {
+ return $response
+ ->withHeader('Location', '/docs')
+ ->withStatus(302);
+});
+
+// Docs route - serve Swagger UI
+$app->get('/docs', function ($request, $response) {
+ $docsPath = __DIR__ . '/docs/index.html';
+
+ if (!file_exists($docsPath)) {
+ return $response
+ ->withStatus(404)
+ ->withHeader('Content-Type', 'text/html')
+ ->getBody()->write('Documentation not found
');
+ }
+
+ $html = file_get_contents($docsPath);
+ $response->getBody()->write($html);
+
+ return $response->withHeader('Content-Type', 'text/html');
+});
+
+// Serve OpenAPI JSON
+$app->get('/docs/openapi.json', function ($request, $response) {
+ $openApiPath = __DIR__ . '/docs/openapi.json';
+
+ if (!file_exists($openApiPath)) {
+ return $response
+ ->withStatus(404)
+ ->withHeader('Content-Type', 'application/json')
+ ->getBody()->write(json_encode(['error' => 'OpenAPI spec not found']));
+ }
+
+ $json = file_get_contents($openApiPath);
+ $response->getBody()->write($json);
+
+ return $response->withHeader('Content-Type', 'application/json');
+});
+
// Register module routes
HealthRoutes::register($app);
AuthRoutes::register($app);