Creat Service BPJS

This commit is contained in:
2025-08-18 18:09:41 +07:00
parent f953f6d646
commit 1c4f65ffd8
19 changed files with 1073 additions and 3323 deletions

View File

@@ -72,7 +72,7 @@ tools/generate.bat product get post put delete
# Atau langsung dengan Go # Atau langsung dengan Go
go run tools/generate-handler.go product get post go run tools/generate-handler.go product get post
go run tools/ generate-handler.go order get post put delete stats go run tools/generate-handler.go order get post put delete stats
``` ```
### Method Tersedia ### Method Tersedia

View File

@@ -24,56 +24,6 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/api/v1/Order/{id}": {
"get": {
"description": "Returns a single order by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Get Order by ID",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success response",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/auth/login": { "/api/v1/auth/login": {
"post": { "post": {
"description": "Authenticate user with username and password to receive JWT token", "description": "Authenticate user with username and password to receive JWT token",
@@ -264,116 +214,9 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/order/{id}": { "/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
"put": {
"description": "Updates an existing order record",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Update order",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Order update request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "Order updated successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
}
},
"400": {
"description": "Bad request or validation error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "Order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
},
"delete": {
"description": "Soft deletes a order by setting status to 'deleted'",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Delete order",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Order deleted successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "Order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/orders": {
"get": { "get": {
"description": "Returns a paginated list of orders with optional summary statistics", "description": "Search participant data based on Population NIK and service date",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -381,142 +224,52 @@ const docTemplate = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"order" "bpjs"
], ],
"summary": "Get order with pagination and optional aggregation", "summary": "Get participant data by NIK",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"default": 10, "description": "NIK KTP",
"description": "Limit (max 100)", "name": "nik",
"name": "limit", "in": "path",
"in": "query" "required": true
},
{
"type": "integer",
"default": 0,
"description": "Offset",
"name": "offset",
"in": "query"
},
{
"type": "boolean",
"default": false,
"description": "Include aggregation summary",
"name": "include_summary",
"in": "query"
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by status", "description": "Service date/SEP date (format: yyyy-MM-dd)",
"name": "status", "name": "tglSEP",
"in": "query" "in": "path",
}, "required": true
{
"type": "string",
"description": "Search in multiple fields",
"name": "search",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Success response", "description": "Participant data",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse" "type": "object",
"additionalProperties": true
} }
}, },
"400": { "400": {
"description": "Bad request", "description": "Bad request",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" "type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Participant not found",
"schema": {
"type": "object",
"additionalProperties": true
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal server error",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" "type": "object",
} "additionalProperties": true
}
}
},
"post": {
"description": "Creates a new order record",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Create order",
"parameters": [
{
"description": "Order creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
}
}
],
"responses": {
"201": {
"description": "Order created successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
}
},
"400": {
"description": "Bad request or validation error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/orders/stats": {
"get": {
"description": "Returns comprehensive statistics about order data",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Get order statistics",
"parameters": [
{
"type": "string",
"description": "Filter statistics by status",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "Statistics data",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
} }
} }
} }
@@ -986,224 +739,6 @@ const docTemplate = `{
} }
} }
}, },
"api-service_internal_models_order.AggregateData": {
"type": "object",
"properties": {
"by_status": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
},
"created_today": {
"type": "integer"
},
"last_updated": {
"type": "string"
},
"total_active": {
"type": "integer"
},
"total_draft": {
"type": "integer"
},
"total_inactive": {
"type": "integer"
},
"updated_today": {
"type": "integer"
}
}
},
"api-service_internal_models_order.ErrorResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"timestamp": {
"type": "string"
}
}
},
"api-service_internal_models_order.MetaResponse": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"has_next": {
"type": "boolean"
},
"has_prev": {
"type": "boolean"
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
},
"total_pages": {
"type": "integer"
}
}
},
"api-service_internal_models_order.NullableInt32": {
"type": "object",
"properties": {
"int32": {
"type": "integer"
},
"valid": {
"type": "boolean"
}
}
},
"api-service_internal_models_order.Order": {
"type": "object",
"properties": {
"date_created": {
"$ref": "#/definitions/sql.NullTime"
},
"date_updated": {
"$ref": "#/definitions/sql.NullTime"
},
"id": {
"type": "string"
},
"name": {
"$ref": "#/definitions/sql.NullString"
},
"sort": {
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
},
"status": {
"type": "string"
},
"user_created": {
"$ref": "#/definitions/sql.NullString"
},
"user_updated": {
"$ref": "#/definitions/sql.NullString"
}
}
},
"api-service_internal_models_order.OrderCreateRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"draft",
"active",
"inactive"
]
}
}
},
"api-service_internal_models_order.OrderCreateResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderDeleteResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderGetByIDResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderGetResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
}
},
"message": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
},
"summary": {
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
}
}
},
"api-service_internal_models_order.OrderUpdateRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"draft",
"active",
"inactive"
]
}
}
},
"api-service_internal_models_order.OrderUpdateResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_retribusi.AggregateData": { "api-service_internal_models_retribusi.AggregateData": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -22,56 +22,6 @@
"host": "localhost:8080", "host": "localhost:8080",
"basePath": "/api/v1", "basePath": "/api/v1",
"paths": { "paths": {
"/api/v1/Order/{id}": {
"get": {
"description": "Returns a single order by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Order"
],
"summary": "Get Order by ID",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Success response",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderGetByIDResponse"
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/auth/login": { "/api/v1/auth/login": {
"post": { "post": {
"description": "Authenticate user with username and password to receive JWT token", "description": "Authenticate user with username and password to receive JWT token",
@@ -262,116 +212,9 @@
} }
} }
}, },
"/api/v1/order/{id}": { "/api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}": {
"put": {
"description": "Updates an existing order record",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Update order",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
},
{
"description": "Order update request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateRequest"
}
}
],
"responses": {
"200": {
"description": "Order updated successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderUpdateResponse"
}
},
"400": {
"description": "Bad request or validation error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "Order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
},
"delete": {
"description": "Soft deletes a order by setting status to 'deleted'",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Delete order",
"parameters": [
{
"type": "string",
"description": "Order ID (UUID)",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Order deleted successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderDeleteResponse"
}
},
"400": {
"description": "Invalid ID format",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"404": {
"description": "Order not found",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/orders": {
"get": { "get": {
"description": "Returns a paginated list of orders with optional summary statistics", "description": "Search participant data based on Population NIK and service date",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@@ -379,142 +222,52 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"order" "bpjs"
], ],
"summary": "Get order with pagination and optional aggregation", "summary": "Get participant data by NIK",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "string",
"default": 10, "description": "NIK KTP",
"description": "Limit (max 100)", "name": "nik",
"name": "limit", "in": "path",
"in": "query" "required": true
},
{
"type": "integer",
"default": 0,
"description": "Offset",
"name": "offset",
"in": "query"
},
{
"type": "boolean",
"default": false,
"description": "Include aggregation summary",
"name": "include_summary",
"in": "query"
}, },
{ {
"type": "string", "type": "string",
"description": "Filter by status", "description": "Service date/SEP date (format: yyyy-MM-dd)",
"name": "status", "name": "tglSEP",
"in": "query" "in": "path",
}, "required": true
{
"type": "string",
"description": "Search in multiple fields",
"name": "search",
"in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "Success response", "description": "Participant data",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderGetResponse" "type": "object",
"additionalProperties": true
} }
}, },
"400": { "400": {
"description": "Bad request", "description": "Bad request",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" "type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Participant not found",
"schema": {
"type": "object",
"additionalProperties": true
} }
}, },
"500": { "500": {
"description": "Internal server error", "description": "Internal server error",
"schema": { "schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse" "type": "object",
} "additionalProperties": true
}
}
},
"post": {
"description": "Creates a new order record",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Create order",
"parameters": [
{
"description": "Order creation request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateRequest"
}
}
],
"responses": {
"201": {
"description": "Order created successfully",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.OrderCreateResponse"
}
},
"400": {
"description": "Bad request or validation error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
}
}
}
}
},
"/api/v1/orders/stats": {
"get": {
"description": "Returns comprehensive statistics about order data",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"order"
],
"summary": "Get order statistics",
"parameters": [
{
"type": "string",
"description": "Filter statistics by status",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "Statistics data",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/api-service_internal_models_order.ErrorResponse"
} }
} }
} }
@@ -984,224 +737,6 @@
} }
} }
}, },
"api-service_internal_models_order.AggregateData": {
"type": "object",
"properties": {
"by_status": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
},
"created_today": {
"type": "integer"
},
"last_updated": {
"type": "string"
},
"total_active": {
"type": "integer"
},
"total_draft": {
"type": "integer"
},
"total_inactive": {
"type": "integer"
},
"updated_today": {
"type": "integer"
}
}
},
"api-service_internal_models_order.ErrorResponse": {
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"timestamp": {
"type": "string"
}
}
},
"api-service_internal_models_order.MetaResponse": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"has_next": {
"type": "boolean"
},
"has_prev": {
"type": "boolean"
},
"limit": {
"type": "integer"
},
"offset": {
"type": "integer"
},
"total": {
"type": "integer"
},
"total_pages": {
"type": "integer"
}
}
},
"api-service_internal_models_order.NullableInt32": {
"type": "object",
"properties": {
"int32": {
"type": "integer"
},
"valid": {
"type": "boolean"
}
}
},
"api-service_internal_models_order.Order": {
"type": "object",
"properties": {
"date_created": {
"$ref": "#/definitions/sql.NullTime"
},
"date_updated": {
"$ref": "#/definitions/sql.NullTime"
},
"id": {
"type": "string"
},
"name": {
"$ref": "#/definitions/sql.NullString"
},
"sort": {
"$ref": "#/definitions/api-service_internal_models_order.NullableInt32"
},
"status": {
"type": "string"
},
"user_created": {
"$ref": "#/definitions/sql.NullString"
},
"user_updated": {
"$ref": "#/definitions/sql.NullString"
}
}
},
"api-service_internal_models_order.OrderCreateRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"draft",
"active",
"inactive"
]
}
}
},
"api-service_internal_models_order.OrderCreateResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderDeleteResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderGetByIDResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_order.OrderGetResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
}
},
"message": {
"type": "string"
},
"meta": {
"$ref": "#/definitions/api-service_internal_models_order.MetaResponse"
},
"summary": {
"$ref": "#/definitions/api-service_internal_models_order.AggregateData"
}
}
},
"api-service_internal_models_order.OrderUpdateRequest": {
"type": "object",
"required": [
"status"
],
"properties": {
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1
},
"status": {
"type": "string",
"enum": [
"draft",
"active",
"inactive"
]
}
}
},
"api-service_internal_models_order.OrderUpdateResponse": {
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/api-service_internal_models_order.Order"
},
"message": {
"type": "string"
}
}
},
"api-service_internal_models_retribusi.AggregateData": { "api-service_internal_models_retribusi.AggregateData": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -30,150 +30,6 @@ definitions:
username: username:
type: string type: string
type: object type: object
api-service_internal_models_order.AggregateData:
properties:
by_status:
additionalProperties:
type: integer
type: object
created_today:
type: integer
last_updated:
type: string
total_active:
type: integer
total_draft:
type: integer
total_inactive:
type: integer
updated_today:
type: integer
type: object
api-service_internal_models_order.ErrorResponse:
properties:
code:
type: integer
error:
type: string
message:
type: string
timestamp:
type: string
type: object
api-service_internal_models_order.MetaResponse:
properties:
current_page:
type: integer
has_next:
type: boolean
has_prev:
type: boolean
limit:
type: integer
offset:
type: integer
total:
type: integer
total_pages:
type: integer
type: object
api-service_internal_models_order.NullableInt32:
properties:
int32:
type: integer
valid:
type: boolean
type: object
api-service_internal_models_order.Order:
properties:
date_created:
$ref: '#/definitions/sql.NullTime'
date_updated:
$ref: '#/definitions/sql.NullTime'
id:
type: string
name:
$ref: '#/definitions/sql.NullString'
sort:
$ref: '#/definitions/api-service_internal_models_order.NullableInt32'
status:
type: string
user_created:
$ref: '#/definitions/sql.NullString'
user_updated:
$ref: '#/definitions/sql.NullString'
type: object
api-service_internal_models_order.OrderCreateRequest:
properties:
name:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
required:
- status
type: object
api-service_internal_models_order.OrderCreateResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_order.Order'
message:
type: string
type: object
api-service_internal_models_order.OrderDeleteResponse:
properties:
id:
type: string
message:
type: string
type: object
api-service_internal_models_order.OrderGetByIDResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_order.Order'
message:
type: string
type: object
api-service_internal_models_order.OrderGetResponse:
properties:
data:
items:
$ref: '#/definitions/api-service_internal_models_order.Order'
type: array
message:
type: string
meta:
$ref: '#/definitions/api-service_internal_models_order.MetaResponse'
summary:
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
type: object
api-service_internal_models_order.OrderUpdateRequest:
properties:
name:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
required:
- status
type: object
api-service_internal_models_order.OrderUpdateResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_order.Order'
message:
type: string
type: object
api-service_internal_models_retribusi.AggregateData: api-service_internal_models_retribusi.AggregateData:
properties: properties:
by_dinas: by_dinas:
@@ -466,39 +322,6 @@ info:
title: API Service title: API Service
version: 1.0.0 version: 1.0.0
paths: paths:
/api/v1/Order/{id}:
get:
consumes:
- application/json
description: Returns a single order by ID
parameters:
- description: Order ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderGetByIDResponse'
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"404":
description: order not found
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
summary: Get Order by ID
tags:
- Order
/api/v1/auth/login: /api/v1/auth/login:
post: post:
consumes: consumes:
@@ -622,177 +445,48 @@ paths:
summary: Register new user summary: Register new user
tags: tags:
- Authentication - Authentication
/api/v1/order/{id}: /api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP}:
delete:
consumes:
- application/json
description: Soft deletes a order by setting status to 'deleted'
parameters:
- description: Order ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Order deleted successfully
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderDeleteResponse'
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"404":
description: Order not found
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
summary: Delete order
tags:
- order
put:
consumes:
- application/json
description: Updates an existing order record
parameters:
- description: Order ID (UUID)
in: path
name: id
required: true
type: string
- description: Order update request
in: body
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateRequest'
produces:
- application/json
responses:
"200":
description: Order updated successfully
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderUpdateResponse'
"400":
description: Bad request or validation error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"404":
description: Order not found
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
summary: Update order
tags:
- order
/api/v1/orders:
get: get:
consumes: consumes:
- application/json - application/json
description: Returns a paginated list of orders with optional summary statistics description: Search participant data based on Population NIK and service date
parameters: parameters:
- default: 10 - description: NIK KTP
description: Limit (max 100) in: path
in: query name: nik
name: limit required: true
type: integer
- default: 0
description: Offset
in: query
name: offset
type: integer
- default: false
description: Include aggregation summary
in: query
name: include_summary
type: boolean
- description: Filter by status
in: query
name: status
type: string type: string
- description: Search in multiple fields - description: 'Service date/SEP date (format: yyyy-MM-dd)'
in: query in: path
name: search name: tglSEP
required: true
type: string type: string
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: Success response description: Participant data
schema: schema:
$ref: '#/definitions/api-service_internal_models_order.OrderGetResponse' additionalProperties: true
type: object
"400": "400":
description: Bad request description: Bad request
schema: schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse' additionalProperties: true
type: object
"404":
description: Participant not found
schema:
additionalProperties: true
type: object
"500": "500":
description: Internal server error description: Internal server error
schema: schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse' additionalProperties: true
summary: Get order with pagination and optional aggregation type: object
summary: Get participant data by NIK
tags: tags:
- order - bpjs
post:
consumes:
- application/json
description: Creates a new order record
parameters:
- description: Order creation request
in: body
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderCreateRequest'
produces:
- application/json
responses:
"201":
description: Order created successfully
schema:
$ref: '#/definitions/api-service_internal_models_order.OrderCreateResponse'
"400":
description: Bad request or validation error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
summary: Create order
tags:
- order
/api/v1/orders/stats:
get:
consumes:
- application/json
description: Returns comprehensive statistics about order data
parameters:
- description: Filter statistics by status
in: query
name: status
type: string
produces:
- application/json
responses:
"200":
description: Statistics data
schema:
$ref: '#/definitions/api-service_internal_models_order.AggregateData'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models_order.ErrorResponse'
summary: Get order statistics
tags:
- order
/api/v1/retribusi/{id}: /api/v1/retribusi/{id}:
delete: delete:
consumes: consumes:

View File

@@ -63,3 +63,9 @@ KEYCLOAK_ISSUER=https://auth.rssa.top/realms/sandbox
KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran KEYCLOAK_AUDIENCE=nuxtsim-pendaftaran
KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs KEYCLOAK_JWKS_URL=https://auth.rssa.top/realms/sandbox/protocol/openid-connect/certs
KEYCLOAK_ENABLED=false KEYCLOAK_ENABLED=false
# BPJS Configuration
BPJS_BASEURL=https://apijkn.bpjs-kesehatan.go.id/vclaim-rest
BPJS_CONSID=5257
BPJS_USERKEY=4cf1cbef8c008440bbe9ef9ba789e482
BPJS_SECRETKEY=1bV363512D

3
go.mod
View File

@@ -16,8 +16,10 @@ require (
) )
require ( require (
github.com/go-playground/validator/v10 v10.27.0
github.com/go-sql-driver/mysql v1.8.1 github.com/go-sql-driver/mysql v1.8.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mashingan/smapping v0.1.19
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.6 github.com/swaggo/swag v1.16.6
@@ -39,7 +41,6 @@ require (
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect

2
go.sum
View File

@@ -132,6 +132,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ=
github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I= github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=

View File

@@ -1,6 +1,11 @@
package config package config
import ( import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"log" "log"
"os" "os"
"strconv" "strconv"
@@ -9,10 +14,11 @@ import (
) )
type Config struct { type Config struct {
Server ServerConfig Server ServerConfig
Databases map[string]DatabaseConfig Databases map[string]DatabaseConfig
ReadReplicas map[string][]DatabaseConfig // For read replicas ReadReplicas map[string][]DatabaseConfig // For read replicas
Keycloak KeycloakConfig Keycloak KeycloakConfig
Bpjs BpjsConfig
} }
type ServerConfig struct { type ServerConfig struct {
@@ -21,20 +27,20 @@ type ServerConfig struct {
} }
type DatabaseConfig struct { type DatabaseConfig struct {
Name string Name string
Type string // postgres, mysql, sqlserver, sqlite, mongodb Type string // postgres, mysql, sqlserver, sqlite, mongodb
Host string Host string
Port int Port int
Username string Username string
Password string Password string
Database string Database string
Schema string Schema string
SSLMode string SSLMode string
Path string // For SQLite Path string // For SQLite
Options string // Additional connection options Options string // Additional connection options
MaxOpenConns int // Max open connections MaxOpenConns int // Max open connections
MaxIdleConns int // Max idle connections MaxIdleConns int // Max idle connections
ConnMaxLifetime time.Duration // Connection max lifetime ConnMaxLifetime time.Duration // Connection max lifetime
} }
type KeycloakConfig struct { type KeycloakConfig struct {
@@ -44,6 +50,52 @@ type KeycloakConfig struct {
Enabled bool Enabled bool
} }
type BpjsConfig struct {
BaseURL string `json:"base_url"`
ConsID string `json:"cons_id"`
UserKey string `json:"user_key"`
SecretKey string `json:"secret_key"`
Timeout time.Duration `json:"timeout"`
}
// SetHeader generates required headers for BPJS VClaim API
func (cfg BpjsConfig) SetHeader() (string, string, string, string, string) {
timenow := time.Now().UTC()
t, err := time.Parse(time.RFC3339, "1970-01-01T00:00:00Z")
if err != nil {
log.Fatal(err)
}
tstamp := timenow.Unix() - t.Unix()
secret := []byte(cfg.SecretKey)
message := []byte(cfg.ConsID + "&" + fmt.Sprint(tstamp))
hash := hmac.New(sha256.New, secret)
hash.Write(message)
// to lowercase hexits
hex.EncodeToString(hash.Sum(nil))
// to base64
xSignature := base64.StdEncoding.EncodeToString(hash.Sum(nil))
return cfg.ConsID, cfg.SecretKey, cfg.UserKey, fmt.Sprint(tstamp), xSignature
}
type ConfigBpjs struct {
Cons_id string
Secret_key string
User_key string
}
// SetHeader for backward compatibility
func (cfg ConfigBpjs) SetHeader() (string, string, string, string, string) {
bpjsConfig := BpjsConfig{
ConsID: cfg.Cons_id,
SecretKey: cfg.Secret_key,
UserKey: cfg.User_key,
}
return bpjsConfig.SetHeader()
}
func LoadConfig() *Config { func LoadConfig() *Config {
config := &Config{ config := &Config{
Server: ServerConfig{ Server: ServerConfig{
@@ -58,6 +110,13 @@ func LoadConfig() *Config {
JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"), JwksURL: getEnv("KEYCLOAK_JWKS_URL", "https://keycloak.example.com/auth/realms/yourrealm/protocol/openid-connect/certs"),
Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true), Enabled: getEnvAsBool("KEYCLOAK_ENABLED", true),
}, },
Bpjs: BpjsConfig{
BaseURL: getEnv("BPJS_BASEURL", "https://apijkn.bpjs-kesehatan.go.id"),
ConsID: getEnv("BPJS_CONSID", ""),
UserKey: getEnv("BPJS_USERKEY", ""),
SecretKey: getEnv("BPJS_SECRETKEY", ""),
Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")),
},
} }
// Load database configurations // Load database configurations
@@ -75,18 +134,18 @@ func (c *Config) loadDatabaseConfigs() {
// Primary database configuration // Primary database configuration
c.Databases["default"] = DatabaseConfig{ c.Databases["default"] = DatabaseConfig{
Name: "default", Name: "default",
Type: getEnv("DB_CONNECTION", "postgres"), Type: getEnv("DB_CONNECTION", "postgres"),
Host: getEnv("DB_HOST", "localhost"), Host: getEnv("DB_HOST", "localhost"),
Port: getEnvAsInt("DB_PORT", 5432), Port: getEnvAsInt("DB_PORT", 5432),
Username: getEnv("DB_USERNAME", ""), Username: getEnv("DB_USERNAME", ""),
Password: getEnv("DB_PASSWORD", ""), Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_DATABASE", "satu_db"), Database: getEnv("DB_DATABASE", "satu_db"),
Schema: getEnv("DB_SCHEMA", "public"), Schema: getEnv("DB_SCHEMA", "public"),
SSLMode: getEnv("DB_SSLMODE", "disable"), SSLMode: getEnv("DB_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25), MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25), MaxIdleConns: getEnvAsInt("DB_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv("DB_CONN_MAX_LIFETIME", "5m")),
} }
// SATUDATA database configuration // SATUDATA database configuration
@@ -111,9 +170,9 @@ func (c *Config) loadDatabaseConfigs() {
// Parse specific database configurations // Parse specific database configurations
if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") || if strings.HasSuffix(key, "_CONNECTION") || strings.HasSuffix(key, "_HOST") ||
strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") || strings.HasSuffix(key, "_DATABASE") || strings.HasSuffix(key, "_USERNAME") ||
strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") || strings.HasSuffix(key, "_PASSWORD") || strings.HasSuffix(key, "_PORT") ||
strings.HasSuffix(key, "_NAME") { strings.HasSuffix(key, "_NAME") {
segments := strings.Split(key, "_") segments := strings.Split(key, "_")
if len(segments) >= 2 { if len(segments) >= 2 {
@@ -131,25 +190,25 @@ func (c *Config) loadDatabaseConfigs() {
// Create DatabaseConfig from parsed configurations for additional databases // Create DatabaseConfig from parsed configurations for additional databases
for name, config := range dbConfigs { for name, config := range dbConfigs {
// Skip empty configurations or system configurations // Skip empty configurations or system configurations
if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" { if name == "" || strings.Contains(name, "chrome_crashpad_pipe") || name == "primary" {
continue continue
} }
dbConfig := DatabaseConfig{ dbConfig := DatabaseConfig{
Name: name, Name: name,
Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")), Type: getEnvFromMap(config, "connection", getEnvFromMap(config, "type", "postgres")),
Host: getEnvFromMap(config, "host", "localhost"), Host: getEnvFromMap(config, "host", "localhost"),
Port: getEnvAsIntFromMap(config, "port", 5432), Port: getEnvAsIntFromMap(config, "port", 5432),
Username: getEnvFromMap(config, "username", ""), Username: getEnvFromMap(config, "username", ""),
Password: getEnvFromMap(config, "password", ""), Password: getEnvFromMap(config, "password", ""),
Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)), Database: getEnvFromMap(config, "database", getEnvFromMap(config, "name", name)),
Schema: getEnvFromMap(config, "schema", "public"), Schema: getEnvFromMap(config, "schema", "public"),
SSLMode: getEnvFromMap(config, "sslmode", "disable"), SSLMode: getEnvFromMap(config, "sslmode", "disable"),
Path: getEnvFromMap(config, "path", ""), Path: getEnvFromMap(config, "path", ""),
Options: getEnvFromMap(config, "options", ""), Options: getEnvFromMap(config, "options", ""),
MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25), MaxOpenConns: getEnvAsIntFromMap(config, "max_open_conns", 25),
MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25), MaxIdleConns: getEnvAsIntFromMap(config, "max_idle_conns", 25),
ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")), ConnMaxLifetime: parseDuration(getEnvFromMap(config, "conn_max_lifetime", "5m")),
} }
// Skip if username is empty and it's not a system config // Skip if username is empty and it's not a system config
@@ -199,18 +258,18 @@ func (c *Config) loadReadReplicaConfigs() {
if replicaConfig == nil { if replicaConfig == nil {
// Create new replica config // Create new replica config
newConfig := DatabaseConfig{ newConfig := DatabaseConfig{
Name: replicaKey, Name: replicaKey,
Type: c.Databases[dbName].Type, Type: c.Databases[dbName].Type,
Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host), Host: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_HOST", c.Databases[dbName].Host),
Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port), Port: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PORT", c.Databases[dbName].Port),
Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username), Username: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_USERNAME", c.Databases[dbName].Username),
Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password), Password: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_PASSWORD", c.Databases[dbName].Password),
Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database), Database: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_DATABASE", c.Databases[dbName].Database),
Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema), Schema: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SCHEMA", c.Databases[dbName].Schema),
SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode), SSLMode: getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_SSLMODE", c.Databases[dbName].SSLMode),
MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns), MaxOpenConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_OPEN_CONNS", c.Databases[dbName].MaxOpenConns),
MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns), MaxIdleConns: getEnvAsInt("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_MAX_IDLE_CONNS", c.Databases[dbName].MaxIdleConns),
ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv("DB_"+strings.ToUpper(dbName)+"_REPLICA_"+replicaIndex+"_CONN_MAX_LIFETIME", "5m")),
} }
c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig) c.ReadReplicas[dbName] = append(c.ReadReplicas[dbName], newConfig)
replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1] replicaConfig = &c.ReadReplicas[dbName][len(c.ReadReplicas[dbName])-1]
@@ -249,167 +308,167 @@ func (c *Config) addSpecificDatabase(prefix, defaultType string) {
host := getEnv(strings.ToUpper(prefix)+"_HOST", "") host := getEnv(strings.ToUpper(prefix)+"_HOST", "")
if host != "" { if host != "" {
dbConfig := DatabaseConfig{ dbConfig := DatabaseConfig{
Name: prefix, Name: prefix,
Type: connection, Type: connection,
Host: host, Host: host,
Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432), Port: getEnvAsInt(strings.ToUpper(prefix)+"_PORT", 5432),
Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""), Username: getEnv(strings.ToUpper(prefix)+"_USERNAME", ""),
Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""), Password: getEnv(strings.ToUpper(prefix)+"_PASSWORD", ""),
Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)), Database: getEnv(strings.ToUpper(prefix)+"_DATABASE", getEnv(strings.ToUpper(prefix)+"_NAME", prefix)),
Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"), Schema: getEnv(strings.ToUpper(prefix)+"_SCHEMA", "public"),
SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"), SSLMode: getEnv(strings.ToUpper(prefix)+"_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25), MaxOpenConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25), MaxIdleConns: getEnvAsInt(strings.ToUpper(prefix)+"_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv(strings.ToUpper(prefix)+"_CONN_MAX_LIFETIME", "5m")),
} }
c.Databases[prefix] = dbConfig c.Databases[prefix] = dbConfig
} }
} }
// PostgreSQL database // PostgreSQL database
func (c *Config) addPostgreSQLConfigs() { func (c *Config) addPostgreSQLConfigs() {
// SATUDATA database configuration // SATUDATA database configuration
// defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost") // defaultPOSTGRESHost := getEnv("POSTGRES_HOST", "localhost")
// if defaultPOSTGRESHost != "" { // if defaultPOSTGRESHost != "" {
// c.Databases["postgres"] = DatabaseConfig{ // c.Databases["postgres"] = DatabaseConfig{
// Name: "postgres", // Name: "postgres",
// Type: getEnv("POSTGRES_CONNECTION", "postgres"), // Type: getEnv("POSTGRES_CONNECTION", "postgres"),
// Host: defaultPOSTGRESHost, // Host: defaultPOSTGRESHost,
// Port: getEnvAsInt("POSTGRES_PORT", 5432), // Port: getEnvAsInt("POSTGRES_PORT", 5432),
// Username: getEnv("POSTGRES_USERNAME", ""), // Username: getEnv("POSTGRES_USERNAME", ""),
// Password: getEnv("POSTGRES_PASSWORD", ""), // Password: getEnv("POSTGRES_PASSWORD", ""),
// Database: getEnv("POSTGRES_DATABASE", "postgres"), // Database: getEnv("POSTGRES_DATABASE", "postgres"),
// Schema: getEnv("POSTGRES_SCHEMA", "public"), // Schema: getEnv("POSTGRES_SCHEMA", "public"),
// SSLMode: getEnv("POSTGRES_SSLMODE", "disable"), // SSLMode: getEnv("POSTGRES_SSLMODE", "disable"),
// MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), // MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
// MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), // MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
// ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), // ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
// } // }
// } // }
// Support for custom PostgreSQL configurations with POSTGRES_ prefix // Support for custom PostgreSQL configurations with POSTGRES_ prefix
envVars := os.Environ() envVars := os.Environ()
for _, envVar := range envVars { for _, envVar := range envVars {
parts := strings.SplitN(envVar, "=", 2) parts := strings.SplitN(envVar, "=", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
key := parts[0] key := parts[0]
// Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY]) // Parse PostgreSQL configurations (format: POSTGRES_[NAME]_[PROPERTY])
if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") { if strings.HasPrefix(key, "POSTGRES_") && strings.Contains(key, "_") {
segments := strings.Split(key, "_") segments := strings.Split(key, "_")
if len(segments) >= 3 { if len(segments) >= 3 {
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
// Skip if it's a standard PostgreSQL configuration // Skip if it's a standard PostgreSQL configuration
if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" { if dbName == "connection" || dbName == "dev" || dbName == "default" || dbName == "satudata" {
continue continue
} }
// Create or update PostgreSQL configuration // Create or update PostgreSQL configuration
if _, exists := c.Databases[dbName]; !exists { if _, exists := c.Databases[dbName]; !exists {
c.Databases[dbName] = DatabaseConfig{ c.Databases[dbName] = DatabaseConfig{
Name: dbName, Name: dbName,
Type: "postgres", Type: "postgres",
Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"), Host: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432), Port: getEnvAsInt("POSTGRES_"+strings.ToUpper(dbName)+"_PORT", 5432),
Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""), Username: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_USERNAME", ""),
Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""), Password: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName), Database: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"), Schema: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SCHEMA", "public"),
SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), SSLMode: getEnv("POSTGRES_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25), MaxOpenConns: getEnvAsInt("POSTGRES_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25), MaxIdleConns: getEnvAsInt("POSTGRES_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv("POSTGRES_CONN_MAX_LIFETIME", "5m")),
} }
} }
} }
} }
} }
} }
// addMYSQLConfigs adds MYSQL database // addMYSQLConfigs adds MYSQL database
func (c *Config) addMySQLConfigs() { func (c *Config) addMySQLConfigs() {
// Primary MySQL configuration // Primary MySQL configuration
defaultMySQLHost := getEnv("MYSQL_HOST", "") defaultMySQLHost := getEnv("MYSQL_HOST", "")
if defaultMySQLHost != "" { if defaultMySQLHost != "" {
c.Databases["mysql"] = DatabaseConfig{ c.Databases["mysql"] = DatabaseConfig{
Name: "mysql", Name: "mysql",
Type: getEnv("MYSQL_CONNECTION", "mysql"), Type: getEnv("MYSQL_CONNECTION", "mysql"),
Host: defaultMySQLHost, Host: defaultMySQLHost,
Port: getEnvAsInt("MYSQL_PORT", 3306), Port: getEnvAsInt("MYSQL_PORT", 3306),
Username: getEnv("MYSQL_USERNAME", ""), Username: getEnv("MYSQL_USERNAME", ""),
Password: getEnv("MYSQL_PASSWORD", ""), Password: getEnv("MYSQL_PASSWORD", ""),
Database: getEnv("MYSQL_DATABASE", "mysql"), Database: getEnv("MYSQL_DATABASE", "mysql"),
SSLMode: getEnv("MYSQL_SSLMODE", "disable"), SSLMode: getEnv("MYSQL_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
} }
} }
// Support for custom MySQL configurations with MYSQL_ prefix // Support for custom MySQL configurations with MYSQL_ prefix
envVars := os.Environ() envVars := os.Environ()
for _, envVar := range envVars { for _, envVar := range envVars {
parts := strings.SplitN(envVar, "=", 2) parts := strings.SplitN(envVar, "=", 2)
if len(parts) != 2 { if len(parts) != 2 {
continue continue
} }
key := parts[0] key := parts[0]
// Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY]) // Parse MySQL configurations (format: MYSQL_[NAME]_[PROPERTY])
if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") { if strings.HasPrefix(key, "MYSQL_") && strings.Contains(key, "_") {
segments := strings.Split(key, "_") segments := strings.Split(key, "_")
if len(segments) >= 3 { if len(segments) >= 3 {
dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_")) dbName := strings.ToLower(strings.Join(segments[1:len(segments)-1], "_"))
// Skip if it's a standard MySQL configuration // Skip if it's a standard MySQL configuration
if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" { if dbName == "connection" || dbName == "dev" || dbName == "max" || dbName == "conn" {
continue continue
} }
// Create or update MySQL configuration // Create or update MySQL configuration
if _, exists := c.Databases[dbName]; !exists { if _, exists := c.Databases[dbName]; !exists {
mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "") mysqlHost := getEnv("MYSQL_"+strings.ToUpper(dbName)+"_HOST", "")
if mysqlHost != "" { if mysqlHost != "" {
c.Databases[dbName] = DatabaseConfig{ c.Databases[dbName] = DatabaseConfig{
Name: dbName, Name: dbName,
Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"), Type: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_CONNECTION", "mysql"),
Host: mysqlHost, Host: mysqlHost,
Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306), Port: getEnvAsInt("MYSQL_"+strings.ToUpper(dbName)+"_PORT", 3306),
Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""), Username: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_USERNAME", ""),
Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""), Password: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_PASSWORD", ""),
Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName), Database: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_DATABASE", dbName),
SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"), SSLMode: getEnv("MYSQL_"+strings.ToUpper(dbName)+"_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25), MaxOpenConns: getEnvAsInt("MYSQL_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25), MaxIdleConns: getEnvAsInt("MYSQL_MAX_IDLE_CONNS", 25),
ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")), ConnMaxLifetime: parseDuration(getEnv("MYSQL_CONN_MAX_LIFETIME", "5m")),
} }
} }
} }
} }
} }
} }
} }
// addMongoDBConfigs adds MongoDB database configurations from environment variables // addMongoDBConfigs adds MongoDB database configurations from environment variables
func (c *Config) addMongoDBConfigs() { func (c *Config) addMongoDBConfigs() {
// Primary MongoDB configuration // Primary MongoDB configuration
mongoHost := getEnv("MONGODB_HOST", "") mongoHost := getEnv("MONGODB_HOST", "")
if mongoHost != "" { if mongoHost != "" {
c.Databases["mongodb"] = DatabaseConfig{ c.Databases["mongodb"] = DatabaseConfig{
Name: "mongodb", Name: "mongodb",
Type: getEnv("MONGODB_CONNECTION", "mongodb"), Type: getEnv("MONGODB_CONNECTION", "mongodb"),
Host: mongoHost, Host: mongoHost,
Port: getEnvAsInt("MONGODB_PORT", 27017), Port: getEnvAsInt("MONGODB_PORT", 27017),
Username: getEnv("MONGODB_USER", ""), Username: getEnv("MONGODB_USER", ""),
Password: getEnv("MONGODB_PASS", ""), Password: getEnv("MONGODB_PASS", ""),
Database: getEnv("MONGODB_MASTER", "master"), Database: getEnv("MONGODB_MASTER", "master"),
SSLMode: getEnv("MONGODB_SSLMODE", "disable"), SSLMode: getEnv("MONGODB_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
} }
} }
@@ -417,17 +476,17 @@ func (c *Config) addMongoDBConfigs() {
mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "") mongoLocalHost := getEnv("MONGODB_LOCAL_HOST", "")
if mongoLocalHost != "" { if mongoLocalHost != "" {
c.Databases["mongodb_local"] = DatabaseConfig{ c.Databases["mongodb_local"] = DatabaseConfig{
Name: "mongodb_local", Name: "mongodb_local",
Type: getEnv("MONGODB_CONNECTION", "mongodb"), Type: getEnv("MONGODB_CONNECTION", "mongodb"),
Host: mongoLocalHost, Host: mongoLocalHost,
Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017), Port: getEnvAsInt("MONGODB_LOCAL_PORT", 27017),
Username: getEnv("MONGODB_LOCAL_USER", ""), Username: getEnv("MONGODB_LOCAL_USER", ""),
Password: getEnv("MONGODB_LOCAL_PASS", ""), Password: getEnv("MONGODB_LOCAL_PASS", ""),
Database: getEnv("MONGODB_LOCAL_DB", "local"), Database: getEnv("MONGODB_LOCAL_DB", "local"),
SSLMode: getEnv("MONGOD_SSLMODE", "disable"), SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
} }
} }
@@ -453,17 +512,17 @@ func (c *Config) addMongoDBConfigs() {
// Create or update MongoDB configuration // Create or update MongoDB configuration
if _, exists := c.Databases[dbName]; !exists { if _, exists := c.Databases[dbName]; !exists {
c.Databases[dbName] = DatabaseConfig{ c.Databases[dbName] = DatabaseConfig{
Name: dbName, Name: dbName,
Type: "mongodb", Type: "mongodb",
Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"), Host: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_HOST", "localhost"),
Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017), Port: getEnvAsInt("MONGODB_"+strings.ToUpper(dbName)+"_PORT", 27017),
Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""), Username: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_USER", ""),
Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""), Password: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_PASS", ""),
Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName), Database: getEnv("MONGODB_"+strings.ToUpper(dbName)+"_DB", dbName),
SSLMode: getEnv("MONGOD_SSLMODE", "disable"), SSLMode: getEnv("MONGOD_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100), MaxOpenConns: getEnvAsInt("MONGODB_MAX_OPEN_CONNS", 100),
MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10), MaxIdleConns: getEnvAsInt("MONGODB_MAX_IDLE_CONNS", 10),
ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")), ConnMaxLifetime: parseDuration(getEnv("MONGODB_CONN_MAX_LIFETIME", "30m")),
} }
} }
} }
@@ -537,6 +596,19 @@ func (c *Config) Validate() error {
} }
} }
if c.Bpjs.BaseURL == "" {
log.Fatal("BPJS Base URL is required")
}
if c.Bpjs.ConsID == "" {
log.Fatal("BPJS Consumer ID is required")
}
if c.Bpjs.UserKey == "" {
log.Fatal("BPJS User Key is required")
}
if c.Bpjs.SecretKey == "" {
log.Fatal("BPJS Secret Key is required")
}
// Validate Keycloak configuration if enabled // Validate Keycloak configuration if enabled
if c.Keycloak.Enabled { if c.Keycloak.Enabled {
if c.Keycloak.Issuer == "" { if c.Keycloak.Issuer == "" {

View 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,
})
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View 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.")
}

View 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
}

View 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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -1,9 +1,8 @@
package v1 package v1
import ( import (
orderHandlers "api-service/internal/handlers/order" bpjsPesertaHandlers "api-service/internal/handlers/bpjs"
retribusiHandlers "api-service/internal/handlers/retribusi" retribusiHandlers "api-service/internal/handlers/retribusi"
"net/http"
"api-service/internal/config" "api-service/internal/config"
"api-service/internal/middleware" "api-service/internal/middleware"
@@ -50,7 +49,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.POST("/token/generate", tokenHandler.GenerateToken) v1.POST("/token/generate", tokenHandler.GenerateToken)
v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect) v1.POST("/token/generate-direct", tokenHandler.GenerateTokenDirect)
// Retribusi endpoints // Retribusi endpoints
retribusiHandler := retribusiHandlers.NewRetribusiHandler() retribusiHandler := retribusiHandlers.NewRetribusiHandler()
v1.GET("/retribusis", retribusiHandler.GetRetribusi) v1.GET("/retribusis", retribusiHandler.GetRetribusi)
@@ -59,35 +57,16 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi) v1.PUT("/retribusi/:id", retribusiHandler.UpdateRetribusi)
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi) v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
// Protected routes (require authentication) // BPJS endpoints
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
// Order endpoints v1.GET("/bpjs/Peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
orderHandler := orderHandlers.NewOrderHandler()
v1.GET("/orders", orderHandler.GetOrder)
v1.GET("/order/:id", orderHandler.GetOrderByID)
v1.POST("/orders", orderHandler.CreateOrder)
v1.PUT("/order/:id", orderHandler.UpdateOrder)
v1.DELETE("/order/:id", orderHandler.DeleteOrder)
protected := v1.Group("/") protected := v1.Group("/")
protected.Use(middleware.JWTAuthMiddleware(authService)) protected.Use(middleware.JWTAuthMiddleware(authService))
{ {
// WebSocket endpoint // Protected routes (require authentication)
protected.GET("/websocket", WebSocketHandler)
protected.GET("/webservice", WebServiceHandler)
} }
} }
return router return router
} }
// WebSocketHandler handles WebSocket connections
func WebSocketHandler(c *gin.Context) {
// This will be implemented with proper WebSocket handling
c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"})
}
func WebServiceHandler(c *gin.Context) {
// This will be implemented with proper WebSocket handling
c.JSON(http.StatusOK, gin.H{"message": "WebSocket endpoint"})
}

View 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
}

View 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
}