Creat Service BPJS
This commit is contained in:
@@ -72,7 +72,7 @@ tools/generate.bat product get post put delete
|
||||
# Atau langsung dengan Go
|
||||
go run tools/generate-handler.go product get post
|
||||
|
||||
go run tools/ generate-handler.go order get post put delete stats
|
||||
go run tools/generate-handler.go order get post put delete stats
|
||||
```
|
||||
|
||||
### Method Tersedia
|
||||
|
||||
519
docs/docs.go
519
docs/docs.go
@@ -24,56 +24,6 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/api/v1/Order/{id}": {
|
||||
"get": {
|
||||
"description": "Returns a single order by ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Order"
|
||||
],
|
||||
"summary": "Get Order by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate user with username and password to receive JWT token",
|
||||
@@ -264,116 +214,9 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/order/{id}": {
|
||||
"put": {
|
||||
"description": "Updates an existing order record",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Update order",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Order update request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order updated successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request or validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Soft deletes a order by setting status to 'deleted'",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Delete order",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orders": {
|
||||
"/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
|
||||
"get": {
|
||||
"description": "Returns a paginated list of orders with optional summary statistics",
|
||||
"description": "Search participant data based on Population NIK and service date",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -381,142 +224,52 @@ const docTemplate = `{
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
"bpjs"
|
||||
],
|
||||
"summary": "Get order with pagination and optional aggregation",
|
||||
"summary": "Get participant data by NIK",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Limit (max 100)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Include aggregation summary",
|
||||
"name": "include_summary",
|
||||
"in": "query"
|
||||
"type": "string",
|
||||
"description": "NIK KTP",
|
||||
"name": "nik",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search in multiple fields",
|
||||
"name": "search",
|
||||
"in": "query"
|
||||
"description": "Service date/SEP date (format: yyyy-MM-dd)",
|
||||
"name": "tglSEP",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success response",
|
||||
"description": "Participant data",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Participant not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new order record",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Create order",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Order creation request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Order created successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request or validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orders/stats": {
|
||||
"get": {
|
||||
"description": "Returns comprehensive statistics about order data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Get order statistics",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter statistics by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Statistics data",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -986,224 +739,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.AggregateData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"by_status": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"created_today": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_updated": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_active": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_draft": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_inactive": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_today": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.MetaResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current_page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"has_next": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_prev": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_pages": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.NullableInt32": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int32": {
|
||||
"type": "integer"
|
||||
},
|
||||
"valid": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.Order": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date_created": {
|
||||
"$ref": "#/definitions/sql.NullTime"
|
||||
},
|
||||
"date_updated": {
|
||||
"$ref": "#/definitions/sql.NullTime"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
},
|
||||
"sort": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_created": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
},
|
||||
"user_updated": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"draft",
|
||||
"active",
|
||||
"inactive"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderCreateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderDeleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderGetByIDResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderGetResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderUpdateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"draft",
|
||||
"active",
|
||||
"inactive"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderUpdateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_retribusi.AggregateData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -22,56 +22,6 @@
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/api/v1/Order/{id}": {
|
||||
"get": {
|
||||
"description": "Returns a single order by ID",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Order"
|
||||
],
|
||||
"summary": "Get Order by ID",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success response",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/auth/login": {
|
||||
"post": {
|
||||
"description": "Authenticate user with username and password to receive JWT token",
|
||||
@@ -262,116 +212,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/order/{id}": {
|
||||
"put": {
|
||||
"description": "Updates an existing order record",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Update order",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Order update request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order updated successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request or validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "Soft deletes a order by setting status to 'deleted'",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Delete order",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Order ID (UUID)",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Order deleted successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID format",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orders": {
|
||||
"/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
|
||||
"get": {
|
||||
"description": "Returns a paginated list of orders with optional summary statistics",
|
||||
"description": "Search participant data based on Population NIK and service date",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
@@ -379,142 +222,52 @@
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
"bpjs"
|
||||
],
|
||||
"summary": "Get order with pagination and optional aggregation",
|
||||
"summary": "Get participant data by NIK",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 10,
|
||||
"description": "Limit (max 100)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"default": 0,
|
||||
"description": "Offset",
|
||||
"name": "offset",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Include aggregation summary",
|
||||
"name": "include_summary",
|
||||
"in": "query"
|
||||
"type": "string",
|
||||
"description": "NIK KTP",
|
||||
"name": "nik",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Search in multiple fields",
|
||||
"name": "search",
|
||||
"in": "query"
|
||||
"description": "Service date/SEP date (format: yyyy-MM-dd)",
|
||||
"name": "tglSEP",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success response",
|
||||
"description": "Participant data",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Participant not found",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "Creates a new order record",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Create order",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Order creation request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Order created successfully",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request or validation error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/orders/stats": {
|
||||
"get": {
|
||||
"description": "Returns comprehensive statistics about order data",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"order"
|
||||
],
|
||||
"summary": "Get order statistics",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Filter statistics by status",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Statistics data",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal server error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,224 +737,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.AggregateData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"by_status": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"created_today": {
|
||||
"type": "integer"
|
||||
},
|
||||
"last_updated": {
|
||||
"type": "string"
|
||||
},
|
||||
"total_active": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_draft": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_inactive": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_today": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.ErrorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "integer"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.MetaResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current_page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"has_next": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_prev": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total_pages": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.NullableInt32": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int32": {
|
||||
"type": "integer"
|
||||
},
|
||||
"valid": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.Order": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date_created": {
|
||||
"$ref": "#/definitions/sql.NullTime"
|
||||
},
|
||||
"date_updated": {
|
||||
"$ref": "#/definitions/sql.NullTime"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
},
|
||||
"sort": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_created": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
},
|
||||
"user_updated": {
|
||||
"$ref": "#/definitions/sql.NullString"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderCreateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"draft",
|
||||
"active",
|
||||
"inactive"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderCreateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderDeleteResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderGetByIDResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderGetResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderUpdateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"draft",
|
||||
"active",
|
||||
"inactive"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_order.OrderUpdateResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/api-service_internal_models_order.Order"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"api-service_internal_models_retribusi.AggregateData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -30,150 +30,6 @@ definitions:
|
||||
username:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_order.AggregateData:
|
||||
properties:
|
||||
by_status:
|
||||
additionalProperties:
|
||||
type: integer
|
||||
type: object
|
||||
created_today:
|
||||
type: integer
|
||||
last_updated:
|
||||
type: string
|
||||
total_active:
|
||||
type: integer
|
||||
total_draft:
|
||||
type: integer
|
||||
total_inactive:
|
||||
type: integer
|
||||
updated_today:
|
||||
type: integer
|
||||
type: object
|
||||
api-service_internal_models_order.ErrorResponse:
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
error:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
timestamp:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_order.MetaResponse:
|
||||
properties:
|
||||
current_page:
|
||||
type: integer
|
||||
has_next:
|
||||
type: boolean
|
||||
has_prev:
|
||||
type: boolean
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
total_pages:
|
||||
type: integer
|
||||
type: object
|
||||
api-service_internal_models_order.NullableInt32:
|
||||
properties:
|
||||
int32:
|
||||
type: integer
|
||||
valid:
|
||||
type: boolean
|
||||
type: object
|
||||
api-service_internal_models_order.Order:
|
||||
properties:
|
||||
date_created:
|
||||
$ref: '#/definitions/sql.NullTime'
|
||||
date_updated:
|
||||
$ref: '#/definitions/sql.NullTime'
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
sort:
|
||||
$ref: '#/definitions/api-service_internal_models_order.NullableInt32'
|
||||
status:
|
||||
type: string
|
||||
user_created:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
user_updated:
|
||||
$ref: '#/definitions/sql.NullString'
|
||||
type: object
|
||||
api-service_internal_models_order.OrderCreateRequest:
|
||||
properties:
|
||||
name:
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
status:
|
||||
enum:
|
||||
- draft
|
||||
- active
|
||||
- inactive
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
api-service_internal_models_order.OrderCreateResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_order.OrderDeleteResponse:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_order.OrderGetByIDResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_order.OrderGetResponse:
|
||||
properties:
|
||||
data:
|
||||
items:
|
||||
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||
type: array
|
||||
message:
|
||||
type: string
|
||||
meta:
|
||||
$ref: '#/definitions/api-service_internal_models_order.MetaResponse'
|
||||
summary:
|
||||
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
|
||||
type: object
|
||||
api-service_internal_models_order.OrderUpdateRequest:
|
||||
properties:
|
||||
name:
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
type: string
|
||||
status:
|
||||
enum:
|
||||
- draft
|
||||
- active
|
||||
- inactive
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
api-service_internal_models_order.OrderUpdateResponse:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/definitions/api-service_internal_models_order.Order'
|
||||
message:
|
||||
type: string
|
||||
type: object
|
||||
api-service_internal_models_retribusi.AggregateData:
|
||||
properties:
|
||||
by_dinas:
|
||||
@@ -466,39 +322,6 @@ info:
|
||||
title: API Service
|
||||
version: 1.0.0
|
||||
paths:
|
||||
/api/v1/Order/{id}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a single order by ID
|
||||
parameters:
|
||||
- description: Order ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success response
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderGetByIDResponse'
|
||||
"400":
|
||||
description: Invalid ID format
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"404":
|
||||
description: order not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Get Order by ID
|
||||
tags:
|
||||
- Order
|
||||
/api/v1/auth/login:
|
||||
post:
|
||||
consumes:
|
||||
@@ -622,177 +445,48 @@ paths:
|
||||
summary: Register new user
|
||||
tags:
|
||||
- Authentication
|
||||
/api/v1/order/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Soft deletes a order by setting status to 'deleted'
|
||||
parameters:
|
||||
- description: Order ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Order deleted successfully
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderDeleteResponse'
|
||||
"400":
|
||||
description: Invalid ID format
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"404":
|
||||
description: Order not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Delete order
|
||||
tags:
|
||||
- order
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates an existing order record
|
||||
parameters:
|
||||
- description: Order ID (UUID)
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: Order update request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Order updated successfully
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateResponse'
|
||||
"400":
|
||||
description: Bad request or validation error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"404":
|
||||
description: Order not found
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Update order
|
||||
tags:
|
||||
- order
|
||||
/api/v1/orders:
|
||||
/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a paginated list of orders with optional summary statistics
|
||||
description: Search participant data based on Population NIK and service date
|
||||
parameters:
|
||||
- default: 10
|
||||
description: Limit (max 100)
|
||||
in: query
|
||||
name: limit
|
||||
type: integer
|
||||
- default: 0
|
||||
description: Offset
|
||||
in: query
|
||||
name: offset
|
||||
type: integer
|
||||
- default: false
|
||||
description: Include aggregation summary
|
||||
in: query
|
||||
name: include_summary
|
||||
type: boolean
|
||||
- description: Filter by status
|
||||
in: query
|
||||
name: status
|
||||
- description: NIK KTP
|
||||
in: path
|
||||
name: nik
|
||||
required: true
|
||||
type: string
|
||||
- description: Search in multiple fields
|
||||
in: query
|
||||
name: search
|
||||
- description: 'Service date/SEP date (format: yyyy-MM-dd)'
|
||||
in: path
|
||||
name: tglSEP
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Success response
|
||||
description: Participant data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderGetResponse'
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"400":
|
||||
description: Bad request
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"404":
|
||||
description: Participant not found
|
||||
schema:
|
||||
additionalProperties: true
|
||||
type: object
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Get order with pagination and optional aggregation
|
||||
additionalProperties: true
|
||||
type: object
|
||||
summary: Get participant data by NIK
|
||||
tags:
|
||||
- order
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Creates a new order record
|
||||
parameters:
|
||||
- description: Order creation request
|
||||
in: body
|
||||
name: request
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderCreateRequest'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Order created successfully
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.OrderCreateResponse'
|
||||
"400":
|
||||
description: Bad request or validation error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Create order
|
||||
tags:
|
||||
- order
|
||||
/api/v1/orders/stats:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns comprehensive statistics about order data
|
||||
parameters:
|
||||
- description: Filter statistics by status
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Statistics data
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
|
||||
"500":
|
||||
description: Internal server error
|
||||
schema:
|
||||
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
|
||||
summary: Get order statistics
|
||||
tags:
|
||||
- order
|
||||
- bpjs
|
||||
/api/v1/retribusi/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
|
||||
@@ -63,3 +63,9 @@ KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox
|
||||
KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran
|
||||
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
|
||||
KEYCLOAK_ENABLED=false
|
||||
|
||||
# BPJS Configuration
|
||||
BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
|
||||
BPJS_CONSID=5257
|
||||
BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482
|
||||
BPJS_SECRETKEY=1bV363512D
|
||||
3
go.mod
3
go.mod
@@ -16,8 +16,10 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mashingan/smapping v0.1.19
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.6
|
||||
@@ -39,7 +41,6 @@ require (
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
|
||||
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.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ=
|
||||
github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -9,10 +14,11 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Databases map[string]DatabaseConfig
|
||||
Server ServerConfig
|
||||
Databases map[string]DatabaseConfig
|
||||
ReadReplicas map[string][]DatabaseConfig // For read replicas
|
||||
Keycloak KeycloakConfig
|
||||
Keycloak KeycloakConfig
|
||||
Bpjs BpjsConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -21,20 +27,20 @@ type ServerConfig struct {
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Name string
|
||||
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Database string
|
||||
Schema string
|
||||
SSLMode string
|
||||
Path string // For SQLite
|
||||
Options string // Additional connection options
|
||||
MaxOpenConns int // Max open connections
|
||||
MaxIdleConns int // Max idle connections
|
||||
ConnMaxLifetime time.Duration // Connection max lifetime
|
||||
Name string
|
||||
Type string // postgres, mysql, sqlserver, sqlite, mongodb
|
||||
Host string
|
||||
Port int
|
||||
Username string
|
||||
Password string
|
||||
Database string
|
||||
Schema string
|
||||
SSLMode string
|
||||
Path string // For SQLite
|
||||
Options string // Additional connection options
|
||||
MaxOpenConns int // Max open connections
|
||||
MaxIdleConns int // Max idle connections
|
||||
ConnMaxLifetime time.Duration // Connection max lifetime
|
||||
}
|
||||
|
||||
type KeycloakConfig struct {
|
||||
@@ -44,6 +50,52 @@ type KeycloakConfig struct {
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type BpjsConfig struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
ConsID string `json:"cons_id"`
|
||||
UserKey string `json:"user_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
// SetHeader generates required headers for BPJS VClaim API
|
||||
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
|
||||
timenow := time.Now().UTC()
|
||||
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tstamp := timenow.Unix() - t.Unix()
|
||||
secret := []byte(cfg.SecretKey)
|
||||
message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp))
|
||||
hash := hmac.New(sha256.New, secret)
|
||||
hash.Write(message)
|
||||
|
||||
// to lowercase hexits
|
||||
hex.EncodeToString(hash.Sum(nil))
|
||||
// to base64
|
||||
xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
|
||||
return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature
|
||||
}
|
||||
|
||||
type ConfigBpjs struct {
|
||||
Cons_id string
|
||||
Secret_key string
|
||||
User_key string
|
||||
}
|
||||
|
||||
// SetHeader for backward compatibility
|
||||
func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) {
|
||||
bpjsConfig := BpjsConfig{
|
||||
ConsID: cfg.Cons_id,
|
||||
SecretKey: cfg.Secret_key,
|
||||
UserKey: cfg.User_key,
|
||||
}
|
||||
return bpjsConfig.SetHeader()
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
config := &Config{
|
||||
Server: ServerConfig{
|
||||
@@ -58,11 +110,18 @@ func LoadConfig() *Config {
|
||||
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
|
||||
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true),
|
||||
},
|
||||
Bpjs: BpjsConfig{
|
||||
BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"),
|
||||
ConsID: getEnv("BPJS_CONSID", ""),
|
||||
UserKey: getEnv("BPJS_USERKEY", ""),
|
||||
SecretKey: getEnv("BPJS_SECRETKEY", ""),
|
||||
Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")),
|
||||
},
|
||||
}
|
||||
|
||||
// Load database configurations
|
||||
config.loadDatabaseConfigs()
|
||||
|
||||
|
||||
// Load read replica configurations
|
||||
config.loadReadReplicaConfigs()
|
||||
|
||||
@@ -72,21 +131,21 @@ func LoadConfig() *Config {
|
||||
func (c *Config) loadDatabaseConfigs() {
|
||||
// Simplified approach: Directly load from environment variables
|
||||
// This ensures we get the exact values specified in .env
|
||||
|
||||
|
||||
// Primary database configuration
|
||||
c.Databases["default"] = DatabaseConfig{
|
||||
Name: "default",
|
||||
Type: getEnv("DB_CONNECTION", "postgres"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnvAsInt("DB_PORT", 5432),
|
||||
Username: getEnv("DB_USERNAME", ""),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Database: getEnv("DB_DATABASE", "satu_db"),
|
||||
Schema: getEnv("DB_SCHEMA", "public"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
|
||||
Name: "default",
|
||||
Type: getEnv("DB_CONNECTION", "postgres"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnvAsInt("DB_PORT", 5432),
|
||||
Username: getEnv("DB_USERNAME", ""),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
Database: getEnv("DB_DATABASE", "satu_db"),
|
||||
Schema: getEnv("DB_SCHEMA", "public"),
|
||||
SSLMode: getEnv("DB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
|
||||
// SATUDATA database configuration
|
||||
@@ -110,16 +169,16 @@ func (c *Config) loadDatabaseConfigs() {
|
||||
value := parts[1]
|
||||
|
||||
// Parse specific database configurations
|
||||
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
||||
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
||||
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
||||
strings.HasSuffix(key, "_NAME") {
|
||||
|
||||
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
|
||||
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
|
||||
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
|
||||
strings.HasSuffix(key, "_NAME") {
|
||||
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 2 {
|
||||
dbName := strings.ToLower(strings.Join(segments[:len(segments)-1], "_"))
|
||||
property := strings.ToLower(segments[len(segments)-1])
|
||||
|
||||
|
||||
if dbConfigs[dbName] == nil {
|
||||
dbConfigs[dbName] = make(map[string]string)
|
||||
}
|
||||
@@ -131,25 +190,25 @@ func (c *Config) loadDatabaseConfigs() {
|
||||
// Create DatabaseConfig from parsed configurations for additional databases
|
||||
for name, config := range dbConfigs {
|
||||
// Skip empty configurations or system configurations
|
||||
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" {
|
||||
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" {
|
||||
continue
|
||||
}
|
||||
|
||||
dbConfig := DatabaseConfig{
|
||||
Name: name,
|
||||
Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")),
|
||||
Host: getEnvFromMap(config, "host", "localhost"),
|
||||
Port: getEnvAsIntFromMap(config, "port", 5432),
|
||||
Username: getEnvFromMap(config, "username", ""),
|
||||
Password: getEnvFromMap(config, "password", ""),
|
||||
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
||||
Schema: getEnvFromMap(config, "schema", "public"),
|
||||
SSLMode: getEnvFromMap(config, "sslmode", "disable"),
|
||||
Path: getEnvFromMap(config, "path", ""),
|
||||
Options: getEnvFromMap(config, "options", ""),
|
||||
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25),
|
||||
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")),
|
||||
Name: name,
|
||||
Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")),
|
||||
Host: getEnvFromMap(config, "host", "localhost"),
|
||||
Port: getEnvAsIntFromMap(config, "port", 5432),
|
||||
Username: getEnvFromMap(config, "username", ""),
|
||||
Password: getEnvFromMap(config, "password", ""),
|
||||
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
|
||||
Schema: getEnvFromMap(config, "schema", "public"),
|
||||
SSLMode: getEnvFromMap(config, "sslmode", "disable"),
|
||||
Path: getEnvFromMap(config, "path", ""),
|
||||
Options: getEnvFromMap(config, "options", ""),
|
||||
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25),
|
||||
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")),
|
||||
}
|
||||
|
||||
// Skip if username is empty and it's not a system config
|
||||
@@ -182,7 +241,7 @@ func (c *Config) loadReadReplicaConfigs() {
|
||||
property := strings.ToLower(strings.Join(segments[4:], "_"))
|
||||
|
||||
replicaKey := dbName + "_replica_" + replicaIndex
|
||||
|
||||
|
||||
if c.ReadReplicas[dbName] == nil {
|
||||
c.ReadReplicas[dbName] = []DatabaseConfig{}
|
||||
}
|
||||
@@ -199,18 +258,18 @@ func (c *Config) loadReadReplicaConfigs() {
|
||||
if replicaConfig == nil {
|
||||
// Create new replica config
|
||||
newConfig := DatabaseConfig{
|
||||
Name: replicaKey,
|
||||
Type: c.Databases[dbName].Type,
|
||||
Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host),
|
||||
Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port),
|
||||
Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username),
|
||||
Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password),
|
||||
Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database),
|
||||
Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema),
|
||||
SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode),
|
||||
MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns),
|
||||
MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns),
|
||||
ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")),
|
||||
Name: replicaKey,
|
||||
Type: c.Databases[dbName].Type,
|
||||
Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host),
|
||||
Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port),
|
||||
Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username),
|
||||
Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password),
|
||||
Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database),
|
||||
Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema),
|
||||
SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode),
|
||||
MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns),
|
||||
MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns),
|
||||
ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig)
|
||||
replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1]
|
||||
@@ -249,167 +308,167 @@ func (c *Config) addSpecificDatabase(prefix, defaultType string) {
|
||||
host := getEnv(strings.ToUpper(prefix)+"_HOST", "")
|
||||
if host != "" {
|
||||
dbConfig := DatabaseConfig{
|
||||
Name: prefix,
|
||||
Type: connection,
|
||||
Host: host,
|
||||
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432),
|
||||
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
|
||||
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
|
||||
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
|
||||
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"),
|
||||
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")),
|
||||
Name: prefix,
|
||||
Type: connection,
|
||||
Host: host,
|
||||
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432),
|
||||
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
|
||||
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
|
||||
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
|
||||
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"),
|
||||
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
c.Databases[prefix] = dbConfig
|
||||
}
|
||||
}
|
||||
|
||||
// PostgreSQL database
|
||||
func (c *Config) addPostgreSQLConfigs() {
|
||||
// SATUDATA database configuration
|
||||
// defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost")
|
||||
// if defaultPOSTGRESHost != "" {
|
||||
// c.Databases["postgres"] = DatabaseConfig{
|
||||
// Name: "postgres",
|
||||
// Type: getEnv("POSTGRES_CONNECTION", "postgres"),
|
||||
// Host: defaultPOSTGRESHost,
|
||||
// Port: getEnvAsInt("POSTGRES_PORT", 5432),
|
||||
// Username: getEnv("POSTGRES_USERNAME", ""),
|
||||
// Password: getEnv("POSTGRES_PASSWORD", ""),
|
||||
// Database: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||
// Schema: getEnv("POSTGRES_SCHEMA", "public"),
|
||||
// SSLMode: getEnv("POSTGRES_SSLMODE", "disable"),
|
||||
// MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||
// MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||
// ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||
// }
|
||||
// }
|
||||
// SATUDATA database configuration
|
||||
// defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost")
|
||||
// if defaultPOSTGRESHost != "" {
|
||||
// c.Databases["postgres"] = DatabaseConfig{
|
||||
// Name: "postgres",
|
||||
// Type: getEnv("POSTGRES_CONNECTION", "postgres"),
|
||||
// Host: defaultPOSTGRESHost,
|
||||
// Port: getEnvAsInt("POSTGRES_PORT", 5432),
|
||||
// Username: getEnv("POSTGRES_USERNAME", ""),
|
||||
// Password: getEnv("POSTGRES_PASSWORD", ""),
|
||||
// Database: getEnv("POSTGRES_DATABASE", "postgres"),
|
||||
// Schema: getEnv("POSTGRES_SCHEMA", "public"),
|
||||
// SSLMode: getEnv("POSTGRES_SSLMODE", "disable"),
|
||||
// MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||
// MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||
// ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||
// }
|
||||
// }
|
||||
|
||||
// Support for custom PostgreSQL configurations with POSTGRES_ prefix
|
||||
envVars := os.Environ()
|
||||
for _, envVar := range envVars {
|
||||
parts := strings.SplitN(envVar, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
// Support for custom PostgreSQL configurations with POSTGRES_ prefix
|
||||
envVars := os.Environ()
|
||||
for _, envVar := range envVars {
|
||||
parts := strings.SplitN(envVar, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := parts[0]
|
||||
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
|
||||
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 3 {
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
|
||||
// Skip if it's a standard PostgreSQL configuration
|
||||
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
|
||||
continue
|
||||
}
|
||||
key := parts[0]
|
||||
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
|
||||
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 3 {
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
|
||||
// Create or update PostgreSQL configuration
|
||||
if _, exists := c.Databases[dbName]; !exists {
|
||||
c.Databases[dbName] = DatabaseConfig{
|
||||
Name: dbName,
|
||||
Type: "postgres",
|
||||
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
|
||||
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
|
||||
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip if it's a standard PostgreSQL configuration
|
||||
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create or update PostgreSQL configuration
|
||||
if _, exists := c.Databases[dbName]; !exists {
|
||||
c.Databases[dbName] = DatabaseConfig{
|
||||
Name: dbName,
|
||||
Type: "postgres",
|
||||
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
|
||||
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
|
||||
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addMYSQLConfigs adds MYSQL database
|
||||
func (c *Config) addMySQLConfigs() {
|
||||
// Primary MySQL configuration
|
||||
defaultMySQLHost := getEnv("MYSQL_HOST", "")
|
||||
if defaultMySQLHost != "" {
|
||||
c.Databases["mysql"] = DatabaseConfig{
|
||||
Name: "mysql",
|
||||
Type: getEnv("MYSQL_CONNECTION", "mysql"),
|
||||
Host: defaultMySQLHost,
|
||||
Port: getEnvAsInt("MYSQL_PORT", 3306),
|
||||
Username: getEnv("MYSQL_USERNAME", ""),
|
||||
Password: getEnv("MYSQL_PASSWORD", ""),
|
||||
Database: getEnv("MYSQL_DATABASE", "mysql"),
|
||||
SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
// Primary MySQL configuration
|
||||
defaultMySQLHost := getEnv("MYSQL_HOST", "")
|
||||
if defaultMySQLHost != "" {
|
||||
c.Databases["mysql"] = DatabaseConfig{
|
||||
Name: "mysql",
|
||||
Type: getEnv("MYSQL_CONNECTION", "mysql"),
|
||||
Host: defaultMySQLHost,
|
||||
Port: getEnvAsInt("MYSQL_PORT", 3306),
|
||||
Username: getEnv("MYSQL_USERNAME", ""),
|
||||
Password: getEnv("MYSQL_PASSWORD", ""),
|
||||
Database: getEnv("MYSQL_DATABASE", "mysql"),
|
||||
SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
|
||||
// Support for custom MySQL configurations with MYSQL_ prefix
|
||||
envVars := os.Environ()
|
||||
for _, envVar := range envVars {
|
||||
parts := strings.SplitN(envVar, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
// Support for custom MySQL configurations with MYSQL_ prefix
|
||||
envVars := os.Environ()
|
||||
for _, envVar := range envVars {
|
||||
parts := strings.SplitN(envVar, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := parts[0]
|
||||
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
|
||||
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 3 {
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
|
||||
// Skip if it's a standard MySQL configuration
|
||||
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
|
||||
continue
|
||||
}
|
||||
key := parts[0]
|
||||
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
|
||||
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 3 {
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
|
||||
// Create or update MySQL configuration
|
||||
if _, exists := c.Databases[dbName]; !exists {
|
||||
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
|
||||
if mysqlHost != "" {
|
||||
c.Databases[dbName] = DatabaseConfig{
|
||||
Name: dbName,
|
||||
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
|
||||
Host: mysqlHost,
|
||||
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
|
||||
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip if it's a standard MySQL configuration
|
||||
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create or update MySQL configuration
|
||||
if _, exists := c.Databases[dbName]; !exists {
|
||||
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
|
||||
if mysqlHost != "" {
|
||||
c.Databases[dbName] = DatabaseConfig{
|
||||
Name: dbName,
|
||||
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
|
||||
Host: mysqlHost,
|
||||
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
|
||||
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
|
||||
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
|
||||
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
|
||||
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
|
||||
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// addMongoDBConfigs adds MongoDB database configurations from environment variables
|
||||
func (c *Config) addMongoDBConfigs() {
|
||||
// Primary MongoDB configuration
|
||||
mongoHost := getEnv("MONGODB_HOST", "")
|
||||
if mongoHost != "" {
|
||||
c.Databases["mongodb"] = DatabaseConfig{
|
||||
Name: "mongodb",
|
||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||
Host: mongoHost,
|
||||
Port: getEnvAsInt("MONGODB_PORT", 27017),
|
||||
Username: getEnv("MONGODB_USER", ""),
|
||||
Password: getEnv("MONGODB_PASS", ""),
|
||||
Database: getEnv("MONGODB_MASTER", "master"),
|
||||
SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
Name: "mongodb",
|
||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||
Host: mongoHost,
|
||||
Port: getEnvAsInt("MONGODB_PORT", 27017),
|
||||
Username: getEnv("MONGODB_USER", ""),
|
||||
Password: getEnv("MONGODB_PASS", ""),
|
||||
Database: getEnv("MONGODB_MASTER", "master"),
|
||||
SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,17 +476,17 @@ func (c *Config) addMongoDBConfigs() {
|
||||
mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "")
|
||||
if mongoLocalHost != "" {
|
||||
c.Databases["mongodb_local"] = DatabaseConfig{
|
||||
Name: "mongodb_local",
|
||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||
Host: mongoLocalHost,
|
||||
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
|
||||
Username: getEnv("MONGODB_LOCAL_USER", ""),
|
||||
Password: getEnv("MONGODB_LOCAL_PASS", ""),
|
||||
Database: getEnv("MONGODB_LOCAL_DB", "local"),
|
||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
Name: "mongodb_local",
|
||||
Type: getEnv("MONGODB_CONNECTION", "mongodb"),
|
||||
Host: mongoLocalHost,
|
||||
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
|
||||
Username: getEnv("MONGODB_LOCAL_USER", ""),
|
||||
Password: getEnv("MONGODB_LOCAL_PASS", ""),
|
||||
Database: getEnv("MONGODB_LOCAL_DB", "local"),
|
||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +503,7 @@ func (c *Config) addMongoDBConfigs() {
|
||||
if strings.HasPrefix(key, "MONGODB_") && strings.Contains(key, "_") {
|
||||
segments := strings.Split(key, "_")
|
||||
if len(segments) >= 3 {
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
|
||||
// Skip if it's a standard MongoDB configuration
|
||||
if dbName == "connection" || dbName == "dev" || dbName == "local" {
|
||||
continue
|
||||
@@ -453,17 +512,17 @@ func (c *Config) addMongoDBConfigs() {
|
||||
// Create or update MongoDB configuration
|
||||
if _, exists := c.Databases[dbName]; !exists {
|
||||
c.Databases[dbName] = DatabaseConfig{
|
||||
Name: dbName,
|
||||
Type: "mongodb",
|
||||
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
|
||||
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
|
||||
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
|
||||
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
|
||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
Name: dbName,
|
||||
Type: "mongodb",
|
||||
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
|
||||
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
|
||||
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
|
||||
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
|
||||
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
|
||||
SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
|
||||
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
|
||||
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
|
||||
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,6 +596,19 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bpjs.BaseURL == "" {
|
||||
log.Fatal("BPJS Base URL is required")
|
||||
}
|
||||
if c.Bpjs.ConsID == "" {
|
||||
log.Fatal("BPJS Consumer ID is required")
|
||||
}
|
||||
if c.Bpjs.UserKey == "" {
|
||||
log.Fatal("BPJS User Key is required")
|
||||
}
|
||||
if c.Bpjs.SecretKey == "" {
|
||||
log.Fatal("BPJS Secret Key is required")
|
||||
}
|
||||
|
||||
// Validate Keycloak configuration if enabled
|
||||
if c.Keycloak.Enabled {
|
||||
if c.Keycloak.Issuer == "" {
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
orderHandlers "api-service/internal/handlers/order"
|
||||
bpjsPesertaHandlers "api-service/internal/handlers/bpjs"
|
||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||
"net/http"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/middleware"
|
||||
@@ -50,7 +49,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
v1.POST("/token/generate", tokenHandler.GenerateToken)
|
||||
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
|
||||
|
||||
|
||||
// Retribusi endpoints
|
||||
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
|
||||
v1.GET("/retribusis", retribusiHandler.GetRetribusi)
|
||||
@@ -59,35 +57,16 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi)
|
||||
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
|
||||
|
||||
// Protected routes (require authentication)
|
||||
|
||||
// Order endpoints
|
||||
orderHandler := orderHandlers.NewOrderHandler()
|
||||
v1.GET("/orders", orderHandler.GetOrder)
|
||||
v1.GET("/order/:id", orderHandler.GetOrderByID)
|
||||
v1.POST("/orders", orderHandler.CreateOrder)
|
||||
v1.PUT("/order/:id", orderHandler.UpdateOrder)
|
||||
v1.DELETE("/order/:id", orderHandler.DeleteOrder)
|
||||
// BPJS endpoints
|
||||
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
|
||||
v1.GET("/bpjs/Peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
|
||||
|
||||
protected := v1.Group("/")
|
||||
protected.Use(middleware.JWTAuthMiddleware(authService))
|
||||
{
|
||||
// WebSocket endpoint
|
||||
protected.GET("/websocket", WebSocketHandler)
|
||||
protected.GET("/webservice", WebServiceHandler)
|
||||
// Protected routes (require authentication)
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// WebSocketHandler handles WebSocket connections
|
||||
func WebSocketHandler(c *gin.Context) {
|
||||
// This will be implemented with proper WebSocket handling
|
||||
c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"})
|
||||
}
|
||||
|
||||
func WebServiceHandler(c *gin.Context) {
|
||||
// This will be implemented with proper WebSocket handling
|
||||
c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"})
|
||||
}
|
||||
|
||||
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