perbaikan

This commit is contained in:
2025-08-24 16:18:15 +07:00
parent 9838c48eab
commit 7681c796e8
24 changed files with 2443 additions and 2057 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
basePath: /api/v1
definitions:
api-service_internal_models.AggregateData:
gin.H:
additionalProperties: {}
type: object
models.AggregateData:
properties:
by_dinas:
additionalProperties:
@@ -27,7 +30,7 @@ definitions:
updated_today:
type: integer
type: object
api-service_internal_models.ErrorResponse:
models.ErrorResponse:
properties:
code:
type: integer
@@ -38,277 +41,12 @@ definitions:
timestamp:
type: string
type: object
api-service_internal_models.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.NullableInt32:
properties:
int32:
type: integer
valid:
type: boolean
type: object
api-service_internal_models_auth.LoginRequest:
properties:
password:
type: string
username:
type: string
required:
- password
- username
type: object
api-service_internal_models_auth.TokenResponse:
properties:
access_token:
type: string
expires_in:
type: integer
token_type:
type: string
type: object
api-service_internal_models_auth.User:
properties:
email:
type: string
id:
type: string
role:
type: string
username:
type: string
type: object
api-service_internal_models_retribusi.Retribusi:
properties:
date_created:
$ref: '#/definitions/sql.NullTime'
date_updated:
$ref: '#/definitions/sql.NullTime'
dinas:
$ref: '#/definitions/sql.NullString'
id:
type: string
jenis:
$ref: '#/definitions/sql.NullString'
kelompok_obyek:
$ref: '#/definitions/sql.NullString'
kode_tarif:
$ref: '#/definitions/sql.NullString'
pelayanan:
$ref: '#/definitions/sql.NullString'
rekening_denda:
$ref: '#/definitions/sql.NullString'
rekening_pokok:
$ref: '#/definitions/sql.NullString'
satuan:
$ref: '#/definitions/sql.NullString'
satuan_overtime:
$ref: '#/definitions/sql.NullString'
sort:
$ref: '#/definitions/api-service_internal_models.NullableInt32'
status:
type: string
tarif:
$ref: '#/definitions/sql.NullString'
tarif_overtime:
$ref: '#/definitions/sql.NullString'
uraian_1:
$ref: '#/definitions/sql.NullString'
uraian_2:
$ref: '#/definitions/sql.NullString'
uraian_3:
$ref: '#/definitions/sql.NullString'
user_created:
$ref: '#/definitions/sql.NullString'
user_updated:
$ref: '#/definitions/sql.NullString'
type: object
api-service_internal_models_retribusi.RetribusiCreateRequest:
properties:
dinas:
maxLength: 255
minLength: 1
type: string
jenis:
maxLength: 255
minLength: 1
type: string
kelompok_obyek:
maxLength: 255
minLength: 1
type: string
kode_tarif:
maxLength: 255
minLength: 1
type: string
pelayanan:
maxLength: 255
minLength: 1
type: string
rekening_denda:
maxLength: 255
minLength: 1
type: string
rekening_pokok:
maxLength: 255
minLength: 1
type: string
satuan:
maxLength: 255
minLength: 1
type: string
satuan_overtime:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
tarif:
type: string
tarif_overtime:
type: string
uraian_1:
type: string
uraian_2:
type: string
uraian_3:
type: string
required:
- status
type: object
api-service_internal_models_retribusi.RetribusiCreateResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_retribusi.Retribusi'
message:
type: string
type: object
api-service_internal_models_retribusi.RetribusiDeleteResponse:
properties:
id:
type: string
message:
type: string
type: object
api-service_internal_models_retribusi.RetribusiGetByIDResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_retribusi.Retribusi'
message:
type: string
type: object
api-service_internal_models_retribusi.RetribusiGetResponse:
properties:
data:
items:
$ref: '#/definitions/api-service_internal_models_retribusi.Retribusi'
type: array
message:
type: string
meta:
$ref: '#/definitions/api-service_internal_models.MetaResponse'
summary:
$ref: '#/definitions/api-service_internal_models.AggregateData'
type: object
api-service_internal_models_retribusi.RetribusiUpdateRequest:
properties:
dinas:
maxLength: 255
minLength: 1
type: string
jenis:
maxLength: 255
minLength: 1
type: string
kelompok_obyek:
maxLength: 255
minLength: 1
type: string
kode_tarif:
maxLength: 255
minLength: 1
type: string
pelayanan:
maxLength: 255
minLength: 1
type: string
rekening_denda:
maxLength: 255
minLength: 1
type: string
rekening_pokok:
maxLength: 255
minLength: 1
type: string
satuan:
maxLength: 255
minLength: 1
type: string
satuan_overtime:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
tarif:
type: string
tarif_overtime:
type: string
uraian_1:
type: string
uraian_2:
type: string
uraian_3:
type: string
required:
- status
type: object
api-service_internal_models_retribusi.RetribusiUpdateResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_retribusi.Retribusi'
message:
type: string
type: object
gin.H:
additionalProperties: {}
type: object
models.DiagnosaResponse:
properties:
data:
additionalProperties: true
type: object
message:
type: string
type: object
models.Flag:
properties:
cob:
flag:
type: string
required:
- cob
- flag
type: object
models.Jaminan:
properties:
@@ -345,6 +83,16 @@ definitions:
penanggungJawab:
type: string
type: object
models.LoginRequest:
properties:
password:
type: string
username:
type: string
required:
- password
- username
type: object
models.LokasiLaka:
properties:
kdKabupaten:
@@ -354,6 +102,30 @@ definitions:
kdPropinsi:
type: string
type: object
models.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
models.NullableInt32:
properties:
int32:
type: integer
valid:
type: boolean
type: object
models.Penjamin:
properties:
keterangan:
@@ -371,6 +143,207 @@ definitions:
type: string
required:
- eksekutif
- tujuan
type: object
models.Retribusi:
properties:
date_created:
$ref: '#/definitions/sql.NullTime'
date_updated:
$ref: '#/definitions/sql.NullTime'
dinas:
$ref: '#/definitions/sql.NullString'
id:
type: string
jenis:
$ref: '#/definitions/sql.NullString'
kelompok_obyek:
$ref: '#/definitions/sql.NullString'
kode_tarif:
$ref: '#/definitions/sql.NullString'
pelayanan:
$ref: '#/definitions/sql.NullString'
rekening_denda:
$ref: '#/definitions/sql.NullString'
rekening_pokok:
$ref: '#/definitions/sql.NullString'
satuan:
$ref: '#/definitions/sql.NullString'
satuan_overtime:
$ref: '#/definitions/sql.NullString'
sort:
$ref: '#/definitions/models.NullableInt32'
status:
type: string
tarif:
$ref: '#/definitions/sql.NullString'
tarif_overtime:
$ref: '#/definitions/sql.NullString'
uraian_1:
$ref: '#/definitions/sql.NullString'
uraian_2:
$ref: '#/definitions/sql.NullString'
uraian_3:
$ref: '#/definitions/sql.NullString'
user_created:
$ref: '#/definitions/sql.NullString'
user_updated:
$ref: '#/definitions/sql.NullString'
type: object
models.RetribusiCreateRequest:
properties:
dinas:
maxLength: 255
minLength: 1
type: string
jenis:
maxLength: 255
minLength: 1
type: string
kelompok_obyek:
maxLength: 255
minLength: 1
type: string
kode_tarif:
maxLength: 255
minLength: 1
type: string
pelayanan:
maxLength: 255
minLength: 1
type: string
rekening_denda:
maxLength: 255
minLength: 1
type: string
rekening_pokok:
maxLength: 255
minLength: 1
type: string
satuan:
maxLength: 255
minLength: 1
type: string
satuan_overtime:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
tarif:
type: string
tarif_overtime:
type: string
uraian_1:
type: string
uraian_2:
type: string
uraian_3:
type: string
required:
- status
type: object
models.RetribusiCreateResponse:
properties:
data:
$ref: '#/definitions/models.Retribusi'
message:
type: string
type: object
models.RetribusiDeleteResponse:
properties:
id:
type: string
message:
type: string
type: object
models.RetribusiGetByIDResponse:
properties:
data:
$ref: '#/definitions/models.Retribusi'
message:
type: string
type: object
models.RetribusiGetResponse:
properties:
data:
items:
$ref: '#/definitions/models.Retribusi'
type: array
message:
type: string
meta:
$ref: '#/definitions/models.MetaResponse'
summary:
$ref: '#/definitions/models.AggregateData'
type: object
models.RetribusiUpdateRequest:
properties:
dinas:
maxLength: 255
minLength: 1
type: string
jenis:
maxLength: 255
minLength: 1
type: string
kelompok_obyek:
maxLength: 255
minLength: 1
type: string
kode_tarif:
maxLength: 255
minLength: 1
type: string
pelayanan:
maxLength: 255
minLength: 1
type: string
rekening_denda:
maxLength: 255
minLength: 1
type: string
rekening_pokok:
maxLength: 255
minLength: 1
type: string
satuan:
maxLength: 255
minLength: 1
type: string
satuan_overtime:
maxLength: 255
minLength: 1
type: string
status:
enum:
- draft
- active
- inactive
type: string
tarif:
type: string
tarif_overtime:
type: string
uraian_1:
type: string
uraian_2:
type: string
uraian_3:
type: string
required:
- status
type: object
models.RetribusiUpdateResponse:
properties:
data:
$ref: '#/definitions/models.Retribusi'
message:
type: string
type: object
models.Rujukan:
properties:
@@ -390,17 +363,17 @@ definitions:
type: object
models.SepPostRequest:
properties:
t_sep:
tsep:
$ref: '#/definitions/models.TSepPost'
required:
- t_sep
- tsep
type: object
models.SepPutRequest:
properties:
t_sep:
tsep:
$ref: '#/definitions/models.TSepPut'
required:
- t_sep
- tsep
type: object
models.SepResponse:
properties:
@@ -520,6 +493,26 @@ definitions:
- noSep
- user
type: object
models.TokenResponse:
properties:
access_token:
type: string
expires_in:
type: integer
token_type:
type: string
type: object
models.User:
properties:
email:
type: string
id:
type: string
role:
type: string
username:
type: string
type: object
sql.NullString:
properties:
string:
@@ -561,14 +554,14 @@ paths:
name: login
required: true
schema:
$ref: '#/definitions/api-service_internal_models_auth.LoginRequest'
$ref: '#/definitions/models.LoginRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api-service_internal_models_auth.TokenResponse'
$ref: '#/definitions/models.TokenResponse'
"400":
description: Bad request
schema:
@@ -593,7 +586,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/api-service_internal_models_auth.User'
$ref: '#/definitions/models.User'
"401":
description: Unauthorized
schema:
@@ -625,7 +618,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/api-service_internal_models_auth.TokenResponse'
$ref: '#/definitions/models.TokenResponse'
"400":
description: Bad request
schema:
@@ -715,36 +708,6 @@ paths:
summary: Get participant data by NIK
tags:
- bpjs
/api/v1/bpjs/reference/referensi/diagnosa:
get:
consumes:
- application/json
description: Get all diagnosa reference data
produces:
- application/json
responses:
"200":
description: Success response
schema:
$ref: '#/definitions/models.DiagnosaResponse'
"400":
description: Bad request
schema:
additionalProperties: true
type: object
"404":
description: Data not found
schema:
additionalProperties: true
type: object
"500":
description: Internal server error
schema:
additionalProperties: true
type: object
summary: Get all diagnosa reference data
tags:
- bpjs/reference
/api/v1/retribusi/{id}:
delete:
consumes:
@@ -762,19 +725,19 @@ paths:
"200":
description: Retribusi deleted successfully
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiDeleteResponse'
$ref: '#/definitions/models.RetribusiDeleteResponse'
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"404":
description: Retribusi not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Delete retribusi
tags:
- retribusi
@@ -794,19 +757,19 @@ paths:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiGetByIDResponse'
$ref: '#/definitions/models.RetribusiGetByIDResponse'
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"404":
description: Retribusi not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Get Retribusi by ID
tags:
- retribusi
@@ -825,26 +788,26 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiUpdateRequest'
$ref: '#/definitions/models.RetribusiUpdateRequest'
produces:
- application/json
responses:
"200":
description: Retribusi updated successfully
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiUpdateResponse'
$ref: '#/definitions/models.RetribusiUpdateResponse'
"400":
description: Bad request or validation error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"404":
description: Retribusi not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Update retribusi
tags:
- retribusi
@@ -891,15 +854,15 @@ paths:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiGetResponse'
$ref: '#/definitions/models.RetribusiGetResponse'
"400":
description: Bad request
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Get retribusi with pagination and optional aggregation
tags:
- retribusi
@@ -913,22 +876,22 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiCreateRequest'
$ref: '#/definitions/models.RetribusiCreateRequest'
produces:
- application/json
responses:
"201":
description: Retribusi created successfully
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiCreateResponse'
$ref: '#/definitions/models.RetribusiCreateResponse'
"400":
description: Bad request or validation error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Create retribusi
tags:
- retribusi
@@ -966,15 +929,15 @@ paths:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_retribusi.RetribusiGetResponse'
$ref: '#/definitions/models.RetribusiGetResponse'
"400":
description: Bad request
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Get retribusi with dynamic filtering
tags:
- retribusi
@@ -994,11 +957,11 @@ paths:
"200":
description: Statistics data
schema:
$ref: '#/definitions/api-service_internal_models.AggregateData'
$ref: '#/definitions/models.AggregateData'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
$ref: '#/definitions/models.ErrorResponse'
summary: Get retribusi statistics
tags:
- retribusi
@@ -1013,14 +976,14 @@ paths:
name: token
required: true
schema:
$ref: '#/definitions/api-service_internal_models_auth.LoginRequest'
$ref: '#/definitions/models.LoginRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api-service_internal_models_auth.TokenResponse'
$ref: '#/definitions/models.TokenResponse'
"400":
description: Bad request
schema:
@@ -1057,7 +1020,7 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/api-service_internal_models_auth.TokenResponse'
$ref: '#/definitions/models.TokenResponse'
"400":
description: Bad request
schema:
@@ -1096,11 +1059,11 @@ paths:
$ref: '#/definitions/gin.H'
summary: Create a new SEP
tags:
- bpjs
- SEP
put:
consumes:
- application/json
description: Update an existing Surat Eligibilitas Peserta
description: Update Surat Eligibilitas Peserta
parameters:
- description: SEP update request
in: body
@@ -1123,9 +1086,9 @@ paths:
description: Internal server error
schema:
$ref: '#/definitions/gin.H'
summary: Update an existing SEP
summary: Update SEP
tags:
- bpjs
- SEP
/sep/{noSep}:
delete:
consumes:
@@ -1157,9 +1120,9 @@ paths:
description: Internal server error
schema:
$ref: '#/definitions/gin.H'
summary: Delete an existing SEP
summary: Delete SEP
tags:
- bpjs
- SEP
get:
consumes:
- application/json
@@ -1185,9 +1148,9 @@ paths:
description: Internal server error
schema:
$ref: '#/definitions/gin.H'
summary: Get an existing SEP
summary: Get SEP
tags:
- bpjs
- SEP
schemes:
- http
- https

View File

@@ -77,4 +77,16 @@ BRIDGING_SATUSEHAT_CLIENT_SECRET=Al3PTYAW6axPiAFwaFlpn8qShLFW5YGMgG8w1qhexgCc7lG
BRIDGING_SATUSEHAT_AUTH_URL=https://api-satusehat.kemkes.go.id/oauth2/v1
BRIDGING_SATUSEHAT_BASE_URL=https://api-satusehat.kemkes.go.id/fhir-r4/v1
BRIDGING_SATUSEHAT_CONSENT_URL=https://api-satusehat.dto.kemkes.go.id/consent/v1
BRIDGING_SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2
BRIDGING_SATUSEHAT_KFA_URL=https://api-satusehat.kemkes.go.id/kfa-v2
SWAGGER_TITLE=My Custom API Service
SWAGGER_DESCRIPTION=This is a custom API service for managing various resources
SWAGGER_VERSION=2.0.0
SWAGGER_CONTACT_NAME=STIM IT Support
SWAGGER_HOST=api.mycompany.com:8080
SWAGGER_BASE_PATH=/api/v2
SWAGGER_SCHEMES=https
API_TITLE=API Service UJICOBA
API_DESCRIPTION=Dokumentation SWAGGER
API_VERSION=3.0.0

View File

@@ -0,0 +1,175 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"api-service/internal/config"
services "api-service/internal/services/satusehat"
"github.com/joho/godotenv"
)
func main() {
fmt.Println("SatuSehat Specific Requests - Organization & Patient by NIK")
fmt.Println("==========================================================")
// Load environment variables from .env file
err := godotenv.Load("../.env")
if err != nil {
log.Printf("Warning: Could not load .env file: %v", err)
}
// Set debug logging
os.Setenv("LOG_LEVEL", "debug")
// Load configuration from environment variables
cfg := config.LoadConfig()
// Create SatuSehat service
service := services.NewSatuSehatServiceFromConfig(cfg)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Test 1: Get Organization by ID (using OrgID from config)
fmt.Println("\n1. Get Organization by ID:")
orgID := cfg.SatuSehat.OrgID
fmt.Printf(" Using OrgID: %s\n", orgID)
orgEndpoint := fmt.Sprintf("/Organization/%s", orgID)
orgResult, err := service.GetRawResponse(ctx, orgEndpoint)
if err != nil {
log.Printf("Error getting organization: %v", err)
} else {
fmt.Printf(" Status Code: %d\n", orgResult.StatusCode)
fmt.Printf(" Success: %v\n", orgResult.Success)
fmt.Printf(" Message: %s\n", orgResult.Message)
if orgResult.Data != nil {
fmt.Printf(" Data Type: %T\n", orgResult.Data)
// Convert data to JSON for better readability
if jsonData, err := json.MarshalIndent(orgResult.Data, " ", " "); err == nil {
fmt.Printf(" Organization Data: \n%s\n", string(jsonData))
} else {
fmt.Printf(" Data (raw): %+v\n", orgResult.Data)
}
}
if orgResult.Error != nil {
fmt.Printf(" Error Code: %s\n", orgResult.Error.Code)
fmt.Printf(" Error Details: %s\n", orgResult.Error.Details)
}
}
// Test 2: Get Patient by specific NIK
fmt.Println("\n2. Get Patient by Specific NIK:")
specificNIK := "3512162601960002"
fmt.Printf(" Using NIK: %s\n", specificNIK)
patientResult, err := service.GetPatientByNIK(ctx, specificNIK)
if err != nil {
log.Printf("Error getting patient: %v", err)
} else {
fmt.Printf(" Status Code: %d\n", patientResult.StatusCode)
fmt.Printf(" Success: %v\n", patientResult.Success)
fmt.Printf(" Message: %s\n", patientResult.Message)
if patientResult.Data != nil {
fmt.Printf(" Data Type: %T\n", patientResult.Data)
// Convert data to JSON for better readability
if jsonData, err := json.MarshalIndent(patientResult.Data, " ", " "); err == nil {
fmt.Printf(" Patient Data: \n%s\n", string(jsonData))
// Extract and display specific patient information
displayPatientDetails(patientResult.Data)
} else {
fmt.Printf(" Data (raw): %+v\n", patientResult.Data)
}
}
if patientResult.Error != nil {
fmt.Printf(" Error Code: %s\n", patientResult.Error.Code)
fmt.Printf(" Error Details: %s\n", patientResult.Error.Details)
}
}
// Test 3: Health check to verify token status
fmt.Println("\n3. Service Health Check:")
isValid := service.IsTokenValid()
fmt.Printf(" Token Valid: %v\n", isValid)
if !isValid {
fmt.Println(" Refreshing token...")
err = service.RefreshToken(ctx)
if err != nil {
log.Printf("Error refreshing token: %v", err)
} else {
fmt.Println(" Token refreshed successfully")
fmt.Printf(" Token Valid After Refresh: %v\n", service.IsTokenValid())
}
}
fmt.Println("\nSpecific requests test completed!")
}
// displayPatientDetails extracts and displays specific patient information from FHIR response
func displayPatientDetails(data interface{}) {
fmt.Println("\n --- Patient Details ---")
// Convert to map for easier access
if dataMap, ok := data.(map[string]interface{}); ok {
// Check if it's a Bundle
if resourceType, exists := dataMap["resourceType"]; exists && resourceType == "Bundle" {
if entries, exists := dataMap["entry"]; exists {
if entryList, ok := entries.([]interface{}); ok && len(entryList) > 0 {
if firstEntry, ok := entryList[0].(map[string]interface{}); ok {
if resource, exists := firstEntry["resource"]; exists {
if patient, ok := resource.(map[string]interface{}); ok {
// Display basic patient info
fmt.Printf(" Patient ID: %v\n", patient["id"])
// Display name
if names, exists := patient["name"].([]interface{}); exists && len(names) > 0 {
if name, ok := names[0].(map[string]interface{}); ok {
fmt.Printf(" Name: %v\n", name["text"])
}
}
// Display identifiers
if identifiers, exists := patient["identifier"].([]interface{}); exists {
fmt.Println(" Identifiers:")
for _, ident := range identifiers {
if identifier, ok := ident.(map[string]interface{}); ok {
system := identifier["system"]
value := identifier["value"]
fmt.Printf(" - %s: %v\n", system, value)
}
}
}
// Display status
fmt.Printf(" Active: %v\n", patient["active"])
// Display meta information
if meta, exists := patient["meta"].(map[string]interface{}); exists {
fmt.Printf(" Last Updated: %v\n", meta["lastUpdated"])
if profiles, exists := meta["profile"].([]interface{}); exists {
fmt.Printf(" FHIR Profile: %v\n", profiles[0])
}
}
}
}
}
}
}
}
}
fmt.Println(" -----------------------")
}

7
go.mod
View File

@@ -21,9 +21,10 @@ require (
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/rs/zerolog v1.34.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.6
github.com/tidwall/gjson v1.18.0
)
require (
@@ -57,6 +58,7 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.8.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -64,6 +66,9 @@ require (
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect

17
go.sum
View File

@@ -30,6 +30,7 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/daku10/go-lz-string v0.0.6 h1:aO8FFp4QPuNp7+WNyh1DyNjGF3UbZu95tUv9xOZNsYQ=
github.com/daku10/go-lz-string v0.0.6/go.mod h1:Vk++rSG3db8HXJaHEAbxiy/ukjTmPBw/iI+SrVZDzfs=
@@ -68,6 +69,7 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
@@ -136,6 +138,10 @@ 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-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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=
@@ -156,12 +162,16 @@ github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzL
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -183,6 +193,12 @@ github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+z
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
@@ -258,6 +274,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@@ -20,6 +20,22 @@ type Config struct {
Keycloak KeycloakConfig
Bpjs BpjsConfig
SatuSehat SatuSehatConfig
Swagger SwaggerConfig
}
type SwaggerConfig struct {
Title string
Description string
Version string
TermsOfService string
ContactName string
ContactURL string
ContactEmail string
LicenseName string
LicenseURL string
Host string
BasePath string
Schemes []string
}
type ServerConfig struct {
@@ -141,6 +157,20 @@ func LoadConfig() *Config {
KFAURL: getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2"),
Timeout: parseDuration(getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s")),
},
Swagger: SwaggerConfig{
Title: getEnv("SWAGGER_TITLE", "SERVICE API"),
Description: getEnv("SWAGGER_DESCRIPTION", "CUSTUM SERVICE API"),
Version: getEnv("SWAGGER_VERSION", "1.0.0"),
TermsOfService: getEnv("SWAGGER_TERMS_OF_SERVICE", "http://swagger.io/terms/"),
ContactName: getEnv("SWAGGER_CONTACT_NAME", "API Support"),
ContactURL: getEnv("SWAGGER_CONTACT_URL", "http://rssa.example.com/support"),
ContactEmail: getEnv("SWAGGER_CONTACT_EMAIL", "support@swagger.io"),
LicenseName: getEnv("SWAGGER_LICENSE_NAME", "Apache 2.0"),
LicenseURL: getEnv("SWAGGER_LICENSE_URL", "http://www.apache.org/licenses/LICENSE-2.0.html"),
Host: getEnv("SWAGGER_HOST", "localhost:8080"),
BasePath: getEnv("SWAGGER_BASE_PATH", "/api/v1"),
Schemes: parseSchemes(getEnv("SWAGGER_SCHEMES", "http,https")),
},
}
// Load database configurations
@@ -600,6 +630,19 @@ func getEnvAsBool(key string, defaultValue bool) bool {
return defaultValue
}
// parseSchemes parses comma-separated schemes string into a slice
func parseSchemes(schemesStr string) []string {
if schemesStr == "" {
return []string{"http"}
}
schemes := strings.Split(schemesStr, ",")
for i, scheme := range schemes {
schemes[i] = strings.TrimSpace(scheme)
}
return schemes
}
func (c *Config) Validate() error {
if len(c.Databases) == 0 {
log.Fatal("At least one database configuration is required")

View File

@@ -1,85 +0,0 @@
package handlers
import (
"context"
"fmt"
"net/http"
"time"
"api-service/internal/config"
services "api-service/internal/services/bpjs"
"github.com/gin-gonic/gin"
)
// DiagnosaHandler handles BPJS diagnosa operations
type DiagnosaHandler struct {
bpjsService services.VClaimService
}
// NewDiagnosaHandler creates a new DiagnosaHandler instance
func NewDiagnosaHandler(cfg config.BpjsConfig) *DiagnosaHandler {
return &DiagnosaHandler{
bpjsService: services.NewService(cfg),
}
}
// GetAll godoc
// @Summary Get all diagnosa reference data
// @Description Get all diagnosa reference data
// @Tags bpjs/reference
// @Accept json
// @Produce json
// @Success 200 {object} models.DiagnosaResponse "Success response"
// @Failure 400 {object} map[string]interface{} "Bad request"
// @Failure 404 {object} map[string]interface{} "Data not found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/v1/bpjs/reference/referensi/diagnosa [get]
func (h *DiagnosaHandler) GetAll(c *gin.Context) {
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Build endpoint URL
endpoint := "/referensi/diagnosa"
// 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 diagnosa data",
"message": err.Error(),
})
return
}
// Return successful response
c.JSON(http.StatusOK, gin.H{
"message": "Data diagnosa berhasil diambil",
"data": result,
})
}
// Helper methods for error handling and response formatting
// handleBPJSError handles BPJS service errors and returns appropriate HTTP responses
func (h *DiagnosaHandler) handleBPJSError(c *gin.Context, err error, operation string) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Failed to %s", operation),
"message": err.Error(),
})
}
// validateDateFormat validates if the date string is in yyyy-MM-dd format
func (h *DiagnosaHandler) validateDateFormat(dateStr string) error {
_, err := time.Parse("2006-01-02", dateStr)
return err
}
// buildSuccessResponse builds a standardized success response
func (h *DiagnosaHandler) buildSuccessResponse(message string, data interface{}) gin.H {
return gin.H{
"message": message,
"data": data,
}
}

View File

@@ -0,0 +1,24 @@
package healthcheck
import (
"api-service/internal/database"
"net/http"
"github.com/gin-gonic/gin"
)
// HealthCheckHandler handles health check requests
type HealthCheckHandler struct {
dbService database.Service
}
// NewHealthCheckHandler creates a new HealthCheckHandler
func NewHealthCheckHandler(dbService database.Service) *HealthCheckHandler {
return &HealthCheckHandler{dbService: dbService}
}
// CheckHealth checks the health of the application
func (h *HealthCheckHandler) CheckHealth(c *gin.Context) {
healthStatus := h.dbService.Health() // Call the health check function from the database service
c.JSON(http.StatusOK, healthStatus)
}

View File

@@ -1,192 +0,0 @@
package satusehat
import (
"net/http"
"api-service/internal/services/satusehat"
"github.com/gin-gonic/gin"
)
type PatientHandler struct {
service *satusehat.SatuSehatService
}
func NewPatientHandler(service *satusehat.SatuSehatService) *PatientHandler {
return &PatientHandler{
service: service,
}
}
// SearchPatientByNIK godoc
// @Summary Search patient by NIK
// @Description Search patient data from SatuSehat by National Identity Number (NIK)
// @Tags SatuSehat
// @Accept json
// @Produce json
// @Param nik query string true "National Identity Number (NIK)"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /satusehat/patient/search/nik [get]
func (h *PatientHandler) SearchPatientByNIK(c *gin.Context) {
nik := c.Query("nik")
if nik == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "NIK parameter is required",
})
return
}
patientResp, err := h.service.SearchPatientByNIK(c.Request.Context(), nik)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": err.Error(),
})
return
}
patientInfo, err := satusehat.ExtractPatientInfo(patientResp)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Not Found",
"message": "Patient not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": patientInfo,
})
}
// SearchPatientByName godoc
// @Summary Search patient by name
// @Description Search patient data from SatuSehat by name
// @Tags SatuSehat
// @Accept json
// @Produce json
// @Param name query string true "Patient name"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /satusehat/patient/search/name [get]
func (h *PatientHandler) SearchPatientByName(c *gin.Context) {
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Name parameter is required",
})
return
}
patientResp, err := h.service.SearchPatientByName(c.Request.Context(), name)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": err.Error(),
})
return
}
if patientResp == nil || len(patientResp.Entry) == 0 {
c.JSON(http.StatusNotFound, gin.H{
"error": "Not Found",
"message": "Patient not found",
})
return
}
// Return all found patients
var patients []map[string]interface{}
for _, entry := range patientResp.Entry {
patientInfo := map[string]interface{}{
"id": entry.Resource.ID,
"name": satusehat.ExtractPatientName(entry.Resource.Name),
"nik": satusehat.ExtractNIK(entry.Resource.Identifier),
"gender": entry.Resource.Gender,
"birthDate": entry.Resource.BirthDate,
"address": satusehat.ExtractAddress(entry.Resource.Address),
"phone": satusehat.ExtractPhone(entry.Resource.Telecom),
"lastUpdated": entry.Resource.Meta.LastUpdated,
}
patients = append(patients, patientInfo)
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": patients,
"total": len(patients),
})
}
// CreatePatient godoc
// @Summary Create new patient
// @Description Create new patient data in SatuSehat
// @Tags SatuSehat
// @Accept json
// @Produce json
// @Param patient body map[string]interface{} true "Patient data"
// @Success 201 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /satusehat/patient [post]
func (h *PatientHandler) CreatePatient(c *gin.Context) {
var patientData map[string]interface{}
if err := c.ShouldBindJSON(&patientData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Bad Request",
"message": "Invalid JSON format",
})
return
}
response, err := h.service.CreatePatient(c.Request.Context(), patientData)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"success": true,
"data": response,
})
}
// GetAccessToken godoc
// @Summary Get access token
// @Description Get SatuSehat access token
// @Tags SatuSehat
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /satusehat/token [get]
func (h *PatientHandler) GetAccessToken(c *gin.Context) {
token, err := h.service.GetAccessToken(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal Server Error",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": map[string]interface{}{
"access_token": token.AccessToken,
"token_type": token.TokenType,
"expires_in": token.ExpiresIn,
"scope": token.Scope,
"issued_at": token.IssuedAt,
},
})
}

View File

@@ -0,0 +1,100 @@
package swagger
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"api-service/internal/config"
)
// Handler handles Swagger documentation
type Handler struct {
config *config.Config
}
// NewHandler creates a new Swagger handler
func NewHandler(cfg *config.Config) *Handler {
return &Handler{
config: cfg,
}
}
// RegisterRoutes registers Swagger routes
func (h *Handler) RegisterRoutes(router *gin.Engine) {
// Serve Swagger UI
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Serve OpenAPI spec
router.GET("/openapi.json", h.serveOpenAPISpec)
router.GET("/openapi.yaml", h.serveOpenAPISpecYAML)
}
// serveOpenAPISpec serves the OpenAPI JSON specification
func (h *Handler) serveOpenAPISpec(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"openapi": "3.0.0",
"info": map[string]interface{}{
"title": h.config.Swagger.Title,
"description": h.config.Swagger.Description,
"version": h.config.Swagger.Version,
"termsOfService": h.config.Swagger.TermsOfService,
"contact": map[string]interface{}{
"name": h.config.Swagger.ContactName,
"url": h.config.Swagger.ContactURL,
"email": h.config.Swagger.ContactEmail,
},
"license": map[string]interface{}{
"name": h.config.Swagger.LicenseName,
"url": h.config.Swagger.LicenseURL,
},
},
"servers": []map[string]interface{}{
{
"url": strings.Join([]string{strings.ToLower(h.config.Swagger.Schemes[0]), "://", h.config.Swagger.Host, h.config.Swagger.BasePath}, ""),
"description": "API Server",
},
},
"paths": map[string]interface{}{},
"components": map[string]interface{}{
"schemas": map[string]interface{}{},
"securitySchemes": map[string]interface{}{},
},
})
}
// serveOpenAPISpecYAML serves the OpenAPI YAML specification
func (h *Handler) serveOpenAPISpecYAML(c *gin.Context) {
c.YAML(http.StatusOK, map[string]interface{}{
"openapi": "3.0.0",
"info": map[string]interface{}{
"title": h.config.Swagger.Title,
"description": h.config.Swagger.Description,
"version": h.config.Swagger.Version,
"termsOfService": h.config.Swagger.TermsOfService,
"contact": map[string]interface{}{
"name": h.config.Swagger.ContactName,
"url": h.config.Swagger.ContactURL,
"email": h.config.Swagger.ContactEmail,
},
"license": map[string]interface{}{
"name": h.config.Swagger.LicenseName,
"url": h.config.Swagger.LicenseURL,
},
},
"servers": []map[string]interface{}{
{
"url": strings.Join([]string{strings.ToLower(h.config.Swagger.Schemes[0]), "://", h.config.Swagger.Host, h.config.Swagger.BasePath}, ""),
"description": "API Server",
},
},
"paths": map[string]interface{}{},
"components": map[string]interface{}{
"schemas": map[string]interface{}{},
"securitySchemes": map[string]interface{}{},
},
})
}

View File

@@ -7,8 +7,7 @@ import (
"time"
"api-service/internal/config"
models "api-service/internal/models/bpjs/vclaim"
vclaimModels "api-service/internal/models/vclaim"
services "api-service/internal/services/bpjs"
"github.com/gin-gonic/gin"
@@ -27,18 +26,22 @@ func NewSepHandler(cfg config.BpjsConfig) *SepHandler {
// CreateSEP godoc
// @Summary Create a new SEP
// @Description Create a new Surat Eligibilitas Peserta
// @Tags bpjs
// @Tags SEP
// @Accept json
// @Produce json
// @Param request body models.SepPostRequest true "SEP creation request"
// @Success 200 {object} models.SepResponse "SEP created successfully"
// @Param request body vclaimModels.SepPostRequest true "SEP creation request"
// @Success 200 {object} vclaimModels.SepResponse "SEP created successfully"
// @Failure 400 {object} gin.H "Invalid request"
// @Failure 500 {object} gin.H "Internal server error"
// @Router /sep [post]
func (h *SepHandler) CreateSEP(c *gin.Context) {
var req models.SepPostRequest
var req vclaimModels.SepPostRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body", "message": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid body",
"message": err.Error(),
})
return
}
@@ -46,32 +49,39 @@ func (h *SepHandler) CreateSEP(c *gin.Context) {
defer cancel()
var result map[string]interface{}
if err := h.service.Post(ctx, "/SEP/2.0/insert", req, &result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "create failed", "message": err.Error()})
if err := h.service.Post(ctx, "SEP/2.0/insert", req, &result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "create failed",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, models.SepResponse{
c.JSON(http.StatusOK, vclaimModels.SepResponse{
Message: "SEP berhasil dibuat",
Data: result,
})
}
// UpdateSEP godoc
// @Summary Update an existing SEP
// @Description Update an existing Surat Eligibilitas Peserta
// @Tags bpjs
// @Summary Update SEP
// @Description Update Surat Eligibilitas Peserta
// @Tags SEP
// @Accept json
// @Produce json
// @Param request body models.SepPutRequest true "SEP update request"
// @Success 200 {object} models.SepResponse "SEP updated successfully"
// @Param request body vclaimModels.SepPutRequest true "SEP update request"
// @Success 200 {object} vclaimModels.SepResponse "SEP updated successfully"
// @Failure 400 {object} gin.H "Invalid request"
// @Failure 500 {object} gin.H "Internal server error"
// @Router /sep [put]
func (h *SepHandler) UpdateSEP(c *gin.Context) {
var req models.SepPutRequest
var req vclaimModels.SepPutRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body", "message": err.Error()})
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid body",
"message": err.Error(),
})
return
}
@@ -79,85 +89,101 @@ func (h *SepHandler) UpdateSEP(c *gin.Context) {
defer cancel()
var result map[string]interface{}
if err := h.service.Put(ctx, "/SEP/2.0/update", req, &result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed", "message": err.Error()})
if err := h.service.Put(ctx, "SEP/2.0/update", req, &result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "update failed",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, models.SepResponse{
c.JSON(http.StatusOK, vclaimModels.SepResponse{
Message: "SEP berhasil diperbarui",
Data: result,
})
}
// DeleteSEP godoc
// @Summary Delete an existing SEP
// @Summary Delete SEP
// @Description Delete a Surat Eligibilitas Peserta by noSep
// @Tags bpjs
// @Tags SEP
// @Accept json
// @Produce json
// @Param noSep path string true "No SEP"
// @Param user query string true "User"
// @Success 200 {object} models.SepResponse "SEP deleted successfully"
// @Success 200 {object} vclaimModels.SepResponse "SEP deleted successfully"
// @Failure 400 {object} gin.H "Invalid request"
// @Failure 500 {object} gin.H "Internal server error"
// @Router /sep/{noSep} [delete]
func (h *SepHandler) DeleteSEP(c *gin.Context) {
noSep := c.Param("noSep")
user := c.Query("user")
if noSep == "" || user == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "noSep & user required"})
c.JSON(http.StatusBadRequest, gin.H{
"error": "noSep and user required",
})
return
}
body := models.SepDeleteRequest{}
body := vclaimModels.SepDeleteRequest{}
body.TSep.NoSep = noSep
body.TSep.User = user
ctx, cancel := context.WithTimeout(c, 30*time.Second)
defer cancel()
if err := h.service.Delete(ctx, "/SEP/2.0/delete", body); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed", "message": err.Error()})
if err := h.service.Delete(ctx, "SEP/2.0/delete", body); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "delete failed",
"message": err.Error(),
})
return
}
var result map[string]interface{}
c.JSON(http.StatusOK, models.SepResponse{
c.JSON(http.StatusOK, vclaimModels.SepResponse{
Message: "SEP berhasil dihapus",
Data: result,
})
}
// GetSEP godoc
// @Summary Get an existing SEP
// @Summary Get SEP
// @Description Retrieve a Surat Eligibilitas Peserta by noSep
// @Tags bpjs
// @Tags SEP
// @Accept json
// @Produce json
// @Param noSep path string true "No SEP"
// @Success 200 {object} models.SepResponse "Data SEP retrieved successfully"
// @Success 200 {object} vclaimModels.SepResponse "Data SEP retrieved successfully"
// @Failure 400 {object} gin.H "Invalid request"
// @Failure 500 {object} gin.H "Internal server error"
// @Router /sep/{noSep} [get]
func (h *SepHandler) GetSEP(c *gin.Context) {
noSep := c.Param("noSep")
if noSep == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "noSep required"})
c.JSON(http.StatusBadRequest, gin.H{
"error": "noSep required",
})
return
}
ctx, cancel := context.WithTimeout(c, 30*time.Second)
defer cancel()
endpoint := fmt.Sprintf("/SEP/%s", noSep)
endpoint := fmt.Sprintf("SEP/%s", noSep)
var result map[string]interface{}
if err := h.service.Get(ctx, endpoint, &result); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "fetch failed", "message": err.Error()})
c.JSON(http.StatusInternalServerError, gin.H{
"error": "fetch failed",
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, models.SepResponse{
c.JSON(http.StatusOK, vclaimModels.SepResponse{
Message: "Data SEP berhasil diambil",
Data: result,
})

View File

@@ -1,47 +0,0 @@
package models
// DiagnosaResponse represents the response structure for BPJS diagnosa data
type DiagnosaResponse struct {
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
// DiagnosaRawResponse represents the raw response structure from BPJS API
type DiagnosaRawResponse struct {
MetaData struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"metaData"`
Response interface{} `json:"response"`
}
// DiagnosaData represents the diagnosa reference data structure
type DiagnosaData struct {
KdDiag string `json:"kdDiag"`
NmDiag string `json:"nmDiag"`
}
// DiagnosaListResponse represents the response structure for diagnosa list
type DiagnosaListResponse struct {
Diagnosa []DiagnosaData `json:"diagnosa"`
}
// ErrorResponse represents error response structure
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code int `json:"code,omitempty"`
}
// BPJSMetaData represents BPJS API metadata structure
type BPJSMetaData struct {
Code string `json:"code"`
Message string `json:"message"`
}
// DiagnosaFilter represents filter parameters for diagnosa queries
type DiagnosaFilter struct {
NIK *string `form:"nik" json:"nik,omitempty"`
TglSEP *string `form:"tglSEP" json:"tglSEP,omitempty"`
}

View File

@@ -1,25 +1,25 @@
package models
// ==== REQUEST ====
// SepPostRequest represents the request payload for creating a SEP
type SepPostRequest struct {
TSep TSepPost `json:"t_sep" binding:"required"`
TSep TSepPost `json:"tsep" binding:"required"`
}
// TSepPost contains the main SEP data for creation
type TSepPost struct {
NoKartu string `json:"noKartu" binding:"required"`
TglSep string `json:"tglSep" binding:"required"` // yyyy-MM-dd
TglSep string `json:"tglSep" binding:"required"` // yyyy-MM-dd
PpkPelayanan string `json:"ppkPelayanan" binding:"required"`
JnsPelayanan string `json:"jnsPelayanan" binding:"required"`
KlsRawat KlsRawatPost `json:"klsRawat" binding:"required"`
NoMR string `json:"noMR" binding:"required"`
Rujukan Rujukan `json:"rujukan" binding:"required"`
Rujukan Rujukan `json:"rujukan" binding:"required"`
Catatan string `json:"catatan"`
DiagAwal string `json:"diagAwal" binding:"required"`
Poli Poli `json:"poli" binding:"required"`
Cob Flag `json:"cob" binding:"required"`
Katarak Flag `json:"katarak" binding:"required"`
Jaminan Jaminan `json:"jaminan" binding:"required"`
Cob Flag `json:"cob" binding:"required"`
Katarak Flag `json:"katarak" binding:"required"`
Jaminan Jaminan `json:"jaminan" binding:"required"`
TujuanKunj string `json:"tujuanKunj"`
FlagProcedure string `json:"flagProcedure"`
KdPenunjang string `json:"kdPenunjang"`
@@ -30,6 +30,7 @@ type TSepPost struct {
User string `json:"user" binding:"required"`
}
// KlsRawatPost represents class of care data for POST requests
type KlsRawatPost struct {
KlsRawatHak string `json:"klsRawatHak" binding:"required"`
KlsRawatNaik string `json:"klsRawatNaik"`
@@ -37,57 +38,65 @@ type KlsRawatPost struct {
PenanggungJawab string `json:"penanggungJawab"`
}
// Rujukan represents referral data
type Rujukan struct {
AsalRujukan string `json:"asalRujukan" binding:"required"`
TglRujukan string `json:"tglRujukan" binding:"required"`
NoRujukan string `json:"noRujukan" binding:"required"`
NoRujukan string `json:"noRujukan" binding:"required"`
PpkRujukan string `json:"ppkRujukan" binding:"required"`
}
// Poli represents poly/department data
type Poli struct {
Tujuan string `json:"tujuan"`
Tujuan string `json:"tujuan" binding:"required"`
Eksekutif string `json:"eksekutif" binding:"required"`
}
// Flag represents a generic flag structure
type Flag struct {
Flag string `json:"cob,omitempty" json:"katarak,omitempty" binding:"required"`
Flag string `json:"flag" binding:"required"`
}
// Jaminan represents insurance guarantee data
type Jaminan struct {
LakaLantas string `json:"lakaLantas" binding:"required"`
NoLP string `json:"noLP"`
Penjamin Penjamin `json:"penjamin"`
}
// Penjamin represents guarantor data
type Penjamin struct {
TglKejadian string `json:"tglKejadian"`
Keterangan string `json:"keterangan"`
Suplesi Suplesi `json:"suplesi"`
}
// Suplesi represents supplementary data
type Suplesi struct {
Suplesi string `json:"suplesi"`
NoSepSuplesi string `json:"noSepSuplesi"`
LokasiLaka LokasiLaka `json:"lokasiLaka"`
}
// LokasiLaka represents accident location data
type LokasiLaka struct {
KdPropinsi string `json:"kdPropinsi"`
KdKabupaten string `json:"kdKabupaten"`
KdKecamatan string `json:"kdKecamatan"`
}
// Skdp represents SKDP data
type Skdp struct {
NoSurat string `json:"noSurat" binding:"required"`
KodeDPJP string `json:"kodeDPJP" binding:"required"`
}
// ==== UPDATE ====
// SepPutRequest represents the request payload for updating a SEP
type SepPutRequest struct {
TSep TSepPut `json:"t_sep" binding:"required"`
TSep TSepPut `json:"tsep" binding:"required"`
}
// TSepPut contains the main SEP data for updates
type TSepPut struct {
NoSep string `json:"noSep" binding:"required"`
KlsRawat KlsRawatPut `json:"klsRawat"`
@@ -103,6 +112,7 @@ type TSepPut struct {
User string `json:"user" binding:"required"`
}
// KlsRawatPut represents class of care data for PUT requests
type KlsRawatPut struct {
KlsRawatHak string `json:"klsRawatHak"`
KlsRawatNaik string `json:"klsRawatNaik"`
@@ -110,22 +120,21 @@ type KlsRawatPut struct {
PenanggungJawab string `json:"penanggungJawab"`
}
// ==== DELETE ====
// SepDeleteRequest represents the request payload for deleting a SEP
type SepDeleteRequest struct {
TSep struct {
NoSep string `json:"noSep" binding:"required"`
User string `json:"user" binding:"required"`
} `json:"t_sep" binding:"required"`
User string `json:"user" binding:"required"`
} `json:"tsep" binding:"required"`
}
// ==== RESPONSE ====
// SepResponse represents the standard response for SEP operations
type SepResponse struct {
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
}
// SepRawResponse represents the raw response from BPJS API
type SepRawResponse struct {
MetaData struct {
Code string `json:"code"`

View File

@@ -2,18 +2,17 @@ package v1
import (
"api-service/internal/config"
"api-service/internal/database"
authHandlers "api-service/internal/handlers/auth"
bpjsPesertaHandlers "api-service/internal/handlers/bpjs/reference"
healthcheckHandlers "api-service/internal/handlers/healthcheck"
bpjsPesertaHandlers "api-service/internal/handlers/reference"
retribusiHandlers "api-service/internal/handlers/retribusi"
satusehatHandlers "api-service/internal/handlers/satusehat"
swaggerHandlers "api-service/internal/handlers/swagger"
"api-service/internal/middleware"
services "api-service/internal/services/auth"
satusehatServices "api-service/internal/services/satusehat"
"api-service/pkg/logger"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func RegisterRoutes(cfg *config.Config) *gin.Engine {
@@ -34,14 +33,19 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
logger.Fatal("Failed to initialize auth service")
}
// Initialize SatuSehat service
satusehatService := satusehatServices.NewSatuSehatService(&cfg.SatuSehat)
if satusehatService == nil {
logger.Fatal("Failed to initialize SatuSehat service")
}
// Initialize database service for health check
dbService := database.New(cfg)
// Swagger UI route
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Health check endpoint
healthCheckHandler := healthcheckHandlers.NewHealthCheckHandler(dbService)
sistem := router.Group("/api/sistem")
sistem.GET("/health", healthCheckHandler.CheckHealth)
// Initialize Swagger handler
swaggerHandler := swaggerHandlers.NewHandler(cfg)
// Register Swagger routes
swaggerHandler.RegisterRoutes(router)
// API v1 group
v1 := router.Group("/api/v1")
@@ -67,16 +71,6 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
// SatuSehat endpoints
satusehatPatientHandler := satusehatHandlers.NewPatientHandler(satusehatService)
satusehatGroup := v1.Group("/satusehat")
{
satusehatGroup.GET("/patient/search/nik", satusehatPatientHandler.SearchPatientByNIK)
satusehatGroup.GET("/patient/search/name", satusehatPatientHandler.SearchPatientByName)
satusehatGroup.POST("/patient", satusehatPatientHandler.CreatePatient)
satusehatGroup.GET("/token", satusehatPatientHandler.GetAccessToken)
}
// ============= PUBLISHED ROUTES ===============================================
// // Retribusi endpoints

View File

@@ -12,6 +12,8 @@ import (
"api-service/internal/config"
"github.com/mashingan/smapping"
"github.com/rs/zerolog/log"
"github.com/tidwall/gjson"
)
// VClaimService interface for VClaim operations
@@ -49,6 +51,11 @@ type ResponDTO struct {
// NewService creates a new VClaim service instance
func NewService(cfg config.BpjsConfig) VClaimService {
log.Info().
Str("base_url", cfg.BaseURL).
Dur("timeout", cfg.Timeout).
Msg("Creating new VClaim service instance")
service := &Service{
config: cfg,
httpClient: &http.Client{
@@ -88,8 +95,20 @@ func (s *Service) SetHTTPClient(client *http.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
log.Info().
Str("method", method).
Str("endpoint", endpoint).
Str("full_url", fullURL).
Msg("Preparing HTTP request")
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
if err != nil {
log.Error().
Err(err).
Str("method", method).
Str("endpoint", endpoint).
Msg("Failed to create HTTP request")
return nil, fmt.Errorf("failed to create request: %w", err)
}
@@ -102,6 +121,14 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
req.Header.Set("X-signature", xSignature)
req.Header.Set("user_key", userKey)
log.Debug().
Str("method", method).
Str("endpoint", endpoint).
Str("x_cons_id", consID).
Str("x_timestamp", tstamp).
Str("user_key", userKey).
Msg("Request headers set")
return req, nil
}
@@ -109,22 +136,55 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) {
defer res.Body.Close()
log.Info().
Int("status_code", res.StatusCode).
Str("status", res.Status).
Msg("Processing HTTP response")
body, err := io.ReadAll(res.Body)
if err != nil {
log.Error().
Err(err).
Int("status_code", res.StatusCode).
Msg("Failed to read response body")
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Log response body for debugging (truncate if too long)
bodyStr := string(body)
if len(bodyStr) > 1000 {
bodyStr = bodyStr[:1000] + "...(truncated)"
}
log.Debug().
Int("status_code", res.StatusCode).
Str("response_body", bodyStr).
Msg("Raw response received")
// Check HTTP status
if res.StatusCode >= 400 {
log.Error().
Int("status_code", res.StatusCode).
Str("response_body", bodyStr).
Msg("HTTP error response")
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 {
log.Error().
Err(err).
Int("status_code", res.StatusCode).
Msg("Failed to unmarshal raw response")
return nil, fmt.Errorf("failed to unmarshal raw response: %w", err)
}
// Log metadata
log.Info().
Str("meta_code", respMentah.MetaData.Code).
Str("meta_message", respMentah.MetaData.Message).
Msg("Response metadata")
// Create final response
finalResp := &ResponDTO{
MetaData: respMentah.MetaData,
@@ -132,6 +192,7 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) {
// If response is empty, return as is
if respMentah.Response == "" {
log.Debug().Msg("Empty response received, returning metadata only")
return finalResp, nil
}
@@ -139,17 +200,47 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) {
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp)
if err != nil {
log.Error().
Err(err).
Str("meta_code", respMentah.MetaData.Code).
Msg("Failed to decrypt response")
return nil, fmt.Errorf("failed to decrypt response: %w", err)
}
log.Debug().
Str("encrypted_length", fmt.Sprintf("%d bytes", len(respMentah.Response))).
Str("decrypted_length", fmt.Sprintf("%d bytes", len(respDecrypt))).
Msg("Response decrypted successfully")
// Unmarshal decrypted response
if respDecrypt != "" {
if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil {
// If JSON unmarshal fails, store as string
log.Warn().
Err(err).
Msg("Failed to unmarshal decrypted response, storing as string")
finalResp.Response = respDecrypt
} else {
log.Debug().Msg("Decrypted response unmarshaled successfully")
// Use gjson to extract and log some metadata from the response if it's JSON
if jsonBytes, err := json.Marshal(finalResp.Response); err == nil {
jsonStr := string(jsonBytes)
// Extract some common fields using gjson
if metaCode := gjson.Get(jsonStr, "metaData.code"); metaCode.Exists() {
log.Info().
Str("response_meta_code", metaCode.String()).
Msg("Final response metadata")
}
}
}
}
log.Info().
Str("meta_code", finalResp.MetaData.Code).
Str("meta_message", finalResp.MetaData.Message).
Msg("Response processing completed")
return finalResp, nil
}

View File

@@ -0,0 +1,676 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"api-service/internal/config"
"api-service/pkg/logger"
"github.com/mashingan/smapping"
"github.com/tidwall/gjson"
)
// SatuSehatService interface for SATUSEHAT operations
type SatuSehatService interface {
// Standard HTTP methods
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
// Raw response methods
GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error)
PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error)
// FHIR specific methods
PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error)
GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error)
// Token management
RefreshToken(ctx context.Context) error
IsTokenValid() bool
GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error)
}
// SatuSehatService struct for SATUSEHAT service
type SatuSehatServiceStruct struct {
config config.SatuSehatConfig
httpClient *http.Client
token TokenDetail
tokenMutex sync.RWMutex
}
// Token detail structure
type TokenDetail struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
IssuedAt int64 `json:"issued_at"`
OrganizationName string `json:"organization_name"`
DeveloperEmail string `json:"developer.email"`
ClientID string `json:"client_id"`
ApplicationName string `json:"application_name"`
Status string `json:"status"`
ExpiryTime time.Time `json:"-"`
}
// Response structures
type SatuSehatResponMentahDTO struct {
StatusCode int `json:"status_code"`
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
type SatuSehatResponDTO struct {
StatusCode int `json:"status_code"`
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data"`
Error *ErrorInfo `json:"error,omitempty"`
}
type ErrorInfo struct {
Code string `json:"code"`
Details string `json:"details"`
}
// Token methods
func (t *TokenDetail) IsExpired() bool {
if t.ExpiryTime.IsZero() {
return true
}
return time.Now().UTC().After(t.ExpiryTime.Add(-5 * time.Minute))
}
func (t *TokenDetail) SetExpired() {
t.ExpiryTime = time.Time{}
}
// NewSatuSehatService creates a new SATUSEHAT service instance
func NewSatuSehatService(cfg config.SatuSehatConfig) SatuSehatService {
service := &SatuSehatServiceStruct{
config: cfg,
httpClient: &http.Client{
Timeout: cfg.Timeout,
},
}
return service
}
// NewSatuSehatServiceFromConfig creates service from main config
func NewSatuSehatServiceFromConfig(cfg *config.Config) SatuSehatService {
return NewSatuSehatService(cfg.SatuSehat)
}
// NewSatuSehatServiceFromInterface creates service from interface (for backward compatibility)
func NewSatuSehatServiceFromInterface(cfg interface{}) (SatuSehatService, error) {
var satusehatConfig config.SatuSehatConfig
// Try to map from interface
err := smapping.FillStruct(&satusehatConfig, smapping.MapFields(&cfg))
if err != nil {
return nil, fmt.Errorf("failed to map config: %w", err)
}
if satusehatConfig.Timeout == 0 {
satusehatConfig.Timeout = 30 * time.Second
}
return NewSatuSehatService(satusehatConfig), nil
}
// SetHTTPClient allows custom http client configuration
func (s *SatuSehatServiceStruct) SetHTTPClient(client *http.Client) {
s.httpClient = client
}
// RefreshToken obtains new access token
func (s *SatuSehatServiceStruct) RefreshToken(ctx context.Context) error {
s.tokenMutex.Lock()
defer s.tokenMutex.Unlock()
// Double-check pattern
if !s.token.IsExpired() {
return nil
}
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
formData := fmt.Sprintf("client_id=%s&client_secret=%s", s.config.ClientID, s.config.ClientSecret)
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(formData))
if err != nil {
return fmt.Errorf("failed to create token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute token request: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read token response: %w", err)
}
if res.StatusCode != 200 {
// Log the error response for debugging
fmt.Printf("DEBUG: Token request failed with status %d: %s\n", res.StatusCode, string(body))
return fmt.Errorf("token request failed with status %d: %s", res.StatusCode, string(body))
}
// Debug: log the raw response for troubleshooting
fmt.Printf("DEBUG: SATUSEHAT token response - Status: %d, Body: %s\n", res.StatusCode, string(body))
fmt.Printf("DEBUG: Request URL: %s\n", tokenURL)
fmt.Printf("DEBUG: Request Headers: %+v\n", req.Header)
return s.parseTokenResponse(body)
}
// parseTokenResponse parses token response from SATUSEHAT
func (s *SatuSehatServiceStruct) parseTokenResponse(body []byte) error {
// Debug: log the raw response for detailed analysis
fmt.Printf("DEBUG: Raw token response body: %s\n", string(body))
result := gjson.ParseBytes(body)
// Check if we have a valid access token
accessToken := result.Get("access_token").String()
if accessToken == "" {
return fmt.Errorf("no access token found in response: %s", string(body))
}
issuedAt := result.Get("issued_at").Int()
expiresIn := result.Get("expires_in").Int()
// Handle timestamp conversion (issued_at could be in milliseconds or seconds)
var expiryTime time.Time
if issuedAt > 1000000000000 { // If timestamp is in milliseconds
expiryTime = time.Unix(issuedAt/1000, 0).Add(time.Duration(expiresIn) * time.Second)
} else if issuedAt > 0 { // If timestamp is in seconds
expiryTime = time.Unix(issuedAt, 0).Add(time.Duration(expiresIn) * time.Second)
} else {
// If no issued_at, use current time + expires_in
expiryTime = time.Now().UTC().Add(time.Duration(expiresIn) * time.Second)
}
s.token = TokenDetail{
AccessToken: accessToken,
TokenType: result.Get("token_type").String(),
ExpiresIn: expiresIn,
IssuedAt: issuedAt,
OrganizationName: result.Get("organization_name").String(),
DeveloperEmail: result.Get("developer\\.email").String(),
ClientID: result.Get("client_id").String(),
ApplicationName: result.Get("application_name").String(),
Status: result.Get("status").String(),
ExpiryTime: expiryTime,
}
logger.Info("SATUSEHAT token refreshed successfully", map[string]interface{}{
"expires_at": s.token.ExpiryTime,
"organization": s.token.OrganizationName,
"token_type": s.token.TokenType,
"client_id": s.token.ClientID,
})
return nil
}
// IsTokenValid checks if current token is valid
func (s *SatuSehatServiceStruct) IsTokenValid() bool {
s.tokenMutex.RLock()
defer s.tokenMutex.RUnlock()
return !s.token.IsExpired()
}
// ensureValidToken ensures we have a valid token
func (s *SatuSehatServiceStruct) ensureValidToken(ctx context.Context) error {
s.tokenMutex.RLock()
needsRefresh := s.token.IsExpired()
s.tokenMutex.RUnlock()
if needsRefresh {
return s.RefreshToken(ctx)
}
return nil
}
// prepareRequest prepares HTTP request with required headers
func (s *SatuSehatServiceStruct) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
// Ensure valid token
if err := s.ensureValidToken(ctx); err != nil {
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
}
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
s.tokenMutex.RLock()
token := s.token.AccessToken
s.tokenMutex.RUnlock()
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return req, nil
}
// processResponse processes response from SATUSEHAT API
func (s *SatuSehatServiceStruct) processResponse(res *http.Response) (*SatuSehatResponDTO, 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)
}
// Create response
resp := &SatuSehatResponDTO{
StatusCode: res.StatusCode,
Success: res.StatusCode >= 200 && res.StatusCode < 300,
}
// Handle different status codes
switch {
case res.StatusCode == 401:
s.tokenMutex.Lock()
s.token.SetExpired()
s.tokenMutex.Unlock()
resp.Error = &ErrorInfo{
Code: "UNAUTHORIZED",
Details: "Token expired or invalid",
}
resp.Message = "Unauthorized access"
case res.StatusCode >= 400 && res.StatusCode < 500:
resp.Error = &ErrorInfo{
Code: "CLIENT_ERROR",
Details: string(body),
}
resp.Message = "Client error"
case res.StatusCode >= 500:
resp.Error = &ErrorInfo{
Code: "SERVER_ERROR",
Details: string(body),
}
resp.Message = "Server error"
default:
resp.Message = "Success"
}
// Parse JSON response if successful
if resp.Success && len(body) > 0 {
var jsonData interface{}
if err := json.Unmarshal(body, &jsonData); err != nil {
// If JSON unmarshal fails, store as string
resp.Data = string(body)
} else {
resp.Data = jsonData
}
}
return resp, nil
}
// Get performs HTTP GET request
func (s *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, 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 *SatuSehatServiceStruct) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, 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)
}
// FHIR-specific methods
// PostBundle posts FHIR bundle to SATUSEHAT
func (s *SatuSehatServiceStruct) PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error) {
return s.PostRawResponse(ctx, "", bundle)
}
// GetPatientByNIK retrieves patient by NIK
func (s *SatuSehatServiceStruct) GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
endpoint := fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
return s.GetRawResponse(ctx, endpoint)
}
// GetPractitionerByNIK retrieves practitioner by NIK
func (s *SatuSehatServiceStruct) GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
endpoint := fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
return s.GetRawResponse(ctx, endpoint)
}
// GetResourceByID retrieves any FHIR resource by ID
func (s *SatuSehatServiceStruct) GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error) {
endpoint := fmt.Sprintf("/%s/%s", resourceType, id)
return s.GetRawResponse(ctx, endpoint)
}
// GenerateToken generates a new access token with custom client credentials
func (s *SatuSehatServiceStruct) GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error) {
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
formData := fmt.Sprintf("client_id=%s&client_secret=%s", clientID, clientSecret)
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData))
if err != nil {
return nil, fmt.Errorf("failed to create token request: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute token request: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token response: %w", err)
}
// Process the response using the existing response processor
resp := &SatuSehatResponDTO{
StatusCode: res.StatusCode,
Success: res.StatusCode >= 200 && res.StatusCode < 300,
}
// Handle different status codes
switch {
case res.StatusCode == 401:
resp.Error = &ErrorInfo{
Code: "UNAUTHORIZED",
Details: "Invalid client credentials",
}
resp.Message = "Unauthorized access"
case res.StatusCode >= 400 && res.StatusCode < 500:
resp.Error = &ErrorInfo{
Code: "CLIENT_ERROR",
Details: string(body),
}
resp.Message = "Client error"
case res.StatusCode >= 500:
resp.Error = &ErrorInfo{
Code: "SERVER_ERROR",
Details: string(body),
}
resp.Message = "Server error"
default:
resp.Message = "Success"
}
// Parse JSON response if successful
if resp.Success && len(body) > 0 {
var jsonData interface{}
if err := json.Unmarshal(body, &jsonData); err != nil {
// If JSON unmarshal fails, store as string
resp.Data = string(body)
} else {
resp.Data = jsonData
}
}
return resp, nil
}
// Helper functions
// mapToResult maps the final response to the result interface
func mapToResult(resp *SatuSehatResponDTO, 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 SatuSehatGetRequest(endpoint string, cfg interface{}) interface{} {
service, err := NewSatuSehatServiceFromInterface(cfg)
if err != nil {
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
"error": err.Error(),
})
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.GetRawResponse(ctx, endpoint)
if err != nil {
logger.Error("Failed to get SATUSEHAT response", map[string]interface{}{
"error": err.Error(),
})
return nil
}
return resp
}
func SatuSehatPostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
service, err := NewSatuSehatServiceFromInterface(cfg)
if err != nil {
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
"error": err.Error(),
})
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.PostRawResponse(ctx, endpoint, data)
if err != nil {
logger.Error("Failed to post SATUSEHAT response", map[string]interface{}{
"error": err.Error(),
})
return nil
}
return resp
}
// FHIR helper functions
func SatuSehatGetPatient(nik string, cfg interface{}) interface{} {
service, err := NewSatuSehatServiceFromInterface(cfg)
if err != nil {
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
"error": err.Error(),
})
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.GetPatientByNIK(ctx, nik)
if err != nil {
logger.Error("Failed to get patient", map[string]interface{}{
"error": err.Error(),
"nik": nik,
})
return nil
}
return resp
}
func SatuSehatGetPractitioner(nik string, cfg interface{}) interface{} {
service, err := NewSatuSehatServiceFromInterface(cfg)
if err != nil {
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
"error": err.Error(),
})
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.GetPractitionerByNIK(ctx, nik)
if err != nil {
logger.Error("Failed to get practitioner", map[string]interface{}{
"error": err.Error(),
"nik": nik,
})
return nil
}
return resp
}
func SatuSehatPostBundle(bundle interface{}, cfg interface{}) interface{} {
service, err := NewSatuSehatServiceFromInterface(cfg)
if err != nil {
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
"error": err.Error(),
})
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.PostBundle(ctx, bundle)
if err != nil {
logger.Error("Failed to post bundle", map[string]interface{}{
"error": err.Error(),
})
return nil
}
return resp
}

View File

@@ -1,350 +0,0 @@
package satusehat
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"api-service/internal/config"
)
type SatuSehatService struct {
config *config.SatuSehatConfig
client *http.Client
token *TokenResponse
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
IssuedAt time.Time
}
type PatientResponse struct {
ResourceType string `json:"resourceType"`
ID string `json:"id"`
Meta struct {
VersionID string `json:"versionId"`
LastUpdated string `json:"lastUpdated"`
} `json:"meta"`
Type string `json:"type"`
Total int `json:"total"`
Link []Link `json:"link"`
Entry []Entry `json:"entry"`
}
type Link struct {
Relation string `json:"relation"`
URL string `json:"url"`
}
type Entry struct {
FullURL string `json:"fullUrl"`
Resource struct {
ResourceType string `json:"resourceType"`
ID string `json:"id"`
Meta struct {
VersionID string `json:"versionId"`
LastUpdated string `json:"lastUpdated"`
Profile []string `json:"profile"`
} `json:"meta"`
Identifier []Identifier `json:"identifier"`
Name []Name `json:"name"`
Telecom []Telecom `json:"telecom"`
Gender string `json:"gender"`
BirthDate string `json:"birthDate"`
Deceased bool `json:"deceasedBoolean"`
Address []Address `json:"address"`
MaritalStatus struct {
Coding []Coding `json:"coding"`
} `json:"maritalStatus"`
MultipleBirth bool `json:"multipleBirthBoolean"`
Contact []Contact `json:"contact"`
Communication []Communication `json:"communication"`
Extension []Extension `json:"extension"`
} `json:"resource"`
Search struct {
Mode string `json:"mode"`
} `json:"search"`
}
type Identifier struct {
System string `json:"system"`
Value string `json:"value"`
Use string `json:"use,omitempty"`
}
type Name struct {
Use string `json:"use"`
Text string `json:"text"`
Family string `json:"family"`
Given []string `json:"given"`
}
type Telecom struct {
System string `json:"system"`
Value string `json:"value"`
Use string `json:"use,omitempty"`
}
type Address struct {
Use string `json:"use"`
Type string `json:"type"`
Line []string `json:"line"`
City string `json:"city"`
PostalCode string `json:"postalCode"`
Country string `json:"country"`
Extension []Extension `json:"extension"`
}
type Coding struct {
System string `json:"system"`
Code string `json:"code"`
Display string `json:"display"`
}
type Contact struct {
Relationship []Coding `json:"relationship"`
Name Name `json:"name"`
Telecom []Telecom `json:"telecom"`
Address Address `json:"address"`
Gender string `json:"gender"`
}
type Communication struct {
Language Coding `json:"language"`
Preferred bool `json:"preferred"`
}
type Extension struct {
URL string `json:"url"`
ValueAddress Address `json:"valueAddress,omitempty"`
ValueCode string `json:"valueCode,omitempty"`
}
func NewSatuSehatService(cfg *config.SatuSehatConfig) *SatuSehatService {
return &SatuSehatService{
config: cfg,
client: &http.Client{
Timeout: cfg.Timeout,
},
}
}
func (s *SatuSehatService) GetAccessToken(ctx context.Context) (*TokenResponse, error) {
// Check if we have a valid token
if s.token != nil && time.Since(s.token.IssuedAt) < time.Duration(s.token.ExpiresIn-60)*time.Second {
return s.token, nil
}
url := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.SetBasicAuth(s.config.ClientID, s.config.ClientSecret)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get access token: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get access token, status: %s", resp.Status)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return nil, fmt.Errorf("failed to decode token response: %v", err)
}
tokenResp.IssuedAt = time.Now()
s.token = &tokenResp
return &tokenResp, nil
}
func (s *SatuSehatService) SearchPatientByNIK(ctx context.Context, nik string) (*PatientResponse, error) {
token, err := s.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get access token: %v", err)
}
url := fmt.Sprintf("%s/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", s.config.BaseURL, nik)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to search patient: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status)
}
var patientResp PatientResponse
if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil {
return nil, fmt.Errorf("failed to decode patient response: %v", err)
}
return &patientResp, nil
}
func (s *SatuSehatService) SearchPatientByName(ctx context.Context, name string) (*PatientResponse, error) {
token, err := s.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get access token: %v", err)
}
url := fmt.Sprintf("%s/Patient?name=%s", s.config.BaseURL, name)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to search patient: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status)
}
var patientResp PatientResponse
if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil {
return nil, fmt.Errorf("failed to decode patient response: %v", err)
}
return &patientResp, nil
}
func (s *SatuSehatService) CreatePatient(ctx context.Context, patientData map[string]interface{}) (map[string]interface{}, error) {
token, err := s.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get access token: %v", err)
}
url := fmt.Sprintf("%s/Patient", s.config.BaseURL)
patientData["resourceType"] = "Patient"
jsonData, err := json.Marshal(patientData)
if err != nil {
return nil, fmt.Errorf("failed to marshal patient data: %v", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData)))
if err != nil {
return nil, fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to create patient: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("failed to create patient, status: %s", resp.Status)
}
var response map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
return nil, fmt.Errorf("failed to decode response: %v", err)
}
return response, nil
}
// Helper function to extract patient information
func ExtractPatientInfo(patientResp *PatientResponse) (map[string]interface{}, error) {
if patientResp == nil || len(patientResp.Entry) == 0 {
return nil, fmt.Errorf("no patient data found")
}
entry := patientResp.Entry[0]
resource := entry.Resource
patientInfo := map[string]interface{}{
"id": resource.ID,
"name": ExtractPatientName(resource.Name),
"nik": ExtractNIK(resource.Identifier),
"gender": resource.Gender,
"birthDate": resource.BirthDate,
"address": ExtractAddress(resource.Address),
"phone": ExtractPhone(resource.Telecom),
"lastUpdated": resource.Meta.LastUpdated,
}
return patientInfo, nil
}
func ExtractPatientName(names []Name) string {
for _, name := range names {
if name.Use == "official" || name.Text != "" {
if name.Text != "" {
return name.Text
}
return fmt.Sprintf("%s %s", strings.Join(name.Given, " "), name.Family)
}
}
return ""
}
func ExtractNIK(identifiers []Identifier) string {
for _, ident := range identifiers {
if ident.System == "https://fhir.kemkes.go.id/id/nik" {
return ident.Value
}
}
return ""
}
func ExtractAddress(addresses []Address) map[string]interface{} {
if len(addresses) == 0 {
return nil
}
addr := addresses[0]
return map[string]interface{}{
"line": strings.Join(addr.Line, ", "),
"city": addr.City,
"postalCode": addr.PostalCode,
"country": addr.Country,
}
}
func ExtractPhone(telecoms []Telecom) string {
for _, telecom := range telecoms {
if telecom.System == "phone" {
return telecom.Value
}
}
return ""
}

34
test_swagger_env.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"fmt"
"os"
)
func main() {
// Set environment variables for testing
os.Setenv("SWAGGER_TITLE", "My Custom API Service")
os.Setenv("SWAGGER_DESCRIPTION", "This is a custom API service for managing various resources")
os.Setenv("SWAGGER_VERSION", "2.0.0")
os.Setenv("SWAGGER_CONTACT_NAME", "Support Team")
os.Setenv("SWAGGER_CONTACT_URL", "https://mycompany.com/support")
os.Setenv("SWAGGER_CONTACT_EMAIL", "support@mycompany.com")
os.Setenv("SWAGGER_LICENSE_NAME", "MIT License")
os.Setenv("SWAGGER_LICENSE_URL", "https://opensource.org/licenses/MIT")
os.Setenv("SWAGGER_HOST", "api.mycompany.com:8080")
os.Setenv("SWAGGER_BASE_PATH", "/api/v2")
os.Setenv("SWAGGER_SCHEMES", "https")
fmt.Println("Environment variables set for Swagger documentation:")
fmt.Println("SWAGGER_TITLE:", os.Getenv("SWAGGER_TITLE"))
fmt.Println("SWAGGER_DESCRIPTION:", os.Getenv("SWAGGER_DESCRIPTION"))
fmt.Println("SWAGGER_VERSION:", os.Getenv("SWAGGER_VERSION"))
fmt.Println("SWAGGER_CONTACT_NAME:", os.Getenv("SWAGGER_CONTACT_NAME"))
fmt.Println("SWAGGER_HOST:", os.Getenv("SWAGGER_HOST"))
fmt.Println("SWAGGER_BASE_PATH:", os.Getenv("SWAGGER_BASE_PATH"))
fmt.Println("SWAGGER_SCHEMES:", os.Getenv("SWAGGER_SCHEMES"))
fmt.Println("\nTo test the Swagger generation, run:")
fmt.Println("swag init -g cmd/api/main.go --parseDependency --parseInternal")
fmt.Println("Then check docs/docs.go to see the updated values")
}

View File

@@ -13,8 +13,7 @@ type HandlerData struct {
Name string
NameLower string
NamePlural string
Category string
CategoryPath string
Category string // Untuk backward compatibility (bagian pertama)
ModuleName string
TableName string
HasGet bool
@@ -48,16 +47,18 @@ func main() {
methods = []string{"get", "post", "put", "delete", "dynamic", "search"}
}
// Parse category and entity
// Parse category and entity - support up to 4 levels
var category, entityName string
if strings.Contains(entityPath, "/") {
parts := strings.Split(entityPath, "/")
if len(parts) != 2 {
fmt.Println("❌ Error: Invalid path format. Use 'category/entity' or just 'entity'")
if len(parts) < 2 || len(parts) > 4 {
fmt.Println("❌ Error: Invalid path format. Use up to 4 levels like 'level1/level2/level3/entity'")
os.Exit(1)
}
// Entity name is the last part
entityName = parts[len(parts)-1]
// Category for backward compatibility (first part)
category = parts[0]
entityName = parts[1]
} else {
category = ""
entityName = entityPath
@@ -81,7 +82,6 @@ func main() {
NameLower: entityLower,
NamePlural: entityPlural,
Category: category,
CategoryPath: category,
ModuleName: "api-service",
TableName: tableName,
HasPagination: true,

View File