diff --git a/README.md b/README.md index 20a762f..daca451 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ tools/generate.bat product get post put delete # Atau langsung dengan Go go run tools/generate-handler.go product get post -go run tools/ generate-handler.go order get post put delete stats +go run tools/generate-handler.go order get post put delete stats ``` ### Method Tersedia diff --git a/docs/docs.go b/docs/docs.go index b560b2e..a262912 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -24,56 +24,6 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/v1/Order/{id}": { - "get": { - "description": "Returns a single order by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Order" - ], - "summary": "Get Order by ID", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Success response", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse" - } - }, - "400": { - "description": "Invalid ID format", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, "/api/v1/auth/login": { "post": { "description": "Authenticate user with username and password to receive JWT token", @@ -264,116 +214,9 @@ const docTemplate = `{ } } }, - "/api/v1/order/{id}": { - "put": { - "description": "Updates an existing order record", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Update order", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Order update request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "Order updated successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse" - } - }, - "400": { - "description": "Bad request or validation error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "Order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - }, - "delete": { - "description": "Soft deletes a order by setting status to 'deleted'", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Delete order", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Order deleted successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse" - } - }, - "400": { - "description": "Invalid ID format", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "Order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, - "/api/v1/orders": { + "/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": { "get": { - "description": "Returns a paginated list of orders with optional summary statistics", + "description": "Search participant data based on Population NIK and service date", "consumes": [ "application/json" ], @@ -381,142 +224,52 @@ const docTemplate = `{ "application/json" ], "tags": [ - "order" + "bpjs" ], - "summary": "Get order with pagination and optional aggregation", + "summary": "Get participant data by NIK", "parameters": [ { - "type": "integer", - "default": 10, - "description": "Limit (max 100)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "default": 0, - "description": "Offset", - "name": "offset", - "in": "query" - }, - { - "type": "boolean", - "default": false, - "description": "Include aggregation summary", - "name": "include_summary", - "in": "query" + "type": "string", + "description": "NIK KTP", + "name": "nik", + "in": "path", + "required": true }, { "type": "string", - "description": "Filter by status", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Search in multiple fields", - "name": "search", - "in": "query" + "description": "Service date/SEP date (format: yyyy-MM-dd)", + "name": "tglSEP", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "Success response", + "description": "Participant data", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "Bad request", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Participant not found", + "schema": { + "type": "object", + "additionalProperties": true } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - }, - "post": { - "description": "Creates a new order record", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Create order", - "parameters": [ - { - "description": "Order creation request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest" - } - } - ], - "responses": { - "201": { - "description": "Order created successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse" - } - }, - "400": { - "description": "Bad request or validation error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, - "/api/v1/orders/stats": { - "get": { - "description": "Returns comprehensive statistics about order data", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Get order statistics", - "parameters": [ - { - "type": "string", - "description": "Filter statistics by status", - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Statistics data", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.AggregateData" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" + "type": "object", + "additionalProperties": true } } } @@ -986,224 +739,6 @@ const docTemplate = `{ } } }, - "api-service_internal_models_order.AggregateData": { - "type": "object", - "properties": { - "by_status": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "created_today": { - "type": "integer" - }, - "last_updated": { - "type": "string" - }, - "total_active": { - "type": "integer" - }, - "total_draft": { - "type": "integer" - }, - "total_inactive": { - "type": "integer" - }, - "updated_today": { - "type": "integer" - } - } - }, - "api-service_internal_models_order.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "api-service_internal_models_order.MetaResponse": { - "type": "object", - "properties": { - "current_page": { - "type": "integer" - }, - "has_next": { - "type": "boolean" - }, - "has_prev": { - "type": "boolean" - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" - } - } - }, - "api-service_internal_models_order.NullableInt32": { - "type": "object", - "properties": { - "int32": { - "type": "integer" - }, - "valid": { - "type": "boolean" - } - } - }, - "api-service_internal_models_order.Order": { - "type": "object", - "properties": { - "date_created": { - "$ref": "#/definitions/sql.NullTime" - }, - "date_updated": { - "$ref": "#/definitions/sql.NullTime" - }, - "id": { - "type": "string" - }, - "name": { - "$ref": "#/definitions/sql.NullString" - }, - "sort": { - "$ref": "#/definitions/api-service_internal_models_order.NullableInt32" - }, - "status": { - "type": "string" - }, - "user_created": { - "$ref": "#/definitions/sql.NullString" - }, - "user_updated": { - "$ref": "#/definitions/sql.NullString" - } - } - }, - "api-service_internal_models_order.OrderCreateRequest": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "status": { - "type": "string", - "enum": [ - "draft", - "active", - "inactive" - ] - } - } - }, - "api-service_internal_models_order.OrderCreateResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderDeleteResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderGetByIDResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderGetResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - } - }, - "message": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/api-service_internal_models_order.MetaResponse" - }, - "summary": { - "$ref": "#/definitions/api-service_internal_models_order.AggregateData" - } - } - }, - "api-service_internal_models_order.OrderUpdateRequest": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "status": { - "type": "string", - "enum": [ - "draft", - "active", - "inactive" - ] - } - } - }, - "api-service_internal_models_order.OrderUpdateResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, "api-service_internal_models_retribusi.AggregateData": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 8056014..8777099 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -22,56 +22,6 @@ "host": "localhost:8080", "basePath": "/api/v1", "paths": { - "/api/v1/Order/{id}": { - "get": { - "description": "Returns a single order by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Order" - ], - "summary": "Get Order by ID", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Success response", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse" - } - }, - "400": { - "description": "Invalid ID format", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, "/api/v1/auth/login": { "post": { "description": "Authenticate user with username and password to receive JWT token", @@ -262,116 +212,9 @@ } } }, - "/api/v1/order/{id}": { - "put": { - "description": "Updates an existing order record", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Update order", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Order update request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest" - } - } - ], - "responses": { - "200": { - "description": "Order updated successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse" - } - }, - "400": { - "description": "Bad request or validation error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "Order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - }, - "delete": { - "description": "Soft deletes a order by setting status to 'deleted'", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Delete order", - "parameters": [ - { - "type": "string", - "description": "Order ID (UUID)", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "Order deleted successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse" - } - }, - "400": { - "description": "Invalid ID format", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "404": { - "description": "Order not found", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, - "/api/v1/orders": { + "/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": { "get": { - "description": "Returns a paginated list of orders with optional summary statistics", + "description": "Search participant data based on Population NIK and service date", "consumes": [ "application/json" ], @@ -379,142 +222,52 @@ "application/json" ], "tags": [ - "order" + "bpjs" ], - "summary": "Get order with pagination and optional aggregation", + "summary": "Get participant data by NIK", "parameters": [ { - "type": "integer", - "default": 10, - "description": "Limit (max 100)", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "default": 0, - "description": "Offset", - "name": "offset", - "in": "query" - }, - { - "type": "boolean", - "default": false, - "description": "Include aggregation summary", - "name": "include_summary", - "in": "query" + "type": "string", + "description": "NIK KTP", + "name": "nik", + "in": "path", + "required": true }, { "type": "string", - "description": "Filter by status", - "name": "status", - "in": "query" - }, - { - "type": "string", - "description": "Search in multiple fields", - "name": "search", - "in": "query" + "description": "Service date/SEP date (format: yyyy-MM-dd)", + "name": "tglSEP", + "in": "path", + "required": true } ], "responses": { "200": { - "description": "Success response", + "description": "Participant data", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse" + "type": "object", + "additionalProperties": true } }, "400": { "description": "Bad request", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" + "type": "object", + "additionalProperties": true + } + }, + "404": { + "description": "Participant not found", + "schema": { + "type": "object", + "additionalProperties": true } }, "500": { "description": "Internal server error", "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - }, - "post": { - "description": "Creates a new order record", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Create order", - "parameters": [ - { - "description": "Order creation request", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest" - } - } - ], - "responses": { - "201": { - "description": "Order created successfully", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse" - } - }, - "400": { - "description": "Bad request or validation error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" - } - } - } - } - }, - "/api/v1/orders/stats": { - "get": { - "description": "Returns comprehensive statistics about order data", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "order" - ], - "summary": "Get order statistics", - "parameters": [ - { - "type": "string", - "description": "Filter statistics by status", - "name": "status", - "in": "query" - } - ], - "responses": { - "200": { - "description": "Statistics data", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.AggregateData" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" + "type": "object", + "additionalProperties": true } } } @@ -984,224 +737,6 @@ } } }, - "api-service_internal_models_order.AggregateData": { - "type": "object", - "properties": { - "by_status": { - "type": "object", - "additionalProperties": { - "type": "integer" - } - }, - "created_today": { - "type": "integer" - }, - "last_updated": { - "type": "string" - }, - "total_active": { - "type": "integer" - }, - "total_draft": { - "type": "integer" - }, - "total_inactive": { - "type": "integer" - }, - "updated_today": { - "type": "integer" - } - } - }, - "api-service_internal_models_order.ErrorResponse": { - "type": "object", - "properties": { - "code": { - "type": "integer" - }, - "error": { - "type": "string" - }, - "message": { - "type": "string" - }, - "timestamp": { - "type": "string" - } - } - }, - "api-service_internal_models_order.MetaResponse": { - "type": "object", - "properties": { - "current_page": { - "type": "integer" - }, - "has_next": { - "type": "boolean" - }, - "has_prev": { - "type": "boolean" - }, - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - }, - "total_pages": { - "type": "integer" - } - } - }, - "api-service_internal_models_order.NullableInt32": { - "type": "object", - "properties": { - "int32": { - "type": "integer" - }, - "valid": { - "type": "boolean" - } - } - }, - "api-service_internal_models_order.Order": { - "type": "object", - "properties": { - "date_created": { - "$ref": "#/definitions/sql.NullTime" - }, - "date_updated": { - "$ref": "#/definitions/sql.NullTime" - }, - "id": { - "type": "string" - }, - "name": { - "$ref": "#/definitions/sql.NullString" - }, - "sort": { - "$ref": "#/definitions/api-service_internal_models_order.NullableInt32" - }, - "status": { - "type": "string" - }, - "user_created": { - "$ref": "#/definitions/sql.NullString" - }, - "user_updated": { - "$ref": "#/definitions/sql.NullString" - } - } - }, - "api-service_internal_models_order.OrderCreateRequest": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "status": { - "type": "string", - "enum": [ - "draft", - "active", - "inactive" - ] - } - } - }, - "api-service_internal_models_order.OrderCreateResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderDeleteResponse": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderGetByIDResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, - "api-service_internal_models_order.OrderGetResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - } - }, - "message": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/api-service_internal_models_order.MetaResponse" - }, - "summary": { - "$ref": "#/definitions/api-service_internal_models_order.AggregateData" - } - } - }, - "api-service_internal_models_order.OrderUpdateRequest": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "name": { - "type": "string", - "maxLength": 255, - "minLength": 1 - }, - "status": { - "type": "string", - "enum": [ - "draft", - "active", - "inactive" - ] - } - } - }, - "api-service_internal_models_order.OrderUpdateResponse": { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/api-service_internal_models_order.Order" - }, - "message": { - "type": "string" - } - } - }, "api-service_internal_models_retribusi.AggregateData": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 850f0fd..75742f3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -30,150 +30,6 @@ definitions: username: type: string type: object - api-service_internal_models_order.AggregateData: - properties: - by_status: - additionalProperties: - type: integer - type: object - created_today: - type: integer - last_updated: - type: string - total_active: - type: integer - total_draft: - type: integer - total_inactive: - type: integer - updated_today: - type: integer - type: object - api-service_internal_models_order.ErrorResponse: - properties: - code: - type: integer - error: - type: string - message: - type: string - timestamp: - type: string - type: object - api-service_internal_models_order.MetaResponse: - properties: - current_page: - type: integer - has_next: - type: boolean - has_prev: - type: boolean - limit: - type: integer - offset: - type: integer - total: - type: integer - total_pages: - type: integer - type: object - api-service_internal_models_order.NullableInt32: - properties: - int32: - type: integer - valid: - type: boolean - type: object - api-service_internal_models_order.Order: - properties: - date_created: - $ref: '#/definitions/sql.NullTime' - date_updated: - $ref: '#/definitions/sql.NullTime' - id: - type: string - name: - $ref: '#/definitions/sql.NullString' - sort: - $ref: '#/definitions/api-service_internal_models_order.NullableInt32' - status: - type: string - user_created: - $ref: '#/definitions/sql.NullString' - user_updated: - $ref: '#/definitions/sql.NullString' - type: object - api-service_internal_models_order.OrderCreateRequest: - properties: - name: - maxLength: 255 - minLength: 1 - type: string - status: - enum: - - draft - - active - - inactive - type: string - required: - - status - type: object - api-service_internal_models_order.OrderCreateResponse: - properties: - data: - $ref: '#/definitions/api-service_internal_models_order.Order' - message: - type: string - type: object - api-service_internal_models_order.OrderDeleteResponse: - properties: - id: - type: string - message: - type: string - type: object - api-service_internal_models_order.OrderGetByIDResponse: - properties: - data: - $ref: '#/definitions/api-service_internal_models_order.Order' - message: - type: string - type: object - api-service_internal_models_order.OrderGetResponse: - properties: - data: - items: - $ref: '#/definitions/api-service_internal_models_order.Order' - type: array - message: - type: string - meta: - $ref: '#/definitions/api-service_internal_models_order.MetaResponse' - summary: - $ref: '#/definitions/api-service_internal_models_order.AggregateData' - type: object - api-service_internal_models_order.OrderUpdateRequest: - properties: - name: - maxLength: 255 - minLength: 1 - type: string - status: - enum: - - draft - - active - - inactive - type: string - required: - - status - type: object - api-service_internal_models_order.OrderUpdateResponse: - properties: - data: - $ref: '#/definitions/api-service_internal_models_order.Order' - message: - type: string - type: object api-service_internal_models_retribusi.AggregateData: properties: by_dinas: @@ -466,39 +322,6 @@ info: title: API Service version: 1.0.0 paths: - /api/v1/Order/{id}: - get: - consumes: - - application/json - description: Returns a single order by ID - parameters: - - description: Order ID (UUID) - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Success response - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderGetByIDResponse' - "400": - description: Invalid ID format - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "404": - description: order not found - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Get Order by ID - tags: - - Order /api/v1/auth/login: post: consumes: @@ -622,177 +445,48 @@ paths: summary: Register new user tags: - Authentication - /api/v1/order/{id}: - delete: - consumes: - - application/json - description: Soft deletes a order by setting status to 'deleted' - parameters: - - description: Order ID (UUID) - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: Order deleted successfully - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderDeleteResponse' - "400": - description: Invalid ID format - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "404": - description: Order not found - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Delete order - tags: - - order - put: - consumes: - - application/json - description: Updates an existing order record - parameters: - - description: Order ID (UUID) - in: path - name: id - required: true - type: string - - description: Order update request - in: body - name: request - required: true - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderUpdateRequest' - produces: - - application/json - responses: - "200": - description: Order updated successfully - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderUpdateResponse' - "400": - description: Bad request or validation error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "404": - description: Order not found - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Update order - tags: - - order - /api/v1/orders: + /api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}: get: consumes: - application/json - description: Returns a paginated list of orders with optional summary statistics + description: Search participant data based on Population NIK and service date parameters: - - default: 10 - description: Limit (max 100) - in: query - name: limit - type: integer - - default: 0 - description: Offset - in: query - name: offset - type: integer - - default: false - description: Include aggregation summary - in: query - name: include_summary - type: boolean - - description: Filter by status - in: query - name: status + - description: NIK KTP + in: path + name: nik + required: true type: string - - description: Search in multiple fields - in: query - name: search + - description: 'Service date/SEP date (format: yyyy-MM-dd)' + in: path + name: tglSEP + required: true type: string produces: - application/json responses: "200": - description: Success response + description: Participant data schema: - $ref: '#/definitions/api-service_internal_models_order.OrderGetResponse' + additionalProperties: true + type: object "400": description: Bad request schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' + additionalProperties: true + type: object + "404": + description: Participant not found + schema: + additionalProperties: true + type: object "500": description: Internal server error schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Get order with pagination and optional aggregation + additionalProperties: true + type: object + summary: Get participant data by NIK tags: - - order - post: - consumes: - - application/json - description: Creates a new order record - parameters: - - description: Order creation request - in: body - name: request - required: true - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderCreateRequest' - produces: - - application/json - responses: - "201": - description: Order created successfully - schema: - $ref: '#/definitions/api-service_internal_models_order.OrderCreateResponse' - "400": - description: Bad request or validation error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - "500": - description: Internal server error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Create order - tags: - - order - /api/v1/orders/stats: - get: - consumes: - - application/json - description: Returns comprehensive statistics about order data - parameters: - - description: Filter statistics by status - in: query - name: status - type: string - produces: - - application/json - responses: - "200": - description: Statistics data - schema: - $ref: '#/definitions/api-service_internal_models_order.AggregateData' - "500": - description: Internal server error - schema: - $ref: '#/definitions/api-service_internal_models_order.ErrorResponse' - summary: Get order statistics - tags: - - order + - bpjs /api/v1/retribusi/{id}: delete: consumes: diff --git a/example.env b/example.env index 547d2fe..ed71be4 100644 --- a/example.env +++ b/example.env @@ -63,3 +63,9 @@ KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs KEYCLOAK_ENABLED=false + +# BPJS Configuration +BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest +BPJS_CONSID=5257 +BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482 +BPJS_SECRETKEY=1bV363512D \ No newline at end of file diff --git a/go.mod b/go.mod index 80e41d6..51a2d61 100644 --- a/go.mod +++ b/go.mod @@ -16,8 +16,10 @@ require ( ) require ( + github.com/go-playground/validator/v10 v10.27.0 github.com/go-sql-driver/mysql v1.8.1 github.com/joho/godotenv v1.5.1 + github.com/mashingan/smapping v0.1.19 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.6 @@ -39,7 +41,6 @@ require ( github.com/go-openapi/swag v0.19.15 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect diff --git a/go.sum b/go.sum index 9db4dd3..0ee92fd 100644 --- a/go.sum +++ b/go.sum @@ -132,6 +132,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ= +github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= diff --git a/internal/config/config.go b/internal/config/config.go index fd94038..efa806b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,6 +1,11 @@ package config import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" "log" "os" "strconv" @@ -9,10 +14,11 @@ import ( ) type Config struct { - Server ServerConfig - Databases map[string]DatabaseConfig + Server ServerConfig + Databases map[string]DatabaseConfig ReadReplicas map[string][]DatabaseConfig // For read replicas - Keycloak KeycloakConfig + Keycloak KeycloakConfig + Bpjs BpjsConfig } type ServerConfig struct { @@ -21,20 +27,20 @@ type ServerConfig struct { } type DatabaseConfig struct { - Name string - Type string // postgres, mysql, sqlserver, sqlite, mongodb - Host string - Port int - Username string - Password string - Database string - Schema string - SSLMode string - Path string // For SQLite - Options string // Additional connection options - MaxOpenConns int // Max open connections - MaxIdleConns int // Max idle connections - ConnMaxLifetime time.Duration // Connection max lifetime + Name string + Type string // postgres, mysql, sqlserver, sqlite, mongodb + Host string + Port int + Username string + Password string + Database string + Schema string + SSLMode string + Path string // For SQLite + Options string // Additional connection options + MaxOpenConns int // Max open connections + MaxIdleConns int // Max idle connections + ConnMaxLifetime time.Duration // Connection max lifetime } type KeycloakConfig struct { @@ -44,6 +50,52 @@ type KeycloakConfig struct { Enabled bool } +type BpjsConfig struct { + BaseURL string `json:"base_url"` + ConsID string `json:"cons_id"` + UserKey string `json:"user_key"` + SecretKey string `json:"secret_key"` + Timeout time.Duration `json:"timeout"` +} + +// SetHeader generates required headers for BPJS VClaim API +func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) { + timenow := time.Now().UTC() + t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z") + if err != nil { + log.Fatal(err) + } + + tstamp := timenow.Unix() - t.Unix() + secret := []byte(cfg.SecretKey) + message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp)) + hash := hmac.New(sha256.New, secret) + hash.Write(message) + + // to lowercase hexits + hex.EncodeToString(hash.Sum(nil)) + // to base64 + xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature +} + +type ConfigBpjs struct { + Cons_id string + Secret_key string + User_key string +} + +// SetHeader for backward compatibility +func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) { + bpjsConfig := BpjsConfig{ + ConsID: cfg.Cons_id, + SecretKey: cfg.Secret_key, + UserKey: cfg.User_key, + } + return bpjsConfig.SetHeader() +} + func LoadConfig() *Config { config := &Config{ Server: ServerConfig{ @@ -58,11 +110,18 @@ func LoadConfig() *Config { JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"), Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true), }, + Bpjs: BpjsConfig{ + BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"), + ConsID: getEnv("BPJS_CONSID", ""), + UserKey: getEnv("BPJS_USERKEY", ""), + SecretKey: getEnv("BPJS_SECRETKEY", ""), + Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")), + }, } // Load database configurations config.loadDatabaseConfigs() - + // Load read replica configurations config.loadReadReplicaConfigs() @@ -72,21 +131,21 @@ func LoadConfig() *Config { func (c *Config) loadDatabaseConfigs() { // Simplified approach: Directly load from environment variables // This ensures we get the exact values specified in .env - + // Primary database configuration c.Databases["default"] = DatabaseConfig{ - Name: "default", - Type: getEnv("DB_CONNECTION", "postgres"), - Host: getEnv("DB_HOST", "localhost"), - Port: getEnvAsInt("DB_PORT", 5432), - Username: getEnv("DB_USERNAME", ""), - Password: getEnv("DB_PASSWORD", ""), - Database: getEnv("DB_DATABASE", "satu_db"), - Schema: getEnv("DB_SCHEMA", "public"), - SSLMode: getEnv("DB_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25), - MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25), - ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")), + Name: "default", + Type: getEnv("DB_CONNECTION", "postgres"), + Host: getEnv("DB_HOST", "localhost"), + Port: getEnvAsInt("DB_PORT", 5432), + Username: getEnv("DB_USERNAME", ""), + Password: getEnv("DB_PASSWORD", ""), + Database: getEnv("DB_DATABASE", "satu_db"), + Schema: getEnv("DB_SCHEMA", "public"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")), } // SATUDATA database configuration @@ -110,16 +169,16 @@ func (c *Config) loadDatabaseConfigs() { value := parts[1] // Parse specific database configurations - if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") || - strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") || - strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") || - strings.HasSuffix(key, "_NAME") { - + if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") || + strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") || + strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") || + strings.HasSuffix(key, "_NAME") { + segments := strings.Split(key, "_") if len(segments) >= 2 { dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_")) property := strings.ToLower(segments[len(segments)-1]) - + if dbConfigs[dbName] == nil { dbConfigs[dbName] = make(map[string]string) } @@ -131,25 +190,25 @@ func (c *Config) loadDatabaseConfigs() { // Create DatabaseConfig from parsed configurations for additional databases for name, config := range dbConfigs { // Skip empty configurations or system configurations - if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" { + if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" { continue } dbConfig := DatabaseConfig{ - Name: name, - Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")), - Host: getEnvFromMap(config, "host", "localhost"), - Port: getEnvAsIntFromMap(config, "port", 5432), - Username: getEnvFromMap(config, "username", ""), - Password: getEnvFromMap(config, "password", ""), - Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)), - Schema: getEnvFromMap(config, "schema", "public"), - SSLMode: getEnvFromMap(config, "sslmode", "disable"), - Path: getEnvFromMap(config, "path", ""), - Options: getEnvFromMap(config, "options", ""), - MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25), - MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25), - ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")), + Name: name, + Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")), + Host: getEnvFromMap(config, "host", "localhost"), + Port: getEnvAsIntFromMap(config, "port", 5432), + Username: getEnvFromMap(config, "username", ""), + Password: getEnvFromMap(config, "password", ""), + Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)), + Schema: getEnvFromMap(config, "schema", "public"), + SSLMode: getEnvFromMap(config, "sslmode", "disable"), + Path: getEnvFromMap(config, "path", ""), + Options: getEnvFromMap(config, "options", ""), + MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25), + MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25), + ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")), } // Skip if username is empty and it's not a system config @@ -182,7 +241,7 @@ func (c *Config) loadReadReplicaConfigs() { property := strings.ToLower(strings.Join(segments[4:], "_")) replicaKey := dbName + "_replica_" + replicaIndex - + if c.ReadReplicas[dbName] == nil { c.ReadReplicas[dbName] = []DatabaseConfig{} } @@ -199,18 +258,18 @@ func (c *Config) loadReadReplicaConfigs() { if replicaConfig == nil { // Create new replica config newConfig := DatabaseConfig{ - Name: replicaKey, - Type: c.Databases[dbName].Type, - Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host), - Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port), - Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username), - Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password), - Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database), - Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema), - SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode), - MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns), - MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns), - ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")), + Name: replicaKey, + Type: c.Databases[dbName].Type, + Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host), + Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port), + Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username), + Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password), + Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database), + Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema), + SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode), + MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns), + MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns), + ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")), } c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig) replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1] @@ -249,167 +308,167 @@ func (c *Config) addSpecificDatabase(prefix, defaultType string) { host := getEnv(strings.ToUpper(prefix)+"_HOST", "") if host != "" { dbConfig := DatabaseConfig{ - Name: prefix, - Type: connection, - Host: host, - Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432), - Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""), - Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""), - Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)), - Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"), - SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25), - MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25), - ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")), + Name: prefix, + Type: connection, + Host: host, + Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432), + Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""), + Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""), + Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)), + Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"), + SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")), } c.Databases[prefix] = dbConfig } } + // PostgreSQL database func (c *Config) addPostgreSQLConfigs() { - // SATUDATA database configuration - // defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost") - // if defaultPOSTGRESHost != "" { - // c.Databases["postgres"] = DatabaseConfig{ - // Name: "postgres", - // Type: getEnv("POSTGRES_CONNECTION", "postgres"), - // Host: defaultPOSTGRESHost, - // Port: getEnvAsInt("POSTGRES_PORT", 5432), - // Username: getEnv("POSTGRES_USERNAME", ""), - // Password: getEnv("POSTGRES_PASSWORD", ""), - // Database: getEnv("POSTGRES_DATABASE", "postgres"), - // Schema: getEnv("POSTGRES_SCHEMA", "public"), - // SSLMode: getEnv("POSTGRES_SSLMODE", "disable"), - // MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), - // MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), - // ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), - // } - // } + // SATUDATA database configuration + // defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost") + // if defaultPOSTGRESHost != "" { + // c.Databases["postgres"] = DatabaseConfig{ + // Name: "postgres", + // Type: getEnv("POSTGRES_CONNECTION", "postgres"), + // Host: defaultPOSTGRESHost, + // Port: getEnvAsInt("POSTGRES_PORT", 5432), + // Username: getEnv("POSTGRES_USERNAME", ""), + // Password: getEnv("POSTGRES_PASSWORD", ""), + // Database: getEnv("POSTGRES_DATABASE", "postgres"), + // Schema: getEnv("POSTGRES_SCHEMA", "public"), + // SSLMode: getEnv("POSTGRES_SSLMODE", "disable"), + // MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), + // MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), + // ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), + // } + // } - // Support for custom PostgreSQL configurations with POSTGRES_ prefix - envVars := os.Environ() - for _, envVar := range envVars { - parts := strings.SplitN(envVar, "=", 2) - if len(parts) != 2 { - continue - } + // Support for custom PostgreSQL configurations with POSTGRES_ prefix + envVars := os.Environ() + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } - key := parts[0] - // Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY]) - if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") { - segments := strings.Split(key, "_") - if len(segments) >= 3 { - dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) - - // Skip if it's a standard PostgreSQL configuration - if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" { - continue - } + key := parts[0] + // Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY]) + if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") { + segments := strings.Split(key, "_") + if len(segments) >= 3 { + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) - // Create or update PostgreSQL configuration - if _, exists := c.Databases[dbName]; !exists { - c.Databases[dbName] = DatabaseConfig{ - Name: dbName, - Type: "postgres", - Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"), - Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432), - Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""), - Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""), - Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName), - Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"), - SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), - MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), - ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), - } - } - } - } - } + // Skip if it's a standard PostgreSQL configuration + if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" { + continue + } + + // Create or update PostgreSQL configuration + if _, exists := c.Databases[dbName]; !exists { + c.Databases[dbName] = DatabaseConfig{ + Name: dbName, + Type: "postgres", + Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"), + Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432), + Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""), + Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""), + Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName), + Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"), + SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), + } + } + } + } + } } // addMYSQLConfigs adds MYSQL database func (c *Config) addMySQLConfigs() { - // Primary MySQL configuration - defaultMySQLHost := getEnv("MYSQL_HOST", "") - if defaultMySQLHost != "" { - c.Databases["mysql"] = DatabaseConfig{ - Name: "mysql", - Type: getEnv("MYSQL_CONNECTION", "mysql"), - Host: defaultMySQLHost, - Port: getEnvAsInt("MYSQL_PORT", 3306), - Username: getEnv("MYSQL_USERNAME", ""), - Password: getEnv("MYSQL_PASSWORD", ""), - Database: getEnv("MYSQL_DATABASE", "mysql"), - SSLMode: getEnv("MYSQL_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), - MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), - ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), - } - } + // Primary MySQL configuration + defaultMySQLHost := getEnv("MYSQL_HOST", "") + if defaultMySQLHost != "" { + c.Databases["mysql"] = DatabaseConfig{ + Name: "mysql", + Type: getEnv("MYSQL_CONNECTION", "mysql"), + Host: defaultMySQLHost, + Port: getEnvAsInt("MYSQL_PORT", 3306), + Username: getEnv("MYSQL_USERNAME", ""), + Password: getEnv("MYSQL_PASSWORD", ""), + Database: getEnv("MYSQL_DATABASE", "mysql"), + SSLMode: getEnv("MYSQL_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), + } + } - // Support for custom MySQL configurations with MYSQL_ prefix - envVars := os.Environ() - for _, envVar := range envVars { - parts := strings.SplitN(envVar, "=", 2) - if len(parts) != 2 { - continue - } + // Support for custom MySQL configurations with MYSQL_ prefix + envVars := os.Environ() + for _, envVar := range envVars { + parts := strings.SplitN(envVar, "=", 2) + if len(parts) != 2 { + continue + } - key := parts[0] - // Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY]) - if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") { - segments := strings.Split(key, "_") - if len(segments) >= 3 { - dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) - - // Skip if it's a standard MySQL configuration - if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" { - continue - } + key := parts[0] + // Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY]) + if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") { + segments := strings.Split(key, "_") + if len(segments) >= 3 { + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) - // Create or update MySQL configuration - if _, exists := c.Databases[dbName]; !exists { - mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "") - if mysqlHost != "" { - c.Databases[dbName] = DatabaseConfig{ - Name: dbName, - Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"), - Host: mysqlHost, - Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306), - Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""), - Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""), - Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName), - SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), - MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), - ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), - } - } - } - } - } - } + // Skip if it's a standard MySQL configuration + if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" { + continue + } + + // Create or update MySQL configuration + if _, exists := c.Databases[dbName]; !exists { + mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "") + if mysqlHost != "" { + c.Databases[dbName] = DatabaseConfig{ + Name: dbName, + Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"), + Host: mysqlHost, + Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306), + Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""), + Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""), + Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName), + SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), + MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), + ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), + } + } + } + } + } + } } - // addMongoDBConfigs adds MongoDB database configurations from environment variables func (c *Config) addMongoDBConfigs() { // Primary MongoDB configuration mongoHost := getEnv("MONGODB_HOST", "") if mongoHost != "" { c.Databases["mongodb"] = DatabaseConfig{ - Name: "mongodb", - Type: getEnv("MONGODB_CONNECTION", "mongodb"), - Host: mongoHost, - Port: getEnvAsInt("MONGODB_PORT", 27017), - Username: getEnv("MONGODB_USER", ""), - Password: getEnv("MONGODB_PASS", ""), - Database: getEnv("MONGODB_MASTER", "master"), - SSLMode: getEnv("MONGODB_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), - MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), - ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + Name: "mongodb", + Type: getEnv("MONGODB_CONNECTION", "mongodb"), + Host: mongoHost, + Port: getEnvAsInt("MONGODB_PORT", 27017), + Username: getEnv("MONGODB_USER", ""), + Password: getEnv("MONGODB_PASS", ""), + Database: getEnv("MONGODB_MASTER", "master"), + SSLMode: getEnv("MONGODB_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), } } @@ -417,17 +476,17 @@ func (c *Config) addMongoDBConfigs() { mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "") if mongoLocalHost != "" { c.Databases["mongodb_local"] = DatabaseConfig{ - Name: "mongodb_local", - Type: getEnv("MONGODB_CONNECTION", "mongodb"), - Host: mongoLocalHost, - Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017), - Username: getEnv("MONGODB_LOCAL_USER", ""), - Password: getEnv("MONGODB_LOCAL_PASS", ""), - Database: getEnv("MONGODB_LOCAL_DB", "local"), - SSLMode: getEnv("MONGOD_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), - MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), - ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + Name: "mongodb_local", + Type: getEnv("MONGODB_CONNECTION", "mongodb"), + Host: mongoLocalHost, + Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017), + Username: getEnv("MONGODB_LOCAL_USER", ""), + Password: getEnv("MONGODB_LOCAL_PASS", ""), + Database: getEnv("MONGODB_LOCAL_DB", "local"), + SSLMode: getEnv("MONGOD_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), } } @@ -444,7 +503,7 @@ func (c *Config) addMongoDBConfigs() { if strings.HasPrefix(key, "MONGODB_") && strings.Contains(key, "_") { segments := strings.Split(key, "_") if len(segments) >= 3 { - dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) + dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) // Skip if it's a standard MongoDB configuration if dbName == "connection" || dbName == "dev" || dbName == "local" { continue @@ -453,17 +512,17 @@ func (c *Config) addMongoDBConfigs() { // Create or update MongoDB configuration if _, exists := c.Databases[dbName]; !exists { c.Databases[dbName] = DatabaseConfig{ - Name: dbName, - Type: "mongodb", - Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"), - Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017), - Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""), - Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""), - Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName), - SSLMode: getEnv("MONGOD_SSLMODE", "disable"), - MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), - MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), - ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), + Name: dbName, + Type: "mongodb", + Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"), + Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017), + Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""), + Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""), + Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName), + SSLMode: getEnv("MONGOD_SSLMODE", "disable"), + MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), + MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), + ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), } } } @@ -537,6 +596,19 @@ func (c *Config) Validate() error { } } + if c.Bpjs.BaseURL == "" { + log.Fatal("BPJS Base URL is required") + } + if c.Bpjs.ConsID == "" { + log.Fatal("BPJS Consumer ID is required") + } + if c.Bpjs.UserKey == "" { + log.Fatal("BPJS User Key is required") + } + if c.Bpjs.SecretKey == "" { + log.Fatal("BPJS Secret Key is required") + } + // Validate Keycloak configuration if enabled if c.Keycloak.Enabled { if c.Keycloak.Issuer == "" { diff --git a/internal/handlers/bpjs/peserta.go b/internal/handlers/bpjs/peserta.go new file mode 100644 index 0000000..2071476 --- /dev/null +++ b/internal/handlers/bpjs/peserta.go @@ -0,0 +1,92 @@ +package handlers + +import ( + "context" + "fmt" + "net/http" + "time" + + "api-service/internal/config" + services "api-service/internal/services/bpjs" + + "github.com/gin-gonic/gin" +) + +// PesertaHandler handles BPJS participant operations +type PesertaHandler struct { + bpjsService services.VClaimService +} + +// NewPesertaHandler creates a new PesertaHandler instance +func NewPesertaHandler(cfg config.BpjsConfig) *PesertaHandler { + return &PesertaHandler{ + bpjsService: services.NewService(cfg), + } +} + +// GetPesertaByNIK godoc +// @Summary Get participant data by NIK +// @Description Search participant data based on Population NIK and service date +// @Tags bpjs +// @Accept json +// @Produce json +// @Param nik path string true "NIK KTP" +// @Param tglSEP path string true "Service date/SEP date (format: yyyy-MM-dd)" +// @Success 200 {object} map[string]interface{} "Participant data" +// @Failure 400 {object} map[string]interface{} "Bad request" +// @Failure 404 {object} map[string]interface{} "Participant not found" +// @Failure 500 {object} map[string]interface{} "Internal server error" +// @Router /api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP} [get] +func (h *PesertaHandler) GetPesertaByNIK(c *gin.Context) { + nik := c.Param("nik") + tglSEP := c.Param("tglSEP") + + // Validate parameters + if nik == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "NIK parameter is required", + "message": "NIK KTP tidak boleh kosong", + }) + return + } + + if tglSEP == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "tglSEP parameter is required", + "message": "Tanggal SEP tidak boleh kosong", + }) + return + } + + // Validate date format + if _, err := time.Parse("2006-01-02", tglSEP); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid date format", + "message": "Format tanggal harus yyyy-MM-dd", + }) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) + defer cancel() + + // Build endpoint URL + endpoint := fmt.Sprintf("/Peserta/nik/%s/tglSEP/%s", nik, tglSEP) + + // Call BPJS service + var result map[string]interface{} + if err := h.bpjsService.Get(ctx, endpoint, &result); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to fetch participant data", + "message": err.Error(), + }) + return + } + + // Return successful response + c.JSON(http.StatusOK, gin.H{ + "message": "Data peserta berhasil diambil", + "data": result, + }) +} diff --git a/internal/handlers/order/order.go b/internal/handlers/order/order.go deleted file mode 100644 index 7679e6c..0000000 --- a/internal/handlers/order/order.go +++ /dev/null @@ -1,683 +0,0 @@ -package handlers - -import ( - "api-service/internal/config" - "api-service/internal/database" - models "api-service/internal/models/order" - "context" - "database/sql" - "fmt" - "log" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" -) - -var ( - db database.Service - once sync.Once - validate *validator.Validate -) - -// Initialize the database connection and validator -func init() { - once.Do(func() { - db = database.New(config.LoadConfig()) - validate = validator.New() - // Register custom validations if needed - validate.RegisterValidation("order_status", validateOrderStatus) - if db == nil { - log.Fatal("Failed to initialize database connection") - } - }) -} - -// Custom validation for order status -func validateOrderStatus(fl validator.FieldLevel) bool { - return models.IsValidStatus(fl.Field().String()) -} - -// OrderHandler handles order services -type OrderHandler struct { - db database.Service -} - -// NewOrderHandler creates a new OrderHandler -func NewOrderHandler() *OrderHandler { - return &OrderHandler{ - db: db, - } -} - -// GetOrder godoc -// @Summary Get order with pagination and optional aggregation -// @Description Returns a paginated list of orders with optional summary statistics -// @Tags order -// @Accept json -// @Produce json -// @Param limit query int false "Limit (max 100)" default(10) -// @Param offset query int false "Offset" default(0) -// @Param include_summary query bool false "Include aggregation summary" default(false) -// @Param status query string false "Filter by status" -// @Param search query string false "Search in multiple fields" -// @Success 200 {object} models.OrderGetResponse "Success response" -// @Failure 400 {object} models.ErrorResponse "Bad request" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/orders [get] -func (h *OrderHandler) GetOrder(c *gin.Context) { - // Parse pagination parameters - limit, offset, err := h.parsePaginationParams(c) - if err != nil { - h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) - return - } - - // Parse filter parameters - filter := h.parseFilterParams(c) - includeAggregation := c.Query("include_summary") == "true" - - // Get database connection - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - // Create context with timeout - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() - - // Execute concurrent operations - var ( - items []models.Order - total int - aggregateData *models.AggregateData - wg sync.WaitGroup - errChan = make(chan error, 3) - mu sync.Mutex - ) - - // Fetch total count - wg.Add(1) - go func() { - defer wg.Done() - if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { - mu.Lock() - errChan <- fmt.Errorf("failed to get total count: %w", err) - mu.Unlock() - } - }() - - // Fetch main data - FIXED: Proper method name - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.fetchOrders(ctx, dbConn, filter, limit, offset) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to fetch data: %w", err) - } else { - items = result - } - mu.Unlock() - }() - - // Fetch aggregation data if requested - if includeAggregation { - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.getAggregateData(ctx, dbConn, filter) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to get aggregate data: %w", err) - } else { - aggregateData = result - } - mu.Unlock() - }() - } - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) - return - } - } - - // Build response - meta := h.calculateMeta(limit, offset, total) - response := models.OrderGetResponse{ - Message: "Data order berhasil diambil", - Data: items, - Meta: meta, - } - - if includeAggregation && aggregateData != nil { - response.Summary = aggregateData - } - - c.JSON(http.StatusOK, response) -} - -// GetOrderByID godoc -// @Summary Get Order by ID -// @Description Returns a single order by ID -// @Tags order -// @Accept json -// @Produce json -// @Param id path string true "Order ID (UUID)" -// @Success 200 {object} models.OrderGetByIDResponse "Success response" -// @Failure 400 {object} models.ErrorResponse "Invalid ID format" -// @Failure 404 {object} models.ErrorResponse "Order not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/order/{id} [get] -func (h *OrderHandler) GetOrderByID(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.getOrderByID(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Order not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to get order", err, http.StatusInternalServerError) - } - return - } - - response := models.OrderGetByIDResponse{ - Message: "Order details retrieved successfully", - Data: item, - } - - c.JSON(http.StatusOK, response) -} - -// CreateOrder godoc -// @Summary Create order -// @Description Creates a new order record -// @Tags order -// @Accept json -// @Produce json -// @Param request body models.OrderCreateRequest true "Order creation request" -// @Success 201 {object} models.OrderCreateResponse "Order created successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/orders [post] -func (h *OrderHandler) CreateOrder(c *gin.Context) { - var req models.OrderCreateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } - - // Validate request - if err := validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.createOrder(ctx, dbConn, &req) - if err != nil { - h.logAndRespondError(c, "Failed to create order", err, http.StatusInternalServerError) - return - } - - response := models.OrderCreateResponse{ - Message: "Order berhasil dibuat", - Data: item, - } - - c.JSON(http.StatusCreated, response) -} - -// UpdateOrder godoc -// @Summary Update order -// @Description Updates an existing order record -// @Tags order -// @Accept json -// @Produce json -// @Param id path string true "Order ID (UUID)" -// @Param request body models.OrderUpdateRequest true "Order update request" -// @Success 200 {object} models.OrderUpdateResponse "Order updated successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" -// @Failure 404 {object} models.ErrorResponse "Order not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/order/{id} [put] -func (h *OrderHandler) UpdateOrder(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - var req models.OrderUpdateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } - - // Set ID from path parameter - req.ID = id - - // Validate request - if err := validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.updateOrder(ctx, dbConn, &req) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Order not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to update order", err, http.StatusInternalServerError) - } - return - } - - response := models.OrderUpdateResponse{ - Message: "Order berhasil diperbarui", - Data: item, - } - - c.JSON(http.StatusOK, response) -} - -// DeleteOrder godoc -// @Summary Delete order -// @Description Soft deletes a order by setting status to 'deleted' -// @Tags order -// @Accept json -// @Produce json -// @Param id path string true "Order ID (UUID)" -// @Success 200 {object} models.OrderDeleteResponse "Order deleted successfully" -// @Failure 400 {object} models.ErrorResponse "Invalid ID format" -// @Failure 404 {object} models.ErrorResponse "Order not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/order/{id} [delete] -func (h *OrderHandler) DeleteOrder(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - err = h.deleteOrder(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Order not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to delete order", err, http.StatusInternalServerError) - } - return - } - - response := models.OrderDeleteResponse{ - Message: "Order berhasil dihapus", - ID: id, - } - - c.JSON(http.StatusOK, response) -} - -// GetOrderStats godoc -// @Summary Get order statistics -// @Description Returns comprehensive statistics about order data -// @Tags order -// @Accept json -// @Produce json -// @Param status query string false "Filter statistics by status" -// @Success 200 {object} models.AggregateData "Statistics data" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/orders/stats [get] -func (h *OrderHandler) GetOrderStats(c *gin.Context) { - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - filter := h.parseFilterParams(c) - aggregateData, err := h.getAggregateData(ctx, dbConn, filter) - if err != nil { - h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Statistik order berhasil diambil", - "data": aggregateData, - }) -} - -// Database operations -func (h *OrderHandler) getOrderByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Order, error) { - query := "SELECT id, status, date_created, date_updated, name FROM data_order WHERE id = $1 AND status != 'deleted'" - row := dbConn.QueryRowContext(ctx, query, id) - - var item models.Order - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, err - } - - return &item, nil -} - -func (h *OrderHandler) createOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderCreateRequest) (*models.Order, error) { - id := uuid.New().String() - now := time.Now() - - query := "INSERT INTO data_order (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) - - var item models.Order - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to create order: %w", err) - } - - return &item, nil -} - -func (h *OrderHandler) updateOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderUpdateRequest) (*models.Order, error) { - now := time.Now() - - query := "UPDATE data_order SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) - - var item models.Order - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to update order: %w", err) - } - - return &item, nil -} - -func (h *OrderHandler) deleteOrder(ctx context.Context, dbConn *sql.DB, id string) error { - now := time.Now() - query := "UPDATE data_order SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" - result, err := dbConn.ExecContext(ctx, query, id, now) - if err != nil { - return fmt.Errorf("failed to delete order: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %w", err) - } - - if rowsAffected == 0 { - return sql.ErrNoRows - } - - return nil -} - -func (h *OrderHandler) fetchOrders(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, limit, offset int) ([]models.Order, error) { - whereClause, args := h.buildWhereClause(filter) - query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_order WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) - - args = append(args, limit, offset) - rows, err := dbConn.QueryContext(ctx, query, args...) - if err != nil { - return nil, fmt.Errorf("fetch orders query failed: %w", err) - } - defer rows.Close() - - items := make([]models.Order, 0, limit) - for rows.Next() { - var item models.Order - err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("scan Order failed: %w", err) - } - items = append(items, item) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("rows iteration error: %w", err) - } - - return items, nil -} - -func (h *OrderHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, total *int) error { - whereClause, args := h.buildWhereClause(filter) - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_order WHERE %s", whereClause) - if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { - return fmt.Errorf("total count query failed: %w", err) - } - return nil -} - -func (h *OrderHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter) (*models.AggregateData, error) { - aggregate := &models.AggregateData{ - ByStatus: make(map[string]int), - } - - whereClause, args := h.buildWhereClause(filter) - statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_order WHERE %s GROUP BY status ORDER BY status", whereClause) - - rows, err := dbConn.QueryContext(ctx, statusQuery, args...) - if err != nil { - return nil, fmt.Errorf("status query failed: %w", err) - } - defer rows.Close() - - for rows.Next() { - var status string - var count int - if err := rows.Scan(&status, &count); err != nil { - return nil, fmt.Errorf("status scan failed: %w", err) - } - aggregate.ByStatus[status] = count - - switch status { - case "active": - aggregate.TotalActive = count - case "draft": - aggregate.TotalDraft = count - case "inactive": - aggregate.TotalInactive = count - } - } - - return aggregate, nil -} - -// Helper methods -func (h *OrderHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { - log.Printf("[ERROR] %s: %v", message, err) - h.respondError(c, message, err, statusCode) -} - -func (h *OrderHandler) respondError(c *gin.Context, message string, err error, statusCode int) { - errorMessage := message - if gin.Mode() == gin.ReleaseMode { - errorMessage = "Internal server error" - } - - c.JSON(statusCode, models.ErrorResponse{ - Error: errorMessage, - Code: statusCode, - Message: err.Error(), - Timestamp: time.Now(), - }) -} - -func (h *OrderHandler) parsePaginationParams(c *gin.Context) (int, int, error) { - limit := 10 // Default limit - offset := 0 // Default offset - - if limitStr := c.Query("limit"); limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) - } - if parsedLimit <= 0 { - return 0, 0, fmt.Errorf("limit must be greater than 0") - } - if parsedLimit > 100 { - return 0, 0, fmt.Errorf("limit cannot exceed 100") - } - limit = parsedLimit - } - - if offsetStr := c.Query("offset"); offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) - } - if parsedOffset < 0 { - return 0, 0, fmt.Errorf("offset cannot be negative") - } - offset = parsedOffset - } - - return limit, offset, nil -} - -func (h *OrderHandler) parseFilterParams(c *gin.Context) models.OrderFilter { - filter := models.OrderFilter{} - - if status := c.Query("status"); status != "" { - if models.IsValidStatus(status) { - filter.Status = &status - } - } - - if search := c.Query("search"); search != "" { - filter.Search = &search - } - - // Parse date filters - if dateFromStr := c.Query("date_from"); dateFromStr != "" { - if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { - filter.DateFrom = &dateFrom - } - } - - if dateToStr := c.Query("date_to"); dateToStr != "" { - if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { - filter.DateTo = &dateTo - } - } - - return filter -} - -func (h *OrderHandler) buildWhereClause(filter models.OrderFilter) (string, []interface{}) { - conditions := []string{"status != 'deleted'"} - args := []interface{}{} - paramCount := 1 - - if filter.Status != nil { - conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) - args = append(args, *filter.Status) - paramCount++ - } - - if filter.Search != nil { - searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) - conditions = append(conditions, searchCondition) - searchTerm := "%" + *filter.Search + "%" - args = append(args, searchTerm) - paramCount++ - } - - if filter.DateFrom != nil { - conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) - args = append(args, *filter.DateFrom) - paramCount++ - } - - if filter.DateTo != nil { - conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) - args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) - paramCount++ - } - - return strings.Join(conditions, " AND "), args -} - -func (h *OrderHandler) calculateMeta(limit, offset, total int) models.MetaResponse { - totalPages := 0 - currentPage := 1 - if limit > 0 { - totalPages = (total + limit - 1) / limit // Ceiling division - currentPage = (offset / limit) + 1 - } - - return models.MetaResponse{ - Limit: limit, - Offset: offset, - Total: total, - TotalPages: totalPages, - CurrentPage: currentPage, - HasNext: offset+limit < total, - HasPrev: offset > 0, - } -} diff --git a/internal/handlers/product/product.go b/internal/handlers/product/product.go deleted file mode 100644 index b521a68..0000000 --- a/internal/handlers/product/product.go +++ /dev/null @@ -1,683 +0,0 @@ -package handlers - -import ( - "api-service/internal/config" - "api-service/internal/database" - models "api-service/internal/models/product" - "context" - "database/sql" - "fmt" - "log" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-playground/validator/v10" - "github.com/google/uuid" -) - -var ( - db database.Service - once sync.Once - validate *validator.Validate -) - -// Initialize the database connection and validator -func init() { - once.Do(func() { - db = database.New(config.LoadConfig()) - validate = validator.New() - // Register custom validations if needed - validate.RegisterValidation("product_status", validateProductStatus) - if db == nil { - log.Fatal("Failed to initialize database connection") - } - }) -} - -// Custom validation for product status -func validateProductStatus(fl validator.FieldLevel) bool { - return models.IsValidStatus(fl.Field().String()) -} - -// ProductHandler handles product services -type ProductHandler struct { - db database.Service -} - -// NewProductHandler creates a new ProductHandler -func NewProductHandler() *ProductHandler { - return &ProductHandler{ - db: db, - } -} - -// GetProduct godoc -// @Summary Get product with pagination and optional aggregation -// @Description Returns a paginated list of products with optional summary statistics -// @Tags product -// @Accept json -// @Produce json -// @Param limit query int false "Limit (max 100)" default(10) -// @Param offset query int false "Offset" default(0) -// @Param include_summary query bool false "Include aggregation summary" default(false) -// @Param status query string false "Filter by status" -// @Param search query string false "Search in multiple fields" -// @Success 200 {object} models.ProductGetResponse "Success response" -// @Failure 400 {object} models.ErrorResponse "Bad request" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/products [get] -func (h *ProductHandler) GetProduct(c *gin.Context) { - // Parse pagination parameters - limit, offset, err := h.parsePaginationParams(c) - if err != nil { - h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest) - return - } - - // Parse filter parameters - filter := h.parseFilterParams(c) - includeAggregation := c.Query("include_summary") == "true" - - // Get database connection - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - // Create context with timeout - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() - - // Execute concurrent operations - var ( - items []models.Product - total int - aggregateData *models.AggregateData - wg sync.WaitGroup - errChan = make(chan error, 3) - mu sync.Mutex - ) - - // Fetch total count - wg.Add(1) - go func() { - defer wg.Done() - if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil { - mu.Lock() - errChan <- fmt.Errorf("failed to get total count: %w", err) - mu.Unlock() - } - }() - - // Fetch main data - FIXED: Proper method name - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.fetchProducts(ctx, dbConn, filter, limit, offset) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to fetch data: %w", err) - } else { - items = result - } - mu.Unlock() - }() - - // Fetch aggregation data if requested - if includeAggregation { - wg.Add(1) - go func() { - defer wg.Done() - result, err := h.getAggregateData(ctx, dbConn, filter) - mu.Lock() - if err != nil { - errChan <- fmt.Errorf("failed to get aggregate data: %w", err) - } else { - aggregateData = result - } - mu.Unlock() - }() - } - - // Wait for all goroutines - wg.Wait() - close(errChan) - - // Check for errors - for err := range errChan { - if err != nil { - h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) - return - } - } - - // Build response - meta := h.calculateMeta(limit, offset, total) - response := models.ProductGetResponse{ - Message: "Data product berhasil diambil", - Data: items, - Meta: meta, - } - - if includeAggregation && aggregateData != nil { - response.Summary = aggregateData - } - - c.JSON(http.StatusOK, response) -} - -// GetProductByID godoc -// @Summary Get Product by ID -// @Description Returns a single product by ID -// @Tags product -// @Accept json -// @Produce json -// @Param id path string true "Product ID (UUID)" -// @Success 200 {object} models.ProductGetByIDResponse "Success response" -// @Failure 400 {object} models.ErrorResponse "Invalid ID format" -// @Failure 404 {object} models.ErrorResponse "Product not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/product/{id} [get] -func (h *ProductHandler) GetProductByID(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.getProductByID(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Product not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to get product", err, http.StatusInternalServerError) - } - return - } - - response := models.ProductGetByIDResponse{ - Message: "Product details retrieved successfully", - Data: item, - } - - c.JSON(http.StatusOK, response) -} - -// CreateProduct godoc -// @Summary Create product -// @Description Creates a new product record -// @Tags product -// @Accept json -// @Produce json -// @Param request body models.ProductCreateRequest true "Product creation request" -// @Success 201 {object} models.ProductCreateResponse "Product created successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/products [post] -func (h *ProductHandler) CreateProduct(c *gin.Context) { - var req models.ProductCreateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } - - // Validate request - if err := validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.createProduct(ctx, dbConn, &req) - if err != nil { - h.logAndRespondError(c, "Failed to create product", err, http.StatusInternalServerError) - return - } - - response := models.ProductCreateResponse{ - Message: "Product berhasil dibuat", - Data: item, - } - - c.JSON(http.StatusCreated, response) -} - -// UpdateProduct godoc -// @Summary Update product -// @Description Updates an existing product record -// @Tags product -// @Accept json -// @Produce json -// @Param id path string true "Product ID (UUID)" -// @Param request body models.ProductUpdateRequest true "Product update request" -// @Success 200 {object} models.ProductUpdateResponse "Product updated successfully" -// @Failure 400 {object} models.ErrorResponse "Bad request or validation error" -// @Failure 404 {object} models.ErrorResponse "Product not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/product/{id} [put] -func (h *ProductHandler) UpdateProduct(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - var req models.ProductUpdateRequest - if err := c.ShouldBindJSON(&req); err != nil { - h.respondError(c, "Invalid request body", err, http.StatusBadRequest) - return - } - - // Set ID from path parameter - req.ID = id - - // Validate request - if err := validate.Struct(&req); err != nil { - h.respondError(c, "Validation failed", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - item, err := h.updateProduct(ctx, dbConn, &req) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Product not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to update product", err, http.StatusInternalServerError) - } - return - } - - response := models.ProductUpdateResponse{ - Message: "Product berhasil diperbarui", - Data: item, - } - - c.JSON(http.StatusOK, response) -} - -// DeleteProduct godoc -// @Summary Delete product -// @Description Soft deletes a product by setting status to 'deleted' -// @Tags product -// @Accept json -// @Produce json -// @Param id path string true "Product ID (UUID)" -// @Success 200 {object} models.ProductDeleteResponse "Product deleted successfully" -// @Failure 400 {object} models.ErrorResponse "Invalid ID format" -// @Failure 404 {object} models.ErrorResponse "Product not found" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/product/{id} [delete] -func (h *ProductHandler) DeleteProduct(c *gin.Context) { - id := c.Param("id") - - // Validate UUID format - if _, err := uuid.Parse(id); err != nil { - h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) - return - } - - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - err = h.deleteProduct(ctx, dbConn, id) - if err != nil { - if err == sql.ErrNoRows { - h.respondError(c, "Product not found", err, http.StatusNotFound) - } else { - h.logAndRespondError(c, "Failed to delete product", err, http.StatusInternalServerError) - } - return - } - - response := models.ProductDeleteResponse{ - Message: "Product berhasil dihapus", - ID: id, - } - - c.JSON(http.StatusOK, response) -} - -// GetProductStats godoc -// @Summary Get product statistics -// @Description Returns comprehensive statistics about product data -// @Tags product -// @Accept json -// @Produce json -// @Param status query string false "Filter statistics by status" -// @Success 200 {object} models.AggregateData "Statistics data" -// @Failure 500 {object} models.ErrorResponse "Internal server error" -// @Router /api/v1/products/stats [get] -func (h *ProductHandler) GetProductStats(c *gin.Context) { - dbConn, err := h.db.GetDB("satudata") - if err != nil { - h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) - return - } - - ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) - defer cancel() - - filter := h.parseFilterParams(c) - aggregateData, err := h.getAggregateData(ctx, dbConn, filter) - if err != nil { - h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError) - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Statistik product berhasil diambil", - "data": aggregateData, - }) -} - -// Database operations -func (h *ProductHandler) getProductByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Product, error) { - query := "SELECT id, status, date_created, date_updated, name FROM data_product WHERE id = $1 AND status != 'deleted'" - row := dbConn.QueryRowContext(ctx, query, id) - - var item models.Product - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, err - } - - return &item, nil -} - -func (h *ProductHandler) createProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductCreateRequest) (*models.Product, error) { - id := uuid.New().String() - now := time.Now() - - query := "INSERT INTO data_product (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) - - var item models.Product - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to create product: %w", err) - } - - return &item, nil -} - -func (h *ProductHandler) updateProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductUpdateRequest) (*models.Product, error) { - now := time.Now() - - query := "UPDATE data_product SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name" - row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) - - var item models.Product - err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("failed to update product: %w", err) - } - - return &item, nil -} - -func (h *ProductHandler) deleteProduct(ctx context.Context, dbConn *sql.DB, id string) error { - now := time.Now() - query := "UPDATE data_product SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" - result, err := dbConn.ExecContext(ctx, query, id, now) - if err != nil { - return fmt.Errorf("failed to delete product: %w", err) - } - - rowsAffected, err := result.RowsAffected() - if err != nil { - return fmt.Errorf("failed to get affected rows: %w", err) - } - - if rowsAffected == 0 { - return sql.ErrNoRows - } - - return nil -} - -func (h *ProductHandler) fetchProducts(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, limit, offset int) ([]models.Product, error) { - whereClause, args := h.buildWhereClause(filter) - query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_product WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2) - - args = append(args, limit, offset) - rows, err := dbConn.QueryContext(ctx, query, args...) - if err != nil { - return nil, fmt.Errorf("fetch products query failed: %w", err) - } - defer rows.Close() - - items := make([]models.Product, 0, limit) - for rows.Next() { - var item models.Product - err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) - if err != nil { - return nil, fmt.Errorf("scan Product failed: %w", err) - } - items = append(items, item) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("rows iteration error: %w", err) - } - - return items, nil -} - -func (h *ProductHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, total *int) error { - whereClause, args := h.buildWhereClause(filter) - countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_product WHERE %s", whereClause) - if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { - return fmt.Errorf("total count query failed: %w", err) - } - return nil -} - -func (h *ProductHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter) (*models.AggregateData, error) { - aggregate := &models.AggregateData{ - ByStatus: make(map[string]int), - } - - whereClause, args := h.buildWhereClause(filter) - statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_product WHERE %s GROUP BY status ORDER BY status", whereClause) - - rows, err := dbConn.QueryContext(ctx, statusQuery, args...) - if err != nil { - return nil, fmt.Errorf("status query failed: %w", err) - } - defer rows.Close() - - for rows.Next() { - var status string - var count int - if err := rows.Scan(&status, &count); err != nil { - return nil, fmt.Errorf("status scan failed: %w", err) - } - aggregate.ByStatus[status] = count - - switch status { - case "active": - aggregate.TotalActive = count - case "draft": - aggregate.TotalDraft = count - case "inactive": - aggregate.TotalInactive = count - } - } - - return aggregate, nil -} - -// Helper methods -func (h *ProductHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { - log.Printf("[ERROR] %s: %v", message, err) - h.respondError(c, message, err, statusCode) -} - -func (h *ProductHandler) respondError(c *gin.Context, message string, err error, statusCode int) { - errorMessage := message - if gin.Mode() == gin.ReleaseMode { - errorMessage = "Internal server error" - } - - c.JSON(statusCode, models.ErrorResponse{ - Error: errorMessage, - Code: statusCode, - Message: err.Error(), - Timestamp: time.Now(), - }) -} - -func (h *ProductHandler) parsePaginationParams(c *gin.Context) (int, int, error) { - limit := 10 // Default limit - offset := 0 // Default offset - - if limitStr := c.Query("limit"); limitStr != "" { - parsedLimit, err := strconv.Atoi(limitStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr) - } - if parsedLimit <= 0 { - return 0, 0, fmt.Errorf("limit must be greater than 0") - } - if parsedLimit > 100 { - return 0, 0, fmt.Errorf("limit cannot exceed 100") - } - limit = parsedLimit - } - - if offsetStr := c.Query("offset"); offsetStr != "" { - parsedOffset, err := strconv.Atoi(offsetStr) - if err != nil { - return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr) - } - if parsedOffset < 0 { - return 0, 0, fmt.Errorf("offset cannot be negative") - } - offset = parsedOffset - } - - return limit, offset, nil -} - -func (h *ProductHandler) parseFilterParams(c *gin.Context) models.ProductFilter { - filter := models.ProductFilter{} - - if status := c.Query("status"); status != "" { - if models.IsValidStatus(status) { - filter.Status = &status - } - } - - if search := c.Query("search"); search != "" { - filter.Search = &search - } - - // Parse date filters - if dateFromStr := c.Query("date_from"); dateFromStr != "" { - if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { - filter.DateFrom = &dateFrom - } - } - - if dateToStr := c.Query("date_to"); dateToStr != "" { - if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { - filter.DateTo = &dateTo - } - } - - return filter -} - -func (h *ProductHandler) buildWhereClause(filter models.ProductFilter) (string, []interface{}) { - conditions := []string{"status != 'deleted'"} - args := []interface{}{} - paramCount := 1 - - if filter.Status != nil { - conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) - args = append(args, *filter.Status) - paramCount++ - } - - if filter.Search != nil { - searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) - conditions = append(conditions, searchCondition) - searchTerm := "%" + *filter.Search + "%" - args = append(args, searchTerm) - paramCount++ - } - - if filter.DateFrom != nil { - conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) - args = append(args, *filter.DateFrom) - paramCount++ - } - - if filter.DateTo != nil { - conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) - args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) - paramCount++ - } - - return strings.Join(conditions, " AND "), args -} - -func (h *ProductHandler) calculateMeta(limit, offset, total int) models.MetaResponse { - totalPages := 0 - currentPage := 1 - if limit > 0 { - totalPages = (total + limit - 1) / limit // Ceiling division - currentPage = (offset / limit) + 1 - } - - return models.MetaResponse{ - Limit: limit, - Offset: offset, - Total: total, - TotalPages: totalPages, - CurrentPage: currentPage, - HasNext: offset+limit < total, - HasPrev: offset > 0, - } -} diff --git a/internal/helpers/bpjs/lz-string.go b/internal/helpers/bpjs/lz-string.go new file mode 100644 index 0000000..35a85ce --- /dev/null +++ b/internal/helpers/bpjs/lz-string.go @@ -0,0 +1,111 @@ +package helper + +import ( + "errors" + "math" + "unicode/utf8" +) + +// +// Decompress uri encoded lz-string +// http://pieroxy.net/blog/pages/lz-string/index.html +// https://github.com/pieroxy/lz-string/ +// + +// map of "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$" +var keyStrUriSafe map[byte]int = map[byte]int{74: 9, 78: 13, 83: 18, 36: 64, 109: 38, 114: 43, 116: 45, 101: 30, 45: 63, 73: 8, 81: 16, 113: 42, 49: 53, 50: 54, 54: 58, 76: 11, 100: 29, 107: 36, 121: 50, 77: 12, 89: 24, 105: 34, 66: 1, 69: 4, 85: 20, 48: 52, 119: 48, 117: 46, 120: 49, 52: 56, 56: 60, 110: 39, 112: 41, 70: 5, 71: 6, 79: 14, 88: 23, 97: 26, 102: 31, 103: 32, 67: 2, 118: 47, 65: 0, 68: 3, 72: 7, 108: 37, 51: 55, 57: 61, 82: 17, 90: 25, 98: 27, 115: 44, 122: 51, 53: 57, 86: 21, 106: 35, 111: 40, 55: 59, 43: 62, 75: 10, 80: 15, 84: 19, 87: 22, 99: 28, 104: 33} + +type dataStruct struct { + input string + val int + position int + index int + dictionary []string + enlargeIn float64 + numBits int +} + +func getBaseValue(char byte) int { + return keyStrUriSafe[char] +} + +// Input is composed of ASCII characters, so accessing it by array has no UTF-8 pb. +func readBits(nb int, data *dataStruct) int { + result := 0 + power := 1 + for i := 0; i < nb; i++ { + respB := data.val & data.position + data.position = data.position / 2 + if data.position == 0 { + data.position = 32 + data.val = getBaseValue(data.input[data.index]) + data.index += 1 + } + if respB > 0 { + result |= power + } + power *= 2 + } + return result +} + +func appendValue(data *dataStruct, str string) { + data.dictionary = append(data.dictionary, str) + data.enlargeIn -= 1 + if data.enlargeIn == 0 { + data.enlargeIn = math.Pow(2, float64(data.numBits)) + data.numBits += 1 + } +} + +func getString(last string, data *dataStruct) (string, bool, error) { + c := readBits(data.numBits, data) + switch c { + case 0: + str := string(readBits(8, data)) + appendValue(data, str) + return str, false, nil + case 1: + str := string(readBits(16, data)) + appendValue(data, str) + return str, false, nil + case 2: + return "", true, nil + } + if c < len(data.dictionary) { + return data.dictionary[c], false, nil + } + if c == len(data.dictionary) { + return concatWithFirstRune(last, last), false, nil + } + return "", false, errors.New("Bad character encoding.") +} + +// Need to handle UTF-8, so we need to use rune to concatenate +func concatWithFirstRune(str string, getFirstRune string) string { + r, _ := utf8.DecodeRuneInString(getFirstRune) + return str + string(r) +} + +func DecompressFromEncodedUriComponent(input string) (string, error) { + data := dataStruct{input, getBaseValue(input[0]), 32, 1, []string{"0", "1", "2"}, 5, 2} + + result, isEnd, err := getString("", &data) + if err != nil || isEnd { + return result, err + } + last := result + data.numBits += 1 + for { + str, isEnd, err := getString(last, &data) + if err != nil || isEnd { + return result, err + } + + result = result + str + appendValue(&data, concatWithFirstRune(last, str)) + last = str + } + + return "", errors.New("Unexpected end of buffer reached.") +} \ No newline at end of file diff --git a/internal/helpers/bpjs/pkcs7.go b/internal/helpers/bpjs/pkcs7.go new file mode 100644 index 0000000..119de76 --- /dev/null +++ b/internal/helpers/bpjs/pkcs7.go @@ -0,0 +1,25 @@ +package helper + +import "errors" + +func Pad(buf []byte, size int) ([]byte, error) { + bufLen := len(buf) + padLen := size - bufLen%size + padded := make([]byte, bufLen+padLen) + copy(padded, buf) + for i := 0; i < padLen; i++ { + padded[bufLen+i] = byte(padLen) + } + return padded, nil +} + +func Unpad(padded []byte, size int) ([]byte, error) { + if len(padded)%size != 0 { + return nil, errors.New("pkcs7: Padded value wasn't in correct size.") + } + + bufLen := len(padded) - int(padded[len(padded)-1]) + buf := make([]byte, bufLen) + copy(buf, padded[:bufLen]) + return buf, nil +} \ No newline at end of file diff --git a/internal/models/bpjs/peserta.go b/internal/models/bpjs/peserta.go new file mode 100644 index 0000000..bf6d7ba --- /dev/null +++ b/internal/models/bpjs/peserta.go @@ -0,0 +1,84 @@ +package models + +// PesertaResponse represents the response structure for BPJS participant data +type PesertaResponse struct { + Message string `json:"message"` + Data map[string]interface{} `json:"data"` +} + +// PesertaRawResponse represents the raw response structure from BPJS API +type PesertaRawResponse struct { + MetaData struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"metaData"` + Response interface{} `json:"response"` +} + +// PesertaRequest represents the request structure for BPJS participant search +type PesertaRequest struct { + NIK string `json:"nik" binding:"required"` + TglSEP string `json:"tglSEP" binding:"required"` +} + +// PesertaData represents the participant data structure +type PesertaData struct { + NoKartu string `json:"noKartu"` + NIK string `json:"nik"` + Nama string `json:"nama"` + Pisa string `json:"pisa"` + Sex string `json:"sex"` + TglLahir string `json:"tglLahir"` + Pob string `json:"pob"` + KdProvider string `json:"kdProvider"` + NmProvider string `json:"nmProvider"` + KelasRawat string `json:"kelasRawat"` + Keterangan string `json:"keterangan"` + NoTelepon string `json:"noTelepon"` + Alamat string `json:"alamat"` + KdPos string `json:"kdPos"` + Pekerjaan string `json:"pekerjaan"` + StatusKawin string `json:"statusKawin"` + TglCetakKartu string `json:"tglCetakKartu"` + TglTAT string `json:"tglTAT"` + TglTMT string `json:"tglTMT"` + ProvUmum struct { + KdProvider string `json:"kdProvider"` + NmProvider string `json:"nmProvider"` + } `json:"provUmum"` + JenisPeserta struct { + KdJenisPeserta string `json:"kdJenisPeserta"` + NmJenisPeserta string `json:"nmJenisPeserta"` + } `json:"jenisPeserta"` + KelasTanggungan struct { + KdKelas string `json:"kdKelas"` + NmKelas string `json:"nmKelas"` + } `json:"kelasTanggungan"` + Informasi struct { + Dinsos string `json:"dinsos"` + NoSKTM string `json:"noSKTM"` + ProlanisPRB string `json:"prolanisPRB"` + } `json:"informasi"` + Cob struct { + NoAsuransi string `json:"noAsuransi"` + NmAsuransi string `json:"nmAsuransi"` + TglTAT string `json:"tglTAT"` + TglTMT string `json:"tglTMT"` + } `json:"cob"` + HakKelas struct { + Kode string `json:"kode"` + Nama string `json:"nama"` + } `json:"hakKelas"` + Mr struct { + NoMR string `json:"noMR"` + NoTelepon string `json:"noTelepon"` + } `json:"mr"` + ProvRujuk struct { + KdProvider string `json:"kdProvider"` + NmProvider string `json:"nmProvider"` + } `json:"provRujuk"` + StatusPeserta struct { + Kode string `json:"kode"` + Nama string `json:"nama"` + } `json:"statusPeserta"` +} diff --git a/internal/models/order/order.go b/internal/models/order/order.go deleted file mode 100644 index 7bc1866..0000000 --- a/internal/models/order/order.go +++ /dev/null @@ -1,195 +0,0 @@ -package models - -import ( - "database/sql" - "database/sql/driver" - "encoding/json" - "time" -) - -// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility -type NullableInt32 struct { - Int32 int32 `json:"int32,omitempty"` - Valid bool `json:"valid"` -} - -// Scan implements the sql.Scanner interface for NullableInt32 -func (n *NullableInt32) Scan(value interface{}) error { - var ni sql.NullInt32 - if err := ni.Scan(value); err != nil { - return err - } - n.Int32 = ni.Int32 - n.Valid = ni.Valid - return nil -} - -// Value implements the driver.Valuer interface for NullableInt32 -func (n NullableInt32) Value() (driver.Value, error) { - if !n.Valid { - return nil, nil - } - return n.Int32, nil -} - -// Order represents the data structure for the order table -type Order struct { - ID string `json:"id" db:"id"` - Status string `json:"status" db:"status"` - Sort NullableInt32 `json:"sort,omitempty" db:"sort"` - UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"` - DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"` - UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"` - DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"` - Name sql.NullString `json:"name,omitempty" db:"name"` -} - -// Custom JSON marshaling for Order -func (r Order) MarshalJSON() ([]byte, error) { - type Alias Order - aux := &struct { - Sort *int `json:"sort,omitempty"` - UserCreated *string `json:"user_created,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - UserUpdated *string `json:"user_updated,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - Name *string `json:"name,omitempty"` - *Alias - }{ - Alias: (*Alias)(&r), - } - - if r.Sort.Valid { - sort := int(r.Sort.Int32) - aux.Sort = &sort - } - if r.UserCreated.Valid { - aux.UserCreated = &r.UserCreated.String - } - if r.DateCreated.Valid { - aux.DateCreated = &r.DateCreated.Time - } - if r.UserUpdated.Valid { - aux.UserUpdated = &r.UserUpdated.String - } - if r.DateUpdated.Valid { - aux.DateUpdated = &r.DateUpdated.Time - } - if r.Name.Valid { - aux.Name = &r.Name.String - } - - return json.Marshal(aux) -} - -// Helper methods -func (r *Order) GetName() string { - if r.Name.Valid { - return r.Name.String - } - return "" -} - -// Response struct for GET by ID -type OrderGetByIDResponse struct { - Message string `json:"message"` - Data *Order `json:"data"` -} - -// Enhanced GET response with pagination and aggregation -type OrderGetResponse struct { - Message string `json:"message"` - Data []Order `json:"data"` - Meta MetaResponse `json:"meta"` - Summary *AggregateData `json:"summary,omitempty"` -} - -// Request struct for create -type OrderCreateRequest struct { - Status string `json:"status" validate:"required,oneof=draft active inactive"` - Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` -} - -// Response struct for create -type OrderCreateResponse struct { - Message string `json:"message"` - Data *Order `json:"data"` -} - -// Update request -type OrderUpdateRequest struct { - ID string `json:"-" validate:"required,uuid4"` // ID dari URL path - Status string `json:"status" validate:"required,oneof=draft active inactive"` - Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` -} - -// Response struct for update -type OrderUpdateResponse struct { - Message string `json:"message"` - Data *Order `json:"data"` -} - -// Response struct for delete -type OrderDeleteResponse struct { - Message string `json:"message"` - ID string `json:"id"` -} - -// Metadata for pagination -type MetaResponse struct { - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` - TotalPages int `json:"total_pages"` - CurrentPage int `json:"current_page"` - HasNext bool `json:"has_next"` - HasPrev bool `json:"has_prev"` -} - -// Aggregate data for summary -type AggregateData struct { - TotalActive int `json:"total_active"` - TotalDraft int `json:"total_draft"` - TotalInactive int `json:"total_inactive"` - ByStatus map[string]int `json:"by_status"` - LastUpdated *time.Time `json:"last_updated,omitempty"` - CreatedToday int `json:"created_today"` - UpdatedToday int `json:"updated_today"` -} - -// Error response -type ErrorResponse struct { - Error string `json:"error"` - Code int `json:"code"` - Message string `json:"message"` - Timestamp time.Time `json:"timestamp"` -} - -// Filter struct for query parameters -type OrderFilter struct { - Status *string `json:"status,omitempty" form:"status"` - Search *string `json:"search,omitempty" form:"search"` - DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` - DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` -} - -// Validation constants -const ( - StatusDraft = "draft" - StatusActive = "active" - StatusInactive = "inactive" - StatusDeleted = "deleted" -) - -// ValidStatuses for validation -var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive} - -// IsValidStatus helper function -func IsValidStatus(status string) bool { - for _, validStatus := range ValidStatuses { - if status == validStatus { - return true - } - } - return false -} diff --git a/internal/models/product/product.go b/internal/models/product/product.go deleted file mode 100644 index 923b613..0000000 --- a/internal/models/product/product.go +++ /dev/null @@ -1,195 +0,0 @@ -package models - -import ( - "database/sql" - "database/sql/driver" - "encoding/json" - "time" -) - -// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility -type NullableInt32 struct { - Int32 int32 `json:"int32,omitempty"` - Valid bool `json:"valid"` -} - -// Scan implements the sql.Scanner interface for NullableInt32 -func (n *NullableInt32) Scan(value interface{}) error { - var ni sql.NullInt32 - if err := ni.Scan(value); err != nil { - return err - } - n.Int32 = ni.Int32 - n.Valid = ni.Valid - return nil -} - -// Value implements the driver.Valuer interface for NullableInt32 -func (n NullableInt32) Value() (driver.Value, error) { - if !n.Valid { - return nil, nil - } - return n.Int32, nil -} - -// Product represents the data structure for the product table -type Product struct { - ID string `json:"id" db:"id"` - Status string `json:"status" db:"status"` - Sort NullableInt32 `json:"sort,omitempty" db:"sort"` - UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"` - DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"` - UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"` - DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"` - Name sql.NullString `json:"name,omitempty" db:"name"` -} - -// Custom JSON marshaling for Product -func (r Product) MarshalJSON() ([]byte, error) { - type Alias Product - aux := &struct { - Sort *int `json:"sort,omitempty"` - UserCreated *string `json:"user_created,omitempty"` - DateCreated *time.Time `json:"date_created,omitempty"` - UserUpdated *string `json:"user_updated,omitempty"` - DateUpdated *time.Time `json:"date_updated,omitempty"` - Name *string `json:"name,omitempty"` - *Alias - }{ - Alias: (*Alias)(&r), - } - - if r.Sort.Valid { - sort := int(r.Sort.Int32) - aux.Sort = &sort - } - if r.UserCreated.Valid { - aux.UserCreated = &r.UserCreated.String - } - if r.DateCreated.Valid { - aux.DateCreated = &r.DateCreated.Time - } - if r.UserUpdated.Valid { - aux.UserUpdated = &r.UserUpdated.String - } - if r.DateUpdated.Valid { - aux.DateUpdated = &r.DateUpdated.Time - } - if r.Name.Valid { - aux.Name = &r.Name.String - } - - return json.Marshal(aux) -} - -// Helper methods -func (r *Product) GetName() string { - if r.Name.Valid { - return r.Name.String - } - return "" -} - -// Response struct for GET by ID -type ProductGetByIDResponse struct { - Message string `json:"message"` - Data *Product `json:"data"` -} - -// Enhanced GET response with pagination and aggregation -type ProductGetResponse struct { - Message string `json:"message"` - Data []Product `json:"data"` - Meta MetaResponse `json:"meta"` - Summary *AggregateData `json:"summary,omitempty"` -} - -// Request struct for create -type ProductCreateRequest struct { - Status string `json:"status" validate:"required,oneof=draft active inactive"` - Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` -} - -// Response struct for create -type ProductCreateResponse struct { - Message string `json:"message"` - Data *Product `json:"data"` -} - -// Update request -type ProductUpdateRequest struct { - ID string `json:"-" validate:"required,uuid4"` // ID dari URL path - Status string `json:"status" validate:"required,oneof=draft active inactive"` - Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"` -} - -// Response struct for update -type ProductUpdateResponse struct { - Message string `json:"message"` - Data *Product `json:"data"` -} - -// Response struct for delete -type ProductDeleteResponse struct { - Message string `json:"message"` - ID string `json:"id"` -} - -// Metadata for pagination -type MetaResponse struct { - Limit int `json:"limit"` - Offset int `json:"offset"` - Total int `json:"total"` - TotalPages int `json:"total_pages"` - CurrentPage int `json:"current_page"` - HasNext bool `json:"has_next"` - HasPrev bool `json:"has_prev"` -} - -// Aggregate data for summary -type AggregateData struct { - TotalActive int `json:"total_active"` - TotalDraft int `json:"total_draft"` - TotalInactive int `json:"total_inactive"` - ByStatus map[string]int `json:"by_status"` - LastUpdated *time.Time `json:"last_updated,omitempty"` - CreatedToday int `json:"created_today"` - UpdatedToday int `json:"updated_today"` -} - -// Error response -type ErrorResponse struct { - Error string `json:"error"` - Code int `json:"code"` - Message string `json:"message"` - Timestamp time.Time `json:"timestamp"` -} - -// Filter struct for query parameters -type ProductFilter struct { - Status *string `json:"status,omitempty" form:"status"` - Search *string `json:"search,omitempty" form:"search"` - DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"` - DateTo *time.Time `json:"date_to,omitempty" form:"date_to"` -} - -// Validation constants -const ( - StatusDraft = "draft" - StatusActive = "active" - StatusInactive = "inactive" - StatusDeleted = "deleted" -) - -// ValidStatuses for validation -var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive} - -// IsValidStatus helper function -func IsValidStatus(status string) bool { - for _, validStatus := range ValidStatuses { - if status == validStatus { - return true - } - } - return false -} diff --git a/internal/routes/v1/routes.go b/internal/routes/v1/routes.go index 9dac361..ba076e3 100644 --- a/internal/routes/v1/routes.go +++ b/internal/routes/v1/routes.go @@ -1,9 +1,8 @@ package v1 import ( - orderHandlers "api-service/internal/handlers/order" + bpjsPesertaHandlers "api-service/internal/handlers/bpjs" retribusiHandlers "api-service/internal/handlers/retribusi" - "net/http" "api-service/internal/config" "api-service/internal/middleware" @@ -50,7 +49,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { v1.POST("/token/generate", tokenHandler.GenerateToken) v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) - // Retribusi endpoints retribusiHandler := retribusiHandlers.NewRetribusiHandler() v1.GET("/retribusis", retribusiHandler.GetRetribusi) @@ -59,35 +57,16 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine { v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi) v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi) - // Protected routes (require authentication) - - // Order endpoints - orderHandler := orderHandlers.NewOrderHandler() - v1.GET("/orders", orderHandler.GetOrder) - v1.GET("/order/:id", orderHandler.GetOrderByID) - v1.POST("/orders", orderHandler.CreateOrder) - v1.PUT("/order/:id", orderHandler.UpdateOrder) - v1.DELETE("/order/:id", orderHandler.DeleteOrder) + // BPJS endpoints + bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs) + v1.GET("/bpjs/Peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK) protected := v1.Group("/") protected.Use(middleware.JWTAuthMiddleware(authService)) { - // WebSocket endpoint - protected.GET("/websocket", WebSocketHandler) - protected.GET("/webservice", WebServiceHandler) + // Protected routes (require authentication) } } return router } - -// WebSocketHandler handles WebSocket connections -func WebSocketHandler(c *gin.Context) { - // This will be implemented with proper WebSocket handling - c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"}) -} - -func WebServiceHandler(c *gin.Context) { - // This will be implemented with proper WebSocket handling - c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"}) -} diff --git a/internal/services/bpjs/response.go b/internal/services/bpjs/response.go new file mode 100644 index 0000000..8134949 --- /dev/null +++ b/internal/services/bpjs/response.go @@ -0,0 +1,59 @@ +package services + +import ( + helper "api-service/internal/helpers/bpjs" + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" +) + +// ResponseVclaim decrypts the encrypted response from VClaim API +func ResponseVclaim(encrypted string, key string) (string, error) { + if encrypted == "" { + return "", errors.New("encrypted response is empty") + } + + if key == "" { + return "", errors.New("decryption key is empty") + } + + cipherText, err := base64.StdEncoding.DecodeString(encrypted) + if err != nil { + return "", fmt.Errorf("failed to decode base64: %w", err) + } + + hash := sha256.Sum256([]byte(key)) + block, err := aes.NewCipher(hash[:]) + if err != nil { + return "", fmt.Errorf("failed to create cipher: %w", err) + } + + if len(cipherText) < aes.BlockSize { + return "", errors.New("cipherText too short") + } + + iv := hash[:aes.BlockSize] + if len(cipherText)%aes.BlockSize != 0 { + return "", errors.New("cipherText is not a multiple of the block size") + } + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(cipherText, cipherText) + + // Unpad the decrypted data + cipherText, err = helper.Unpad(cipherText, aes.BlockSize) + if err != nil { + return "", fmt.Errorf("failed to unpad: %w", err) + } + + // Decompress the data + data, err := helper.DecompressFromEncodedUriComponent(string(cipherText)) + if err != nil { + return "", fmt.Errorf("failed to decompress: %w", err) + } + + return data, nil +} diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go new file mode 100644 index 0000000..4a386dd --- /dev/null +++ b/internal/services/bpjs/vclaimBridge.go @@ -0,0 +1,311 @@ +package services + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "api-service/internal/config" + + "github.com/mashingan/smapping" +) + +// VClaimService interface for VClaim operations +type VClaimService interface { + Get(ctx context.Context, endpoint string, result interface{}) error + Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error + Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error + Delete(ctx context.Context, endpoint string, result interface{}) error + GetRawResponse(ctx context.Context, endpoint string) (*ResponDTO, error) + PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTO, error) +} + +// Service struct for VClaim service +type Service struct { + config config.BpjsConfig + httpClient *http.Client +} + +// Response structures +type ResponMentahDTO struct { + MetaData struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"metaData"` + Response string `json:"response"` +} + +type ResponDTO struct { + MetaData struct { + Code string `json:"code"` + Message string `json:"message"` + } `json:"metaData"` + Response interface{} `json:"response"` +} + +// NewService creates a new VClaim service instance +func NewService(cfg config.BpjsConfig) VClaimService { + service := &Service{ + config: cfg, + httpClient: &http.Client{ + Timeout: cfg.Timeout, + }, + } + return service +} + +// NewServiceFromConfig creates service from main config +func NewServiceFromConfig(cfg *config.Config) VClaimService { + return NewService(cfg.Bpjs) +} + +// NewServiceFromInterface creates service from interface (for backward compatibility) +func NewServiceFromInterface(cfg interface{}) (VClaimService, error) { + var bpjsConfig config.BpjsConfig + + // Try to map from interface + err := smapping.FillStruct(&bpjsConfig, smapping.MapFields(&cfg)) + if err != nil { + return nil, fmt.Errorf("failed to map config: %w", err) + } + + if bpjsConfig.Timeout == 0 { + bpjsConfig.Timeout = 30 * time.Second + } + + return NewService(bpjsConfig), nil +} + +// SetHTTPClient allows custom http client configuration +func (s *Service) SetHTTPClient(client *http.Client) { + s.httpClient = client +} + +// prepareRequest prepares HTTP request with required headers +func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) { + fullURL := s.config.BaseURL + endpoint + req, err := http.NewRequestWithContext(ctx, method, fullURL, body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers using the SetHeader method + consID, _, userKey, tstamp, xSignature := s.config.SetHeader() + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-cons-id", consID) + req.Header.Set("X-timestamp", tstamp) + req.Header.Set("X-signature", xSignature) + req.Header.Set("user_key", userKey) + + return req, nil +} + +// processResponse processes response from VClaim API +func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) { + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Check HTTP status + if res.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body)) + } + + // Parse raw response + var respMentah ResponMentahDTO + if err := json.Unmarshal(body, &respMentah); err != nil { + return nil, fmt.Errorf("failed to unmarshal raw response: %w", err) + } + + // Create final response + finalResp := &ResponDTO{ + MetaData: respMentah.MetaData, + } + + // If response is empty, return as is + if respMentah.Response == "" { + return finalResp, nil + } + + // Decrypt response + consID, secretKey, _, tstamp, _ := s.config.SetHeader() + respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp) + if err != nil { + return nil, fmt.Errorf("failed to decrypt response: %w", err) + } + + // Unmarshal decrypted response + if respDecrypt != "" { + if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil { + // If JSON unmarshal fails, store as string + finalResp.Response = respDecrypt + } + } + + return finalResp, nil +} + +// Get performs HTTP GET request +func (s *Service) Get(ctx context.Context, endpoint string, result interface{}) error { + resp, err := s.GetRawResponse(ctx, endpoint) + if err != nil { + return err + } + + return mapToResult(resp, result) +} + +// Post performs HTTP POST request +func (s *Service) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error { + resp, err := s.PostRawResponse(ctx, endpoint, payload) + if err != nil { + return err + } + + return mapToResult(resp, result) +} + +// Put performs HTTP PUT request +func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error { + var buf bytes.Buffer + if payload != nil { + if err := json.NewEncoder(&buf).Encode(payload); err != nil { + return fmt.Errorf("failed to encode payload: %w", err) + } + } + + req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) + if err != nil { + return err + } + + res, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to execute PUT request: %w", err) + } + + resp, err := s.processResponse(res) + if err != nil { + return err + } + + return mapToResult(resp, result) +} + +// Delete performs HTTP DELETE request +func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error { + req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return err + } + + res, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to execute DELETE request: %w", err) + } + + resp, err := s.processResponse(res) + if err != nil { + return err + } + + return mapToResult(resp, result) +} + +// GetRawResponse returns raw response without mapping +func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTO, error) { + req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + res, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute GET request: %w", err) + } + + return s.processResponse(res) +} + +// PostRawResponse returns raw response without mapping +func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTO, error) { + var buf bytes.Buffer + if payload != nil { + if err := json.NewEncoder(&buf).Encode(payload); err != nil { + return nil, fmt.Errorf("failed to encode payload: %w", err) + } + } + + req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf) + if err != nil { + return nil, err + } + + res, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute POST request: %w", err) + } + + return s.processResponse(res) +} + +// mapToResult maps the final response to the result interface +func mapToResult(resp *ResponDTO, result interface{}) error { + respBytes, err := json.Marshal(resp) + if err != nil { + return fmt.Errorf("failed to marshal final response: %w", err) + } + + if err := json.Unmarshal(respBytes, result); err != nil { + return fmt.Errorf("failed to unmarshal to result: %w", err) + } + + return nil +} + +// Backward compatibility functions +func GetRequest(endpoint string, cfg interface{}) interface{} { + service, err := NewServiceFromInterface(cfg) + if err != nil { + fmt.Printf("Failed to create service: %v\n", err) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := service.GetRawResponse(ctx, endpoint) + if err != nil { + fmt.Printf("Failed to get response: %v\n", err) + return nil + } + + return resp +} + +func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} { + service, err := NewServiceFromInterface(cfg) + if err != nil { + fmt.Printf("Failed to create service: %v\n", err) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + resp, err := service.PostRawResponse(ctx, endpoint, data) + if err != nil { + fmt.Printf("Failed to post response: %v\n", err) + return nil + } + + return resp +}