Creat Service BPJS
This commit is contained in:
@@ -72,7 +72,7 @@ tools/generate.bat product get post put delete
|
|||||||
# Atau langsung dengan Go
|
# Atau langsung dengan Go
|
||||||
go run tools/generate-handler.go product get post
|
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
|
### Method Tersedia
|
||||||
|
|||||||
519
docs/docs.go
519
docs/docs.go
@@ -24,56 +24,6 @@ const docTemplate = `{
|
|||||||
"host": "{{.Host}}",
|
"host": "{{.Host}}",
|
||||||
"basePath": "{{.BasePath}}",
|
"basePath": "{{.BasePath}}",
|
||||||
"paths": {
|
"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": {
|
"/api/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate user with username and password to receive JWT token",
|
"description": "Authenticate user with username and password to receive JWT token",
|
||||||
@@ -264,116 +214,9 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/order/{id}": {
|
"/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
|
||||||
"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": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a paginated list of orders with optional summary statistics",
|
"description": "Search participant data based on Population NIK and service date",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -381,142 +224,52 @@ const docTemplate = `{
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"order"
|
"bpjs"
|
||||||
],
|
],
|
||||||
"summary": "Get order with pagination and optional aggregation",
|
"summary": "Get participant data by NIK",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"default": 10,
|
"description": "NIK KTP",
|
||||||
"description": "Limit (max 100)",
|
"name": "nik",
|
||||||
"name": "limit",
|
"in": "path",
|
||||||
"in": "query"
|
"required": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
"type": "string",
|
||||||
"description": "Filter by status",
|
"description": "Service date/SEP date (format: yyyy-MM-dd)",
|
||||||
"name": "status",
|
"name": "tglSEP",
|
||||||
"in": "query"
|
"in": "path",
|
||||||
},
|
"required": true
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Search in multiple fields",
|
|
||||||
"name": "search",
|
|
||||||
"in": "query"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success response",
|
"description": "Participant data",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad request",
|
"description": "Bad request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Participant not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal server error",
|
"description": "Internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
"type": "object",
|
||||||
}
|
"additionalProperties": true
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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": {
|
"api-service_internal_models_retribusi.AggregateData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -22,56 +22,6 @@
|
|||||||
"host": "localhost:8080",
|
"host": "localhost:8080",
|
||||||
"basePath": "/api/v1",
|
"basePath": "/api/v1",
|
||||||
"paths": {
|
"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": {
|
"/api/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate user with username and password to receive JWT token",
|
"description": "Authenticate user with username and password to receive JWT token",
|
||||||
@@ -262,116 +212,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/order/{id}": {
|
"/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
|
||||||
"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": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Returns a paginated list of orders with optional summary statistics",
|
"description": "Search participant data based on Population NIK and service date",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -379,142 +222,52 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"order"
|
"bpjs"
|
||||||
],
|
],
|
||||||
"summary": "Get order with pagination and optional aggregation",
|
"summary": "Get participant data by NIK",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"default": 10,
|
"description": "NIK KTP",
|
||||||
"description": "Limit (max 100)",
|
"name": "nik",
|
||||||
"name": "limit",
|
"in": "path",
|
||||||
"in": "query"
|
"required": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"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",
|
"type": "string",
|
||||||
"description": "Filter by status",
|
"description": "Service date/SEP date (format: yyyy-MM-dd)",
|
||||||
"name": "status",
|
"name": "tglSEP",
|
||||||
"in": "query"
|
"in": "path",
|
||||||
},
|
"required": true
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"description": "Search in multiple fields",
|
|
||||||
"name": "search",
|
|
||||||
"in": "query"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Success response",
|
"description": "Participant data",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "Bad request",
|
"description": "Bad request",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Participant not found",
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"500": {
|
"500": {
|
||||||
"description": "Internal server error",
|
"description": "Internal server error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
"type": "object",
|
||||||
}
|
"additionalProperties": true
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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": {
|
"api-service_internal_models_retribusi.AggregateData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -30,150 +30,6 @@ definitions:
|
|||||||
username:
|
username:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
api-service_internal_models_retribusi.AggregateData:
|
||||||
properties:
|
properties:
|
||||||
by_dinas:
|
by_dinas:
|
||||||
@@ -466,39 +322,6 @@ info:
|
|||||||
title: API Service
|
title: API Service
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
paths:
|
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:
|
/api/v1/auth/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -622,177 +445,48 @@ paths:
|
|||||||
summary: Register new user
|
summary: Register new user
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
/api/v1/order/{id}:
|
/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}:
|
||||||
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:
|
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- 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:
|
parameters:
|
||||||
- default: 10
|
- description: NIK KTP
|
||||||
description: Limit (max 100)
|
in: path
|
||||||
in: query
|
name: nik
|
||||||
name: limit
|
required: true
|
||||||
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
|
|
||||||
type: string
|
type: string
|
||||||
- description: Search in multiple fields
|
- description: 'Service date/SEP date (format: yyyy-MM-dd)'
|
||||||
in: query
|
in: path
|
||||||
name: search
|
name: tglSEP
|
||||||
|
required: true
|
||||||
type: string
|
type: string
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Success response
|
description: Participant data
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/api-service_internal_models_order.OrderGetResponse'
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
"400":
|
"400":
|
||||||
description: Bad request
|
description: Bad request
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
"404":
|
||||||
|
description: Participant not found
|
||||||
|
schema:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
"500":
|
"500":
|
||||||
description: Internal server error
|
description: Internal server error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
additionalProperties: true
|
||||||
summary: Get order with pagination and optional aggregation
|
type: object
|
||||||
|
summary: Get participant data by NIK
|
||||||
tags:
|
tags:
|
||||||
- order
|
- bpjs
|
||||||
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
|
|
||||||
/api/v1/retribusi/{id}:
|
/api/v1/retribusi/{id}:
|
||||||
delete:
|
delete:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -63,3 +63,9 @@ KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox
|
|||||||
KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran
|
KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran
|
||||||
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
|
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
|
||||||
KEYCLOAK_ENABLED=false
|
KEYCLOAK_ENABLED=false
|
||||||
|
|
||||||
|
# BPJS Configuration
|
||||||
|
BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
|
||||||
|
BPJS_CONSID=5257
|
||||||
|
BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482
|
||||||
|
BPJS_SECRETKEY=1bV363512D
|
||||||
3
go.mod
3
go.mod
@@ -16,8 +16,10 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-playground/validator/v10 v10.27.0
|
||||||
github.com/go-sql-driver/mysql v1.8.1
|
github.com/go-sql-driver/mysql v1.8.1
|
||||||
github.com/joho/godotenv v1.5.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/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.6
|
github.com/swaggo/swag v1.16.6
|
||||||
@@ -39,7 +41,6 @@ require (
|
|||||||
github.com/go-openapi/swag v0.19.15 // indirect
|
github.com/go-openapi/swag v0.19.15 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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.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 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
|
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -9,10 +14,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig
|
Server ServerConfig
|
||||||
Databases map[string]DatabaseConfig
|
Databases map[string]DatabaseConfig
|
||||||
ReadReplicas map[string][]DatabaseConfig // For read replicas
|
ReadReplicas map[string][]DatabaseConfig // For read replicas
|
||||||
Keycloak KeycloakConfig
|
Keycloak KeycloakConfig
|
||||||
|
Bpjs BpjsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -21,20 +27,20 @@ type ServerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
Database string
|
Database string
|
||||||
Schema string
|
Schema string
|
||||||
SSLMode string
|
SSLMode string
|
||||||
Path string // For SQLite
|
Path string // For SQLite
|
||||||
Options string // Additional connection options
|
Options string // Additional connection options
|
||||||
MaxOpenConns int // Max open connections
|
MaxOpenConns int // Max open connections
|
||||||
MaxIdleConns int // Max idle connections
|
MaxIdleConns int // Max idle connections
|
||||||
ConnMaxLifetime time.Duration // Connection max lifetime
|
ConnMaxLifetime time.Duration // Connection max lifetime
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeycloakConfig struct {
|
type KeycloakConfig struct {
|
||||||
@@ -44,6 +50,52 @@ type KeycloakConfig struct {
|
|||||||
Enabled bool
|
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 {
|
func LoadConfig() *Config {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
@@ -58,6 +110,13 @@ func LoadConfig() *Config {
|
|||||||
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
|
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
|
||||||
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true),
|
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
|
// Load database configurations
|
||||||
@@ -75,18 +134,18 @@ func (c *Config) loadDatabaseConfigs() {
|
|||||||
|
|
||||||
// Primary database configuration
|
// Primary database configuration
|
||||||
c.Databases["default"] = DatabaseConfig{
|
c.Databases["default"] = DatabaseConfig{
|
||||||
Name: "default",
|
Name: "default",
|
||||||
Type: getEnv("DB_CONNECTION", "postgres"),
|
Type: getEnv("DB_CONNECTION", "postgres"),
|
||||||
Host: getEnv("DB_HOST", "localhost"),
|
Host: getEnv("DB_HOST", "localhost"),
|
||||||
Port: getEnvAsInt("DB_PORT", 5432),
|
Port: getEnvAsInt("DB_PORT", 5432),
|
||||||
Username: getEnv("DB_USERNAME", ""),
|
Username: getEnv("DB_USERNAME", ""),
|
||||||
Password: getEnv("DB_PASSWORD", ""),
|
Password: getEnv("DB_PASSWORD", ""),
|
||||||
Database: getEnv("DB_DATABASE", "satu_db"),
|
Database: getEnv("DB_DATABASE", "satu_db"),
|
||||||
Schema: getEnv("DB_SCHEMA", "public"),
|
Schema: getEnv("DB_SCHEMA", "public"),
|
||||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
|
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
|
||||||
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
|
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
|
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// SATUDATA database configuration
|
// SATUDATA database configuration
|
||||||
@@ -111,9 +170,9 @@ func (c *Config) loadDatabaseConfigs() {
|
|||||||
|
|
||||||
// Parse specific database configurations
|
// Parse specific database configurations
|
||||||
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
||||||
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
||||||
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
||||||
strings.HasSuffix(key, "_NAME") {
|
strings.HasSuffix(key, "_NAME") {
|
||||||
|
|
||||||
segments := strings.Split(key, "_")
|
segments := strings.Split(key, "_")
|
||||||
if len(segments) >= 2 {
|
if len(segments) >= 2 {
|
||||||
@@ -131,25 +190,25 @@ func (c *Config) loadDatabaseConfigs() {
|
|||||||
// Create DatabaseConfig from parsed configurations for additional databases
|
// Create DatabaseConfig from parsed configurations for additional databases
|
||||||
for name, config := range dbConfigs {
|
for name, config := range dbConfigs {
|
||||||
// Skip empty configurations or system configurations
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
dbConfig := DatabaseConfig{
|
dbConfig := DatabaseConfig{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")),
|
Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")),
|
||||||
Host: getEnvFromMap(config, "host", "localhost"),
|
Host: getEnvFromMap(config, "host", "localhost"),
|
||||||
Port: getEnvAsIntFromMap(config, "port", 5432),
|
Port: getEnvAsIntFromMap(config, "port", 5432),
|
||||||
Username: getEnvFromMap(config, "username", ""),
|
Username: getEnvFromMap(config, "username", ""),
|
||||||
Password: getEnvFromMap(config, "password", ""),
|
Password: getEnvFromMap(config, "password", ""),
|
||||||
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
||||||
Schema: getEnvFromMap(config, "schema", "public"),
|
Schema: getEnvFromMap(config, "schema", "public"),
|
||||||
SSLMode: getEnvFromMap(config, "sslmode", "disable"),
|
SSLMode: getEnvFromMap(config, "sslmode", "disable"),
|
||||||
Path: getEnvFromMap(config, "path", ""),
|
Path: getEnvFromMap(config, "path", ""),
|
||||||
Options: getEnvFromMap(config, "options", ""),
|
Options: getEnvFromMap(config, "options", ""),
|
||||||
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25),
|
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25),
|
||||||
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25),
|
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")),
|
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if username is empty and it's not a system config
|
// Skip if username is empty and it's not a system config
|
||||||
@@ -199,18 +258,18 @@ func (c *Config) loadReadReplicaConfigs() {
|
|||||||
if replicaConfig == nil {
|
if replicaConfig == nil {
|
||||||
// Create new replica config
|
// Create new replica config
|
||||||
newConfig := DatabaseConfig{
|
newConfig := DatabaseConfig{
|
||||||
Name: replicaKey,
|
Name: replicaKey,
|
||||||
Type: c.Databases[dbName].Type,
|
Type: c.Databases[dbName].Type,
|
||||||
Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host),
|
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),
|
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),
|
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),
|
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),
|
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),
|
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),
|
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),
|
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),
|
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")),
|
ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig)
|
c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig)
|
||||||
replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1]
|
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", "")
|
host := getEnv(strings.ToUpper(prefix)+"_HOST", "")
|
||||||
if host != "" {
|
if host != "" {
|
||||||
dbConfig := DatabaseConfig{
|
dbConfig := DatabaseConfig{
|
||||||
Name: prefix,
|
Name: prefix,
|
||||||
Type: connection,
|
Type: connection,
|
||||||
Host: host,
|
Host: host,
|
||||||
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432),
|
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432),
|
||||||
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
|
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
|
||||||
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
|
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
|
||||||
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
|
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
|
||||||
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"),
|
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"),
|
||||||
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"),
|
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25),
|
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25),
|
||||||
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25),
|
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")),
|
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
c.Databases[prefix] = dbConfig
|
c.Databases[prefix] = dbConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostgreSQL database
|
// PostgreSQL database
|
||||||
func (c *Config) addPostgreSQLConfigs() {
|
func (c *Config) addPostgreSQLConfigs() {
|
||||||
// SATUDATA database configuration
|
// SATUDATA database configuration
|
||||||
// defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost")
|
// defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost")
|
||||||
// if defaultPOSTGRESHost != "" {
|
// if defaultPOSTGRESHost != "" {
|
||||||
// c.Databases["postgres"] = DatabaseConfig{
|
// c.Databases["postgres"] = DatabaseConfig{
|
||||||
// Name: "postgres",
|
// Name: "postgres",
|
||||||
// Type: getEnv("POSTGRES_CONNECTION", "postgres"),
|
// Type: getEnv("POSTGRES_CONNECTION", "postgres"),
|
||||||
// Host: defaultPOSTGRESHost,
|
// Host: defaultPOSTGRESHost,
|
||||||
// Port: getEnvAsInt("POSTGRES_PORT", 5432),
|
// Port: getEnvAsInt("POSTGRES_PORT", 5432),
|
||||||
// Username: getEnv("POSTGRES_USERNAME", ""),
|
// Username: getEnv("POSTGRES_USERNAME", ""),
|
||||||
// Password: getEnv("POSTGRES_PASSWORD", ""),
|
// Password: getEnv("POSTGRES_PASSWORD", ""),
|
||||||
// Database: getEnv("POSTGRES_DATABASE", "postgres"),
|
// Database: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||||
// Schema: getEnv("POSTGRES_SCHEMA", "public"),
|
// Schema: getEnv("POSTGRES_SCHEMA", "public"),
|
||||||
// SSLMode: getEnv("POSTGRES_SSLMODE", "disable"),
|
// SSLMode: getEnv("POSTGRES_SSLMODE", "disable"),
|
||||||
// MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
// MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||||
// MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
// MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||||
// ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
// ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// Support for custom PostgreSQL configurations with POSTGRES_ prefix
|
// Support for custom PostgreSQL configurations with POSTGRES_ prefix
|
||||||
envVars := os.Environ()
|
envVars := os.Environ()
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
parts := strings.SplitN(envVar, "=", 2)
|
parts := strings.SplitN(envVar, "=", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := parts[0]
|
key := parts[0]
|
||||||
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
|
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
|
||||||
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
|
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
|
||||||
segments := strings.Split(key, "_")
|
segments := strings.Split(key, "_")
|
||||||
if len(segments) >= 3 {
|
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 PostgreSQL configuration
|
// Skip if it's a standard PostgreSQL configuration
|
||||||
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
|
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update PostgreSQL configuration
|
// Create or update PostgreSQL configuration
|
||||||
if _, exists := c.Databases[dbName]; !exists {
|
if _, exists := c.Databases[dbName]; !exists {
|
||||||
c.Databases[dbName] = DatabaseConfig{
|
c.Databases[dbName] = DatabaseConfig{
|
||||||
Name: dbName,
|
Name: dbName,
|
||||||
Type: "postgres",
|
Type: "postgres",
|
||||||
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||||
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
|
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
|
||||||
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||||
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||||
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||||
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
|
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
|
||||||
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||||
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// addMYSQLConfigs adds MYSQL database
|
// addMYSQLConfigs adds MYSQL database
|
||||||
func (c *Config) addMySQLConfigs() {
|
func (c *Config) addMySQLConfigs() {
|
||||||
// Primary MySQL configuration
|
// Primary MySQL configuration
|
||||||
defaultMySQLHost := getEnv("MYSQL_HOST", "")
|
defaultMySQLHost := getEnv("MYSQL_HOST", "")
|
||||||
if defaultMySQLHost != "" {
|
if defaultMySQLHost != "" {
|
||||||
c.Databases["mysql"] = DatabaseConfig{
|
c.Databases["mysql"] = DatabaseConfig{
|
||||||
Name: "mysql",
|
Name: "mysql",
|
||||||
Type: getEnv("MYSQL_CONNECTION", "mysql"),
|
Type: getEnv("MYSQL_CONNECTION", "mysql"),
|
||||||
Host: defaultMySQLHost,
|
Host: defaultMySQLHost,
|
||||||
Port: getEnvAsInt("MYSQL_PORT", 3306),
|
Port: getEnvAsInt("MYSQL_PORT", 3306),
|
||||||
Username: getEnv("MYSQL_USERNAME", ""),
|
Username: getEnv("MYSQL_USERNAME", ""),
|
||||||
Password: getEnv("MYSQL_PASSWORD", ""),
|
Password: getEnv("MYSQL_PASSWORD", ""),
|
||||||
Database: getEnv("MYSQL_DATABASE", "mysql"),
|
Database: getEnv("MYSQL_DATABASE", "mysql"),
|
||||||
SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
|
SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support for custom MySQL configurations with MYSQL_ prefix
|
// Support for custom MySQL configurations with MYSQL_ prefix
|
||||||
envVars := os.Environ()
|
envVars := os.Environ()
|
||||||
for _, envVar := range envVars {
|
for _, envVar := range envVars {
|
||||||
parts := strings.SplitN(envVar, "=", 2)
|
parts := strings.SplitN(envVar, "=", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
key := parts[0]
|
key := parts[0]
|
||||||
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
|
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
|
||||||
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
|
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
|
||||||
segments := strings.Split(key, "_")
|
segments := strings.Split(key, "_")
|
||||||
if len(segments) >= 3 {
|
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 MySQL configuration
|
// Skip if it's a standard MySQL configuration
|
||||||
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
|
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or update MySQL configuration
|
// Create or update MySQL configuration
|
||||||
if _, exists := c.Databases[dbName]; !exists {
|
if _, exists := c.Databases[dbName]; !exists {
|
||||||
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
|
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
|
||||||
if mysqlHost != "" {
|
if mysqlHost != "" {
|
||||||
c.Databases[dbName] = DatabaseConfig{
|
c.Databases[dbName] = DatabaseConfig{
|
||||||
Name: dbName,
|
Name: dbName,
|
||||||
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
|
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
|
||||||
Host: mysqlHost,
|
Host: mysqlHost,
|
||||||
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
|
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
|
||||||
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||||
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||||
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||||
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// addMongoDBConfigs adds MongoDB database configurations from environment variables
|
// addMongoDBConfigs adds MongoDB database configurations from environment variables
|
||||||
func (c *Config) addMongoDBConfigs() {
|
func (c *Config) addMongoDBConfigs() {
|
||||||
// Primary MongoDB configuration
|
// Primary MongoDB configuration
|
||||||
mongoHost := getEnv("MONGODB_HOST", "")
|
mongoHost := getEnv("MONGODB_HOST", "")
|
||||||
if mongoHost != "" {
|
if mongoHost != "" {
|
||||||
c.Databases["mongodb"] = DatabaseConfig{
|
c.Databases["mongodb"] = DatabaseConfig{
|
||||||
Name: "mongodb",
|
Name: "mongodb",
|
||||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||||
Host: mongoHost,
|
Host: mongoHost,
|
||||||
Port: getEnvAsInt("MONGODB_PORT", 27017),
|
Port: getEnvAsInt("MONGODB_PORT", 27017),
|
||||||
Username: getEnv("MONGODB_USER", ""),
|
Username: getEnv("MONGODB_USER", ""),
|
||||||
Password: getEnv("MONGODB_PASS", ""),
|
Password: getEnv("MONGODB_PASS", ""),
|
||||||
Database: getEnv("MONGODB_MASTER", "master"),
|
Database: getEnv("MONGODB_MASTER", "master"),
|
||||||
SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
|
SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,17 +476,17 @@ func (c *Config) addMongoDBConfigs() {
|
|||||||
mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "")
|
mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "")
|
||||||
if mongoLocalHost != "" {
|
if mongoLocalHost != "" {
|
||||||
c.Databases["mongodb_local"] = DatabaseConfig{
|
c.Databases["mongodb_local"] = DatabaseConfig{
|
||||||
Name: "mongodb_local",
|
Name: "mongodb_local",
|
||||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||||
Host: mongoLocalHost,
|
Host: mongoLocalHost,
|
||||||
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
|
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
|
||||||
Username: getEnv("MONGODB_LOCAL_USER", ""),
|
Username: getEnv("MONGODB_LOCAL_USER", ""),
|
||||||
Password: getEnv("MONGODB_LOCAL_PASS", ""),
|
Password: getEnv("MONGODB_LOCAL_PASS", ""),
|
||||||
Database: getEnv("MONGODB_LOCAL_DB", "local"),
|
Database: getEnv("MONGODB_LOCAL_DB", "local"),
|
||||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,17 +512,17 @@ func (c *Config) addMongoDBConfigs() {
|
|||||||
// Create or update MongoDB configuration
|
// Create or update MongoDB configuration
|
||||||
if _, exists := c.Databases[dbName]; !exists {
|
if _, exists := c.Databases[dbName]; !exists {
|
||||||
c.Databases[dbName] = DatabaseConfig{
|
c.Databases[dbName] = DatabaseConfig{
|
||||||
Name: dbName,
|
Name: dbName,
|
||||||
Type: "mongodb",
|
Type: "mongodb",
|
||||||
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||||
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
|
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
|
||||||
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
|
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
|
||||||
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
|
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
|
||||||
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
|
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
|
||||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
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
|
// Validate Keycloak configuration if enabled
|
||||||
if c.Keycloak.Enabled {
|
if c.Keycloak.Enabled {
|
||||||
if c.Keycloak.Issuer == "" {
|
if c.Keycloak.Issuer == "" {
|
||||||
|
|||||||
92
internal/handlers/bpjs/peserta.go
Normal file
92
internal/handlers/bpjs/peserta.go
Normal file
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
111
internal/helpers/bpjs/lz-string.go
Normal file
111
internal/helpers/bpjs/lz-string.go
Normal file
@@ -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.")
|
||||||
|
}
|
||||||
25
internal/helpers/bpjs/pkcs7.go
Normal file
25
internal/helpers/bpjs/pkcs7.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
84
internal/models/bpjs/peserta.go
Normal file
84
internal/models/bpjs/peserta.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
orderHandlers "api-service/internal/handlers/order"
|
bpjsPesertaHandlers "api-service/internal/handlers/bpjs"
|
||||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"api-service/internal/config"
|
"api-service/internal/config"
|
||||||
"api-service/internal/middleware"
|
"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", tokenHandler.GenerateToken)
|
||||||
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
|
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
|
||||||
|
|
||||||
|
|
||||||
// Retribusi endpoints
|
// Retribusi endpoints
|
||||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||||
v1.GET("/retribusis", retribusiHandler.GetRetribusi)
|
v1.GET("/retribusis", retribusiHandler.GetRetribusi)
|
||||||
@@ -59,35 +57,16 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
|||||||
v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi)
|
v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi)
|
||||||
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
|
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
|
||||||
|
|
||||||
// Protected routes (require authentication)
|
// BPJS endpoints
|
||||||
|
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||||
// Order endpoints
|
v1.GET("/bpjs/Peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
|
||||||
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)
|
|
||||||
|
|
||||||
protected := v1.Group("/")
|
protected := v1.Group("/")
|
||||||
protected.Use(middleware.JWTAuthMiddleware(authService))
|
protected.Use(middleware.JWTAuthMiddleware(authService))
|
||||||
{
|
{
|
||||||
// WebSocket endpoint
|
// Protected routes (require authentication)
|
||||||
protected.GET("/websocket", WebSocketHandler)
|
|
||||||
protected.GET("/webservice", WebServiceHandler)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return router
|
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"})
|
|
||||||
}
|
|
||||||
|
|||||||
59
internal/services/bpjs/response.go
Normal file
59
internal/services/bpjs/response.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
311
internal/services/bpjs/vclaimBridge.go
Normal file
311
internal/services/bpjs/vclaimBridge.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user