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

View File

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

View File

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

View File

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

View File

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

3
go.mod
View File

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

2
go.sum
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.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mashingan/smapping v0.1.19 h1:SsEtuPn2UcM1croIupPtGLgWgpYRuS0rSQMvKD9g2BQ=
github.com/mashingan/smapping v0.1.19/go.mod h1:FjfiwFxGOuNxL/OT1WcrNAwTPx0YJeg5JiXwBB1nyig=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=

View File

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

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

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
}