Initial commit
Some checks failed
Go-test / build (push) Has been cancelled

This commit is contained in:
2025-09-04 11:45:34 +07:00
parent 8311311615
commit 6d57abf442
22 changed files with 1361 additions and 5908 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -38,20 +38,6 @@ definitions:
timestamp:
type: string
type: object
api-service_internal_models.ErrorResponseBpjs:
properties:
code:
type: string
errors:
additionalProperties: true
type: object
message:
type: string
request_id:
type: string
status:
type: string
type: object
api-service_internal_models.MetaResponse:
properties:
current_page:
@@ -106,6 +92,45 @@ definitions:
username:
type: string
type: object
api-service_internal_models_qris.Qris:
properties:
created_at:
$ref: '#/definitions/sql.NullTime'
display_name:
$ref: '#/definitions/sql.NullString'
id:
type: string
sort:
$ref: '#/definitions/api-service_internal_models.NullableInt32'
status:
type: string
updated_at:
$ref: '#/definitions/sql.NullTime'
user_created:
$ref: '#/definitions/sql.NullString'
user_updated:
$ref: '#/definitions/sql.NullString'
type: object
api-service_internal_models_qris.QrisGetByIDResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_qris.Qris'
message:
type: string
type: object
api-service_internal_models_qris.QrisGetResponse:
properties:
data:
items:
$ref: '#/definitions/api-service_internal_models_qris.Qris'
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.Retribusi:
properties:
date_created:
@@ -306,345 +331,6 @@ definitions:
message:
type: string
type: object
api-service_internal_models_vclaim_peserta.PesertaData:
properties:
cob:
properties:
nmAsuransi: {}
noAsuransi: {}
tglTAT: {}
tglTMT: {}
type: object
hakKelas:
properties:
keterangan:
type: string
kode:
type: string
type: object
informasi:
properties:
dinsos: {}
eSEP: {}
noSKTM: {}
prolanisPRB:
type: string
type: object
jenisPeserta:
properties:
keterangan:
type: string
kode:
type: string
type: object
mr:
properties:
noMR:
type: string
noTelepon:
type: string
type: object
nama:
type: string
nik:
type: string
noKartu:
type: string
pisa:
type: string
provUmum:
properties:
kdProvider:
type: string
nmProvider:
type: string
type: object
sex:
type: string
statusPeserta:
properties:
keterangan:
type: string
kode:
type: string
type: object
tglCetakKartu:
type: string
tglLahir:
type: string
tglTAT:
type: string
tglTMT:
type: string
umur:
properties:
umurSaatPelayanan:
type: string
umurSekarang:
type: string
type: object
type: object
api-service_internal_models_vclaim_rujukan.RujukanData:
properties:
diagnosa:
properties:
kdDiagnosa:
type: string
nmDiagnosa:
type: string
type: object
kelasRawat:
type: string
nama:
type: string
noKartu:
type: string
noRujukan:
type: string
pelayanan:
type: string
poliRujukan:
properties:
kdPoli:
type: string
nmPoli:
type: string
type: object
provPerujuk:
properties:
kdProvider:
type: string
nmProvider:
type: string
type: object
statusRujukan:
type: string
tglRujukan:
type: string
type: object
api-service_internal_models_vclaim_rujukan.RujukanResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanData'
list:
items:
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanData'
type: array
message:
type: string
request_id:
type: string
status:
type: string
timestamp:
type: string
type: object
api-service_internal_models_vclaim_sep.SepData:
properties:
catatan:
type: string
diagnosa:
type: string
informasi:
properties:
dpjpLayan:
type: string
noSKDP:
type: string
noTelp:
type: string
subSpesialis:
type: string
type: object
jnsPelayanan:
type: string
klsRawat:
type: string
noMR:
type: string
noSep:
type: string
peserta:
$ref: '#/definitions/api-service_internal_models_vclaim_peserta.PesertaData'
poli:
type: string
rujukan:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRujukan'
tglSep:
type: string
type: object
api-service_internal_models_vclaim_sep.SepRequest:
properties:
catatan:
type: string
diagnosa:
type: string
jnsPelayanan:
enum:
- "1"
- "2"
type: string
klsRawat:
enum:
- "1"
- "2"
- "3"
type: string
noKartu:
type: string
noMR:
type: string
noTelp:
type: string
poli:
type: string
ppkPelayanan:
type: string
request_id:
type: string
rujukan:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRujukan'
tglSep:
type: string
timestamp:
type: string
user:
type: string
required:
- diagnosa
- jnsPelayanan
- klsRawat
- noKartu
- noMR
- poli
- ppkPelayanan
- tglSep
- user
type: object
api-service_internal_models_vclaim_sep.SepResponse:
properties:
data:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepData'
message:
type: string
request_id:
type: string
status:
type: string
timestamp:
type: string
type: object
api-service_internal_models_vclaim_sep.SepRujukan:
properties:
asalRujukan:
enum:
- "1"
- "2"
type: string
noRujukan:
type: string
ppkRujukan:
type: string
tglRujukan:
type: string
required:
- asalRujukan
- noRujukan
- ppkRujukan
- tglRujukan
type: object
internal_handlers_vclaim_peserta.PesertaData:
properties:
cob:
properties:
nmAsuransi: {}
noAsuransi: {}
tglTAT: {}
tglTMT: {}
type: object
hakKelas:
properties:
keterangan:
type: string
kode:
type: string
type: object
informasi:
properties:
dinsos: {}
eSEP: {}
noSKTM: {}
prolanisPRB:
type: string
type: object
jenisPeserta:
properties:
keterangan:
type: string
kode:
type: string
type: object
mr:
properties:
noMR:
type: string
noTelepon:
type: string
type: object
nama:
type: string
nik:
type: string
noKartu:
type: string
pisa:
type: string
provUmum:
properties:
kdProvider:
type: string
nmProvider:
type: string
type: object
sex:
type: string
statusPeserta:
properties:
keterangan:
type: string
kode:
type: string
type: object
tglCetakKartu:
type: string
tglLahir:
type: string
tglTAT:
type: string
tglTMT:
type: string
umur:
properties:
umurSaatPelayanan:
type: string
umurSekarang:
type: string
type: object
type: object
internal_handlers_vclaim_peserta.PesertaResponse:
properties:
data:
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaData'
message:
type: string
metaData: {}
request_id:
type: string
status:
type: string
timestamp:
type: string
type: object
sql.NullString:
properties:
string:
@@ -675,138 +361,6 @@ info:
title: API Service
version: 1.0.0
paths:
/Peserta/nik/nik/:nik/tglSEP/:tglsep:
get:
consumes:
- application/json
description: Get participant eligibility information by NIK
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nik
example: '"example_value"'
in: path
name: nik
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully retrieved PesertaBynik data
schema:
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - PesertaBynik not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Get PesertaBynik data
tags:
- Peserta
/Rujukan/:norujukan:
get:
consumes:
- application/json
description: Manage rujukan
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: norujukan
example: '"example_value"'
in: path
name: norujukan
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully retrieved RujukanBynorujukan data
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - RujukanBynorujukan not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Get RujukanBynorujukan data
tags:
- Rujukan
/Rujukan/Peserta/:nokartu:
get:
consumes:
- application/json
description: Manage rujukan
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nokartu
example: '"example_value"'
in: path
name: nokartu
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully retrieved RujukanBynokartu data
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_rujukan.RujukanResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - RujukanBynokartu not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Get RujukanBynokartu data
tags:
- Rujukan
/api/v1/auth/login:
post:
consumes:
@@ -930,6 +484,110 @@ paths:
summary: Register new user
tags:
- Authentication
/api/v1/qris/{id}:
get:
consumes:
- application/json
description: Returns a single qris by ID
parameters:
- description: Qris ID (UUID)
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_qris.QrisGetByIDResponse'
"400":
description: Invalid ID format
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
"404":
description: Qris not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
summary: Get Qris by ID
tags:
- Qris
/api/v1/qriss:
get:
consumes:
- application/json
description: Returns a paginated list of qriss with optional summary statistics
parameters:
- default: 10
description: Limit (max 100)
in: query
name: limit
type: integer
- default: 0
description: Offset
in: query
name: offset
type: integer
- default: false
description: Include aggregation summary
in: query
name: include_summary
type: boolean
- description: Filter by status
in: query
name: status
type: string
- description: Search in multiple fields
in: query
name: search
type: string
produces:
- application/json
responses:
"200":
description: Success response
schema:
$ref: '#/definitions/api-service_internal_models_qris.QrisGetResponse'
"400":
description: Bad request
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
summary: Get qris with pagination and optional aggregation
tags:
- Qris
/api/v1/qriss/stats:
get:
consumes:
- application/json
description: Returns comprehensive statistics about qris data
parameters:
- description: Filter statistics by status
in: query
name: status
type: string
produces:
- application/json
responses:
"200":
description: Statistics data
schema:
$ref: '#/definitions/api-service_internal_models.AggregateData'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponse'
summary: Get qris statistics
tags:
- Qris
/api/v1/retribusi/{id}:
delete:
consumes:
@@ -1252,230 +910,6 @@ paths:
summary: Generate token directly
tags:
- Token
/peserta/nokartu/:nokartu/tglSEP/:tglsep:
get:
consumes:
- application/json
description: Get participant eligibility information by card number
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nokartu
example: '"example_value"'
in: path
name: nokartu
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully retrieved PesertaBynokartu data
schema:
$ref: '#/definitions/internal_handlers_vclaim_peserta.PesertaResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - PesertaBynokartu not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Get PesertaBynokartu data
tags:
- Peserta
/sep:
post:
consumes:
- application/json
description: Manage SEP (Surat Eligibilitas Peserta)
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: SepSep data
in: body
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRequest'
produces:
- application/json
responses:
"201":
description: Successfully created SepSep
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
"400":
description: Bad request - invalid request body or validation error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"409":
description: Conflict - SepSep already exists
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Create new SepSep
tags:
- Sep
/sep/:nosep:
delete:
consumes:
- application/json
description: Manage SEP (Surat Eligibilitas Peserta)
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nosep
example: '"example_value"'
in: path
name: nosep
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully deleted SepSep
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - SepSep not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Delete existing SepSep
tags:
- Sep
get:
consumes:
- application/json
description: Manage SEP (Surat Eligibilitas Peserta)
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nosep
example: '"example_value"'
in: path
name: nosep
required: true
type: string
produces:
- application/json
responses:
"200":
description: Successfully retrieved SepSep data
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
"400":
description: Bad request - invalid parameters
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - SepSep not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Get SepSep data
tags:
- Sep
put:
consumes:
- application/json
description: Manage SEP (Surat Eligibilitas Peserta)
parameters:
- description: Request ID for tracking
in: header
name: X-Request-ID
type: string
- description: nosep
example: '"example_value"'
in: path
name: nosep
required: true
type: string
- description: SepSep data
in: body
name: request
required: true
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepRequest'
produces:
- application/json
responses:
"200":
description: Successfully updated SepSep
schema:
$ref: '#/definitions/api-service_internal_models_vclaim_sep.SepResponse'
"400":
description: Bad request - invalid parameters or request body
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"401":
description: Unauthorized - invalid API credentials
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"404":
description: Not found - SepSep not found
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
"500":
description: Internal server error
schema:
$ref: '#/definitions/api-service_internal_models.ErrorResponseBpjs'
security:
- ApiKeyAuth: []
summary: Update existing SepSep
tags:
- Sep
schemes:
- http
- https

View File

@@ -1,175 +0,0 @@
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(" -----------------------")
}

View File

@@ -200,7 +200,7 @@ func (c *Config) loadDatabaseConfigs() {
Port: getEnvAsInt("DB_PORT", 5432),
Username: getEnv("DB_USERNAME", ""),
Password: getEnv("DB_PASSWORD", ""),
Database: getEnv("DB_DATABASE", "satu_db"),
Database: getEnv("DB_DATABASE", "simrs_backup"),
Schema: getEnv("DB_SCHEMA", "public"),
SSLMode: getEnv("DB_SSLMODE", "disable"),
MaxOpenConns: getEnvAsInt("DB_MAX_OPEN_CONNS", 25),
@@ -669,7 +669,7 @@ func (c *Config) Validate() error {
}
}
if c.Bpjs.BaseURL == "" {
/*if c.Bpjs.BaseURL == "" {
log.Fatal("BPJS Base URL is required")
}
if c.Bpjs.ConsID == "" {
@@ -680,7 +680,7 @@ func (c *Config) Validate() error {
}
if c.Bpjs.SecretKey == "" {
log.Fatal("BPJS Secret Key is required")
}
}*/
// Validate Keycloak configuration if enabled
if c.Keycloak.Enabled {
@@ -696,7 +696,7 @@ func (c *Config) Validate() error {
}
// Validate SatuSehat configuration
if c.SatuSehat.OrgID == "" {
/*if c.SatuSehat.OrgID == "" {
log.Fatal("SatuSehat Organization ID is required")
}
if c.SatuSehat.FasyakesID == "" {
@@ -713,7 +713,7 @@ func (c *Config) Validate() error {
}
if c.SatuSehat.BaseURL == "" {
log.Fatal("SatuSehat Base URL is required")
}
}*/
return nil
}

View File

@@ -0,0 +1,680 @@
package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models"
"api-service/internal/models/qris"
"api-service/internal/utils/validation"
"context"
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
)
var (
qrisdb database.Service
qrisonce sync.Once
qrisvalidate *validator.Validate
)
// Initialize the database connection and validator
func init() {
qrisonce.Do(func() {
qrisdb = database.New(config.LoadConfig())
qrisvalidate = validator.New()
qrisvalidate.RegisterValidation("qris_status", validateQrisStatus)
if qrisdb == nil {
log.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for qris status
func validateQrisStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// QrisHandler handles qris services
type QrisHandler struct {
db database.Service
}
// NewQrisHandler creates a new QrisHandler
func NewQrisHandler() *QrisHandler {
return &QrisHandler{
db: qrisdb,
}
}
// GetQris godoc
// @Summary Get qris with pagination and optional aggregation
// @Description Returns a paginated list of qriss with optional summary statistics
// @Tags Qris
// @Accept json
// @Produce json
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Param include_summary query bool false "Include aggregation summary" default(false)
// @Param status query string false "Filter by status"
// @Param search query string false "Search in multiple fields"
// @Success 200 {object} qris.QrisGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/qriss [get]
func (h *QrisHandler) GetQris(c *gin.Context) {
// Parse pagination parameters
limit, offset, err := h.parsePaginationParams(c)
if err != nil {
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
return
}
// Parse filter parameters
filter := h.parseFilterParams(c)
includeAggregation := c.Query("include_summary") == "true"
// Get database connection
dbConn, err := h.db.GetDB("simrs_backup")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute concurrent operations
var (
items []qris.Qris
total int
aggregateData *models.AggregateData
wg sync.WaitGroup
errChan = make(chan error, 3)
mu sync.Mutex
)
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
mu.Lock()
errChan <- fmt.Errorf("failed to get total count: %w", err)
mu.Unlock()
}
}()
// Fetch main data
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.fetchQriss(ctx, dbConn, filter, limit, offset)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to fetch data: %w", err)
} else {
items = result
}
mu.Unlock()
}()
// Fetch aggregation data if requested
if includeAggregation {
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.getAggregateData(ctx, dbConn, filter)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
} else {
aggregateData = result
}
mu.Unlock()
}()
}
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
return
}
}
// Build response
meta := h.calculateMeta(limit, offset, total)
response := qris.QrisGetResponse{
Message: "Data qris berhasil diambil",
Data: items,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetQrisByID godoc
// @Summary Get Qris by ID
// @Description Returns a single qris by ID
// @Tags Qris
// @Accept json
// @Produce json
// @Param id path string true "Qris ID (UUID)"
// @Success 200 {object} qris.QrisGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Qris not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/qris/{id} [get]
func (h *QrisHandler) GetQrisByID(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
intID, err := strconv.Atoi(id)
if err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("simrs_backup")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.getQrisByID(ctx, dbConn, intID)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Qris not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get qris", err, http.StatusInternalServerError)
}
return
}
response := qris.QrisGetByIDResponse{
Message: "qris details retrieved successfully",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// GetQrisStats godoc
// @Summary Get qris statistics
// @Description Returns comprehensive statistics about qris data
// @Tags Qris
// @Accept json
// @Produce json
// @Param status query string false "Filter statistics by status"
// @Success 200 {object} models.AggregateData "Statistics data"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/qriss/stats [get]
func (h *QrisHandler) GetQrisStats(c *gin.Context) {
dbConn, err := h.db.GetDB("simrs_backup")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
filter := h.parseFilterParams(c)
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
if err != nil {
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Statistik qris berhasil diambil",
"data": aggregateData,
})
}
// Database operations
func (h *QrisHandler) getQrisByID(ctx context.Context, dbConn *sql.DB, id int) (*qris.Qris, error) {
query := "SELECT id, status, created_at, updated_at, display_name FROM t_qrdata WHERE id = $1 AND status IS NOT NULL"
row := dbConn.QueryRowContext(ctx, query, id)
var item qris.Qris
err := row.Scan(&item.ID, &item.Status, &item.CreatedAt, &item.UpdatedAt, &item.DisplayName)
if err != nil {
return nil, err
}
return &item, nil
}
/*func (h *QrisHandler) createQris(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) (*qris.Qris, error) {
id := uuid.New().String()
now := time.Now()
query := "INSERT INTO data_qris_qris (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
var item qris.Qris
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to create qris: %w", err)
}
return &item, nil
}*/
/*func (h *QrisHandler) updateQris(ctx context.Context, dbConn *sql.DB, req *qris.QrisUpdateRequest) (*qris.Qris, error) {
now := time.Now()
query := "UPDATE data_qris_qris SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, sort, user_created, date_created, user_updated, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
var item qris.Qris
err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to update qris: %w", err)
}
return &item, nil
}*/
func (h *QrisHandler) deleteQris(ctx context.Context, dbConn *sql.DB, id string) error {
now := time.Now()
query := "UPDATE data_qris_qris SET status = 'deleted', updated_at = $2 WHERE id = $1 AND status != 'deleted'"
result, err := dbConn.ExecContext(ctx, query, id, now)
if err != nil {
return fmt.Errorf("failed to delete qris: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func (h *QrisHandler) fetchQriss(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, limit, offset int) ([]qris.Qris, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf("SELECT id, status, created_at, updated_at, display_name FROM t_qrdata WHERE %s ORDER BY created_at DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
args = append(args, limit, offset)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch qriss query failed: %w", err)
}
defer rows.Close()
items := make([]qris.Qris, 0, limit)
for rows.Next() {
item, err := h.scanQris(rows)
if err != nil {
return nil, fmt.Errorf("scan Qris failed: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
log.Printf("Successfully fetched %d qriss with filters applied", len(items))
return items, nil
}
// Optimized scanning function
func (h *QrisHandler) scanQris(rows *sql.Rows) (qris.Qris, error) {
var item qris.Qris
// Scan into individual fields to handle nullable types properly
err := rows.Scan(
&item.ID,
&item.Status,
&item.CreatedAt, //.Time, &item.CreatedAt.Valid, // sql.NullTime
&item.UpdatedAt, //.Time, &item.UpdatedAt.Valid, // sql.NullTime
&item.DisplayName, //.String, &item.DisplayName.Valid, // sql.NullString
)
return item, err
}
func (h *QrisHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM t_qrdata WHERE %s", whereClause)
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
return fmt.Errorf("total count query failed: %w", err)
}
return nil
}
// Get comprehensive aggregate data dengan filter support
func (h *QrisHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter qris.QrisFilter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
}
// Build where clause untuk filter
whereClause, args := h.buildWhereClause(filter)
// Use concurrent execution untuk performance
var wg sync.WaitGroup
var mu sync.Mutex
errChan := make(chan error, 4)
// 1. Count by status
wg.Add(1)
go func() {
defer wg.Done()
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM t_qrdata WHERE %s GROUP BY status ORDER BY status", whereClause)
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
if err != nil {
errChan <- fmt.Errorf("status query failed: %w", err)
return
}
defer rows.Close()
mu.Lock()
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
mu.Unlock()
errChan <- fmt.Errorf("status scan failed: %w", err)
return
}
aggregate.ByStatus[status] = count
switch status {
case "active":
aggregate.TotalActive = count
case "draft":
aggregate.TotalDraft = count
case "inactive":
aggregate.TotalInactive = count
}
}
mu.Unlock()
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("status iteration error: %w", err)
}
}()
// 2. Get last updated time dan today statistics
wg.Add(1)
go func() {
defer wg.Done()
// Last updated
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(updated_at) FROM t_qrdata WHERE %s AND updated_at IS NOT NULL", whereClause)
var lastUpdated sql.NullTime
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
errChan <- fmt.Errorf("last updated query failed: %w", err)
return
}
// Today statistics
today := time.Now().Format("2006-01-02")
todayStatsQuery := fmt.Sprintf(`
SELECT
SUM(CASE WHEN DATE(created_at) = $%d THEN 1 ELSE 0 END) as created_today,
SUM(CASE WHEN DATE(updated_at) = $%d AND DATE(created_at) != $%d THEN 1 ELSE 0 END) as updated_today
FROM t_qrdata
WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause)
todayArgs := append(args, today)
var createdToday, updatedToday int
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
errChan <- fmt.Errorf("today stats query failed: %w", err)
return
}
mu.Lock()
if lastUpdated.Valid {
aggregate.LastUpdated = &lastUpdated.Time
}
aggregate.CreatedToday = createdToday
aggregate.UpdatedToday = updatedToday
mu.Unlock()
}()
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
return nil, err
}
}
return aggregate, nil
}
// Enhanced error handling
func (h *QrisHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
log.Printf("[ERROR] %s: %v", message, err)
h.respondError(c, message, err, statusCode)
}
func (h *QrisHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{
Error: errorMessage,
Code: statusCode,
Message: err.Error(),
Timestamp: time.Now(),
})
}
// Parse pagination parameters dengan validation yang lebih ketat
func (h *QrisHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
limit := 10 // Default limit
offset := 0 // Default offset
if limitStr := c.Query("limit"); limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
}
if parsedLimit <= 0 {
return 0, 0, fmt.Errorf("limit must be greater than 0")
}
if parsedLimit > 100 {
return 0, 0, fmt.Errorf("limit cannot exceed 100")
}
limit = parsedLimit
}
if offsetStr := c.Query("offset"); offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
}
if parsedOffset < 0 {
return 0, 0, fmt.Errorf("offset cannot be negative")
}
offset = parsedOffset
}
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
return limit, offset, nil
}
func (h *QrisHandler) parseFilterParams(c *gin.Context) qris.QrisFilter {
filter := qris.QrisFilter{}
if status := c.Query("status"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
// Parse date filters
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
filter.DateFrom = &dateFrom
}
}
if dateToStr := c.Query("date_to"); dateToStr != "" {
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
filter.DateTo = &dateTo
}
}
return filter
}
// Build WHERE clause dengan filter parameters
func (h *QrisHandler) buildWhereClause(filter qris.QrisFilter) (string, []interface{}) {
conditions := []string{"status IS NOT NULL"}
args := []interface{}{}
paramCount := 1
if filter.Status != nil {
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
args = append(args, *filter.Status)
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf("display_name ILIKE $%d", paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
if filter.DateFrom != nil {
conditions = append(conditions, fmt.Sprintf("created_at >= $%d", paramCount))
args = append(args, *filter.DateFrom)
paramCount++
}
if filter.DateTo != nil {
conditions = append(conditions, fmt.Sprintf("created_at <= $%d", paramCount))
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
paramCount++
}
return strings.Join(conditions, " AND "), args
}
func (h *QrisHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages := 0
currentPage := 1
if limit > 0 {
totalPages = (total + limit - 1) / limit // Ceiling division
currentPage = (offset / limit) + 1
}
return models.MetaResponse{
Limit: limit,
Offset: offset,
Total: total,
TotalPages: totalPages,
CurrentPage: currentPage,
HasNext: offset+limit < total,
HasPrev: offset > 0,
}
}
// validateQrisSubmission performs validation for duplicate entries and daily submission limits
/*func (h *QrisHandler) validateQrisSubmission(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) error {
// Import the validation utility
validator := validation.NewDuplicateValidator(dbConn)
// Use default configuration
config := validation.ValidationConfig{
TableName: "data_qris_qris",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
}
// Validate duplicate entries with active status for today
err := validator.ValidateDuplicate(ctx, config, "dummy_id")
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Validate once per day submission
err = validator.ValidateOncePerDay(ctx, "data_qris_qris", "id", "date_created", "daily_limit")
if err != nil {
return fmt.Errorf("daily submission limit exceeded: %w", err)
}
return nil
}*/
// Example usage of the validation utility with custom configuration
/*func (h *QrisHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *qris.QrisCreateRequest) error {
// Create validator instance
validator := validation.NewDuplicateValidator(dbConn)
// Use custom configuration
config := validation.ValidationConfig{
TableName: "data_qris_qris",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
AdditionalFields: map[string]interface{}{
"name": req.Name,
},
}
// Validate with custom fields
fields := map[string]interface{}{
"name": *req.Name,
}
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
if err != nil {
return fmt.Errorf("custom validation failed: %w", err)
}
return nil
}*/
// GetLastSubmissionTime example
func (h *QrisHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
validator := validation.NewDuplicateValidator(dbConn)
return validator.GetLastSubmissionTime(ctx, "t_qrdata", "id", "created_at", identifier)
}

View File

@@ -1,276 +0,0 @@
// Service: VClaim (vclaim)
// Description: BPJS VClaim service for eligibility and SEP management
package peserta
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"api-service/internal/config"
"api-service/internal/models"
"api-service/internal/models/vclaim/peserta"
services "api-service/internal/services/bpjs"
"api-service/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
// VClaimHandler handles VClaim BPJS services
type VClaimHandler struct {
service services.VClaimService
validator *validator.Validate
logger logger.Logger
config config.BpjsConfig
}
// VClaimHandlerConfig contains configuration for VClaimHandler
type VClaimHandlerConfig struct {
BpjsConfig config.BpjsConfig
Logger logger.Logger
Validator *validator.Validate
}
// NewVClaimHandler creates a new VClaimHandler
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
return &VClaimHandler{
service: services.NewService(cfg.BpjsConfig),
validator: cfg.Validator,
logger: cfg.Logger,
config: cfg.BpjsConfig,
}
}
// GetPesertaBynokartu godoc
// @Summary Get PesertaBynokartu data
// @Description Get participant eligibility information by card number
// @Tags Peserta
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nokartu path string true "nokartu" example("example_value")
// @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynokartu data"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - PesertaBynokartu not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /peserta/nokartu/:nokartu/tglSEP/:tglsep [get]
func (h *VClaimHandler) GetPesertaBynokartu(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing GetPesertaBynokartu request", map[string]interface{}{
"request_id": requestID,
"nokartu": c.Param("nokartu"),
"tglsep": c.Param("tglsep"),
})
// Extract path parameters
nokartu := c.Param("nokartu")
tglsep := c.Param("tglsep")
if nokartu == "" {
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nokartu",
RequestID: requestID,
})
return
}
// Call service method
var response peserta.PesertaResponse
endpoint := "/Peserta/nokartu/:nokartu/tglSEP/:tglsep"
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
endpoint = strings.Replace(endpoint, ":tglsep", tglsep, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Map the raw response
response.MetaData = resp.MetaData
if resp.Response != nil {
response.Data = &peserta.PesertaData{}
if respStr, ok := resp.Response.(string); ok {
// Decrypt the response string
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
if err != nil {
h.logger.Error("Failed to decrypt response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if pesertaMap, exists := respMap["peserta"]; exists {
pesertaBytes, _ := json.Marshal(pesertaMap)
json.Unmarshal(pesertaBytes, response.Data)
} else {
// Try to unmarshal the whole response
respBytes, _ := json.Marshal(resp.Response)
json.Unmarshal(respBytes, response.Data)
}
}
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}
// GetPesertaBynik godoc
// @Summary Get PesertaBynik data
// @Description Get participant eligibility information by NIK
// @Tags Peserta
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nik path string true "nik" example("example_value")
// @Success 200 {object} peserta.PesertaResponse "Successfully retrieved PesertaBynik data"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - PesertaBynik not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Peserta/nik/nik/:nik/tglSEP/:tglsep [get]
func (h *VClaimHandler) GetPesertaBynik(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing GetPesertaBynik request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/peserta/nik/:nik/tglSEP/:tglsep",
"nik": c.Param("nik"),
})
// Extract path parameters
nik := c.Param("nik")
tglsep := c.Param("tglsep")
if nik == "" {
h.logger.Error("Missing required parameter nik", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nik",
RequestID: requestID,
})
return
}
if tglsep == "" {
h.logger.Error("Missing required parameter Tanggal SEP", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter Tanggal SEP",
RequestID: requestID,
})
return
}
// Call service method
var response peserta.PesertaResponse
endpoint := "/Peserta/nik/:nik/tglSEP/:tglsep"
endpoint = strings.Replace(endpoint, ":nik", nik, 1)
endpoint = strings.Replace(endpoint, ":tglsep", tglsep, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
h.logger.Error("Failed to get PesertaBynik", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Map the raw response
response.MetaData = resp.MetaData
if resp.Response != nil {
response.Data = &peserta.PesertaData{}
if respStr, ok := resp.Response.(string); ok {
// Decrypt the response string
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
if err != nil {
h.logger.Error("Failed to decrypt response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if pesertaMap, exists := respMap["peserta"]; exists {
pesertaBytes, _ := json.Marshal(pesertaMap)
json.Unmarshal(pesertaBytes, response.Data)
} else {
// Try to unmarshal the whole response
respBytes, _ := json.Marshal(resp.Response)
json.Unmarshal(respBytes, response.Data)
}
}
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}

View File

@@ -1,261 +0,0 @@
// Service: VClaim (vclaim)
// Description: BPJS VClaim service for eligibility and SEP management
package rujukan
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"api-service/internal/config"
"api-service/internal/models"
"api-service/internal/models/vclaim/rujukan"
services "api-service/internal/services/bpjs"
"api-service/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
// VClaimHandler handles VClaim BPJS services
type VClaimHandler struct {
service services.VClaimService
validator *validator.Validate
logger logger.Logger
config config.BpjsConfig
}
// VClaimHandlerConfig contains configuration for VClaimHandler
type VClaimHandlerConfig struct {
BpjsConfig config.BpjsConfig
Logger logger.Logger
Validator *validator.Validate
}
// NewVClaimHandler creates a new VClaimHandler
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
return &VClaimHandler{
service: services.NewService(cfg.BpjsConfig),
validator: cfg.Validator,
logger: cfg.Logger,
config: cfg.BpjsConfig,
}
}
// GetRujukanBynorujukan godoc
// @Summary Get RujukanBynorujukan data
// @Description Manage rujukan
// @Tags Rujukan
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param norujukan path string true "norujukan" example("example_value")
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved RujukanBynorujukan data"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - RujukanBynorujukan not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Rujukan/:norujukan [get]
func (h *VClaimHandler) GetRujukanBynorujukan(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing GetRujukanBynorujukan request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/Rujukan/:norujukan",
"norujukan": c.Param("norujukan"),
})
// Extract path parameters
norujukan := c.Param("norujukan")
if norujukan == "" {
h.logger.Error("Missing required parameter norujukan", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter norujukan",
RequestID: requestID,
})
return
}
// Call service method
var response rujukan.RujukanResponse
endpoint := "/Rujukan/RS/:norujukan"
endpoint = strings.Replace(endpoint, ":norujukan", norujukan, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Map the raw response
response.MetaData = resp.MetaData
if resp.Response != nil {
response.Data = &rujukan.RujukanData{}
if respStr, ok := resp.Response.(string); ok {
// Decrypt the response string
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
if err != nil {
h.logger.Error("Failed to decrypt response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if pesertaMap, exists := respMap["rujukan"]; exists {
pesertaBytes, _ := json.Marshal(pesertaMap)
json.Unmarshal(pesertaBytes, response.Data)
} else {
// Try to unmarshal the whole response
respBytes, _ := json.Marshal(resp.Response)
json.Unmarshal(respBytes, response.Data)
}
}
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}
// GetRujukanBynokartu godoc
// @Summary Get RujukanBynokartu data
// @Description Manage rujukan
// @Tags Rujukan
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nokartu path string true "nokartu" example("example_value")
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved RujukanBynokartu data"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - RujukanBynokartu not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Rujukan/Peserta/:nokartu [get]
func (h *VClaimHandler) GetRujukanBynokartu(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing GetRujukanBynokartu request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/Rujukan/Peserta/:nokartu",
"nokartu": c.Param("nokartu"),
})
// Extract path parameters
nokartu := c.Param("nokartu")
if nokartu == "" {
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nokartu",
RequestID: requestID,
})
return
}
// Call service method
var response rujukan.RujukanResponse
endpoint := "/Rujukan/RS/Peserta/:nokartu"
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
h.logger.Error("Failed to get PesertaBynokartu", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Map the raw response
response.MetaData = resp.MetaData
if resp.Response != nil {
response.Data = &rujukan.RujukanData{}
if respStr, ok := resp.Response.(string); ok {
// Decrypt the response string
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
if err != nil {
h.logger.Error("Failed to decrypt response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
} else {
json.Unmarshal([]byte(decryptedResp), response.Data)
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if pesertaMap, exists := respMap["rujukan"]; exists {
pesertaBytes, _ := json.Marshal(pesertaMap)
json.Unmarshal(pesertaBytes, response.Data)
} else {
// Try to unmarshal the whole response
respBytes, _ := json.Marshal(resp.Response)
json.Unmarshal(respBytes, response.Data)
}
}
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}

View File

@@ -1,416 +0,0 @@
// Service: VClaim (vclaim)
// Description: BPJS VClaim service for eligibility and SEP management
package sep
import (
"context"
"strings"
"net/http"
"time"
"api-service/internal/config"
"api-service/internal/models"
"api-service/internal/models/vclaim/sep"
"api-service/internal/services/bpjs"
"api-service/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
// VClaimHandler handles VClaim BPJS services
type VClaimHandler struct {
service services.VClaimService
validator *validator.Validate
logger logger.Logger
config config.BpjsConfig
}
// VClaimHandlerConfig contains configuration for VClaimHandler
type VClaimHandlerConfig struct {
BpjsConfig config.BpjsConfig
Logger logger.Logger
Validator *validator.Validate
}
// NewVClaimHandler creates a new VClaimHandler
func NewVClaimHandler(cfg VClaimHandlerConfig) *VClaimHandler {
return &VClaimHandler{
service: services.NewService(cfg.BpjsConfig),
validator: cfg.Validator,
logger: cfg.Logger,
config: cfg.BpjsConfig,
}
}
// GetSepSep godoc
// @Summary Get SepSep data
// @Description Manage SEP (Surat Eligibilitas Peserta)
// @Tags Sep
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nosep path string true "nosep" example("example_value")
// @Success 200 {object} sep.SepResponse "Successfully retrieved SepSep data"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /sep/:nosep [get]
func (h *VClaimHandler) GetSepSep(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Generate request ID if not present
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing GetSepSep request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/sep/:nosep",
"nosep": c.Param("nosep"),
})
// Extract path parameters
nosep := c.Param("nosep")
if nosep == "" {
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nosep",
RequestID: requestID,
})
return
}
// Call service method
var response sep.SepResponse
endpoint := "/sep/:nosep"
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
err := h.service.Get(ctx, endpoint, &response)
if err != nil {
h.logger.Error("Failed to get SepSep", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}
// CreateSepSep godoc
// @Summary Create new SepSep
// @Description Manage SEP (Surat Eligibilitas Peserta)
// @Tags Sep
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param request body sep.SepRequest true "SepSep data"
// @Success 201 {object} sep.SepResponse "Successfully created SepSep"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid request body or validation error"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - SepSep already exists"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /sep [post]
func (h *VClaimHandler) CreateSepSep(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing CreateSepSep request", map[string]interface{}{
"request_id": requestID,
})
var req sep.SepRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request
if err := h.validator.Struct(&req); err != nil {
h.logger.Error("Validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// Call service method
var response sep.SepResponse
err := h.service.Post(ctx, "/sep", &req, &response)
if err != nil {
h.logger.Error("Failed to create SepSep", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusCreated, response)
}
// UpdateSepSep godoc
// @Summary Update existing SepSep
// @Description Manage SEP (Surat Eligibilitas Peserta)
// @Tags Sep
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nosep path string true "nosep" example("example_value")
// @Param request body sep.SepRequest true "SepSep data"
// @Success 200 {object} sep.SepResponse "Successfully updated SepSep"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters or request body"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /sep/:nosep [put]
func (h *VClaimHandler) UpdateSepSep(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing UpdateSepSep request", map[string]interface{}{
"request_id": requestID,
})
nosep := c.Param("nosep")
if nosep == "" {
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nosep",
RequestID: requestID,
})
return
}
var req sep.SepRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request body", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Invalid request body: " + err.Error(),
RequestID: requestID,
})
return
}
// Validate request
if err := h.validator.Struct(&req); err != nil {
h.logger.Error("Validation failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Validation failed: " + err.Error(),
RequestID: requestID,
})
return
}
// Call service method
var response sep.SepResponse
endpoint := "/sep/:nosep"
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
err := h.service.Put(ctx, endpoint, &req, &response)
if err != nil {
h.logger.Error("Failed to update SepSep", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}
// DeleteSepSep godoc
// @Summary Delete existing SepSep
// @Description Manage SEP (Surat Eligibilitas Peserta)
// @Tags Sep
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param X-Request-ID header string false "Request ID for tracking"
// @Param nosep path string true "nosep" example("example_value")
// @Success 200 {object} sep.SepResponse "Successfully deleted SepSep"
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - SepSep not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /sep/:nosep [delete]
func (h *VClaimHandler) DeleteSepSep(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
c.Header("X-Request-ID", requestID)
}
h.logger.Info("Processing DeleteSepSep request", map[string]interface{}{
"request_id": requestID,
})
nosep := c.Param("nosep")
if nosep == "" {
h.logger.Error("Missing required parameter nosep", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Missing required parameter nosep",
RequestID: requestID,
})
return
}
// Call service method
var response sep.SepResponse
endpoint := "/sep/:nosep"
endpoint = strings.Replace(endpoint, ":nosep", nosep, 1)
err := h.service.Delete(ctx, endpoint, &response)
if err != nil {
h.logger.Error("Failed to delete SepSep", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Internal server error",
RequestID: requestID,
})
return
}
// Ensure response has proper fields
response.Status = "success"
response.RequestID = requestID
c.JSON(http.StatusOK, response)
}

View File

@@ -1,50 +0,0 @@
package helper
import (
"log"
lzstring "github.com/daku10/go-lz-string"
)
// StringDecrypt - langsung coba decompress tanpa decrypt ulang
func StringDecrypt(key string, encryptedString string) (string, error) {
log.Printf("StringDecrypt: Attempting decompression, data length: %d", len(encryptedString))
// Method 1: Try direct LZ-string decompression (data sudah didecrypt di response.go)
if result, err := lzstring.DecompressFromEncodedURIComponent(encryptedString); err == nil && len(result) > 0 {
log.Printf("StringDecrypt: Direct decompression successful")
return result, nil
}
// Method 2: Try other LZ-string methods
if result, err := lzstring.DecompressFromBase64(encryptedString); err == nil && len(result) > 0 {
log.Printf("StringDecrypt: Base64 decompression successful")
return result, nil
}
// Method 3: If all fail, return the original string
log.Printf("StringDecrypt: All decompression failed, returning original data")
return encryptedString, nil
}
func RemovePKCS7Padding(data []byte) []byte {
if len(data) == 0 {
return data
}
paddingLength := int(data[len(data)-1])
if paddingLength > len(data) || paddingLength == 0 {
log.Printf("RemovePKCS7Padding: Invalid padding length: %d, data length: %d", paddingLength, len(data))
return data // Return original data if padding is invalid
}
// Verify all padding bytes are correct
for i := len(data) - paddingLength; i < len(data); i++ {
if data[i] != byte(paddingLength) {
log.Printf("RemovePKCS7Padding: Invalid padding byte at position %d", i)
return data // Return original data if padding is invalid
}
}
return data[:len(data)-paddingLength]
}

View File

@@ -1,25 +0,0 @@
package helper
import "errors"
func Pad(buf []byte, size int) ([]byte, error) {
bufLen := len(buf)
padLen := size - bufLen%size
padded := make([]byte, bufLen+padLen)
copy(padded, buf)
for i := 0; i < padLen; i++ {
padded[bufLen+i] = byte(padLen)
}
return padded, nil
}
func Unpad(padded []byte, size int) ([]byte, error) {
if len(padded)%size != 0 {
return nil, errors.New("pkcs7: Padded value wasn't in correct size.")
}
bufLen := len(padded) - int(padded[len(padded)-1])
buf := make([]byte, bufLen)
copy(buf, padded[:bufLen])
return buf, nil
}

View File

@@ -1,43 +0,0 @@
package aplicare
import "api-service/internal/models"
// === MONITORING MODELS ===
// MonitoringRequest represents monitoring data request
type MonitoringRequest struct {
models.BaseRequest
TanggalAwal string `json:"tanggal_awal" validate:"required"`
TanggalAkhir string `json:"tanggal_akhir" validate:"required"`
JenisLaporan string `json:"jenis_laporan" validate:"required,oneof=kunjungan klaim rujukan sep"`
PPK string `json:"ppk,omitempty"`
StatusData string `json:"status_data,omitempty"`
models.PaginationRequest
}
// MonitoringData represents monitoring information
type MonitoringData struct {
Tanggal string `json:"tanggal"`
PPK string `json:"ppk"`
NamaPPK string `json:"nama_ppk"`
JumlahKasus int `json:"jumlah_kasus"`
TotalTarif float64 `json:"total_tarif"`
StatusData string `json:"status_data"`
Keterangan string `json:"keterangan,omitempty"`
}
// MonitoringResponse represents monitoring API response
type MonitoringResponse struct {
models.BaseResponse
Data []MonitoringData `json:"data,omitempty"`
Summary *MonitoringSummary `json:"summary,omitempty"`
Pagination *models.PaginationResponse `json:"pagination,omitempty"`
}
// MonitoringSummary represents monitoring summary
type MonitoringSummary struct {
TotalKasus int `json:"total_kasus"`
TotalTarif float64 `json:"total_tarif"`
RataRataTarif float64 `json:"rata_rata_tarif"`
PeriodeLaporan string `json:"periode_laporan"`
}

View File

@@ -1,32 +0,0 @@
package aplicare
import "api-service/internal/models"
// === REFERENSI MODELS ===
// ReferensiRequest represents referensi lookup request
type ReferensiRequest struct {
models.BaseRequest
JenisReferensi string `json:"jenis_referensi" validate:"required,oneof=diagnosa procedure obat alkes faskes dokter poli"`
Keyword string `json:"keyword,omitempty"`
KodeReferensi string `json:"kode_referensi,omitempty"`
models.PaginationRequest
}
// ReferensiData represents referensi information
type ReferensiData struct {
Kode string `json:"kode"`
Nama string `json:"nama"`
Kategori string `json:"kategori,omitempty"`
Status string `json:"status"`
TglBerlaku string `json:"tgl_berlaku,omitempty"`
TglBerakhir string `json:"tgl_berakhir,omitempty"`
Keterangan string `json:"keterangan,omitempty"`
}
// ReferensiResponse represents referensi API response
type ReferensiResponse struct {
models.BaseResponse
Data []ReferensiData `json:"data,omitempty"`
Pagination *models.PaginationResponse `json:"pagination,omitempty"`
}

View File

@@ -1,150 +0,0 @@
package eclaim
import "api-service/internal/models"
// === KLAIM MODELS ===
// KlaimRequest represents klaim submission request
type KlaimRequest struct {
models.BaseRequest
NoSep string `json:"nomor_sep" validate:"required"`
NoKartu string `json:"nomor_kartu" validate:"required"`
NoMR string `json:"nomor_mr" validate:"required"`
TglPulang string `json:"tgl_pulang" validate:"required"`
TglMasuk string `json:"tgl_masuk" validate:"required"`
JnsPelayanan string `json:"jenis_pelayanan" validate:"required,oneof=1 2"`
CaraPulang string `json:"cara_pulang" validate:"required"`
Data KlaimData `json:"data" validate:"required"`
}
// KlaimData represents detailed klaim information
type KlaimData struct {
Diagnosa []DiagnosaKlaim `json:"diagnosa" validate:"required,dive"`
Procedure []ProcedureKlaim `json:"procedure,omitempty"`
Investigasi []InvestigasiKlaim `json:"investigasi,omitempty"`
ObatAlkes []ObatKlaim `json:"obat_alkes,omitempty"`
TarifRS []TarifKlaim `json:"tarif_rs,omitempty"`
DRG *DRGInfo `json:"drg,omitempty"`
SpecialCMG *SpecialCMGInfo `json:"special_cmg,omitempty"`
}
// DiagnosaKlaim represents diagnosis in klaim
type DiagnosaKlaim struct {
KodeDiagnosa string `json:"kode_diagnosa" validate:"required"`
NamaDiagnosa string `json:"nama_diagnosa"`
TipeDiagnosa string `json:"tipe_diagnosa" validate:"required,oneof=1 2"`
}
// ProcedureKlaim represents procedure in klaim
type ProcedureKlaim struct {
KodeTindakan string `json:"kode_tindakan" validate:"required"`
NamaTindakan string `json:"nama_tindakan"`
TanggalTindakan string `json:"tanggal_tindakan" validate:"required"`
Keterangan string `json:"keterangan,omitempty"`
}
// InvestigasiKlaim represents investigation/lab results
type InvestigasiKlaim struct {
KodeInvestigasi string `json:"kode_investigasi" validate:"required"`
NamaInvestigasi string `json:"nama_investigasi"`
Hasil string `json:"hasil,omitempty"`
Satuan string `json:"satuan,omitempty"`
NilaiNormal string `json:"nilai_normal,omitempty"`
}
// ObatKlaim represents medication in klaim
type ObatKlaim struct {
KodeObat string `json:"kode_obat" validate:"required"`
NamaObat string `json:"nama_obat"`
Dosis string `json:"dosis,omitempty"`
Frekuensi string `json:"frekuensi,omitempty"`
Jumlah float64 `json:"jumlah" validate:"min=0"`
Harga float64 `json:"harga" validate:"min=0"`
}
// TarifKlaim represents hospital tariff
type TarifKlaim struct {
KodeTarif string `json:"kode_tarif" validate:"required"`
NamaTarif string `json:"nama_tarif"`
Jumlah int `json:"jumlah" validate:"min=0"`
Tarif float64 `json:"tarif" validate:"min=0"`
Total float64 `json:"total"`
}
// DRGInfo represents DRG information
type DRGInfo struct {
KodeDRG string `json:"kode_drg"`
NamaDRG string `json:"nama_drg"`
TarifDRG float64 `json:"tarif_drg"`
Severity string `json:"severity,omitempty"`
}
// SpecialCMGInfo represents Special CMG information
type SpecialCMGInfo struct {
KodeCMG string `json:"kode_cmg"`
NamaCMG string `json:"nama_cmg"`
TarifCMG float64 `json:"tarif_cmg"`
SubAcute string `json:"sub_acute,omitempty"`
}
// KlaimResponse represents klaim API response
type KlaimResponse struct {
models.BaseResponse
Data *KlaimResponseData `json:"data,omitempty"`
}
// KlaimResponseData represents processed klaim data
type KlaimResponseData struct {
NoKlaim string `json:"nomor_klaim"`
NoSep string `json:"nomor_sep"`
StatusKlaim string `json:"status_klaim"`
TarifAktual float64 `json:"tarif_aktual"`
TarifRS float64 `json:"tarif_rs"`
TarifApproved float64 `json:"tarif_approved"`
Grouper *GrouperResult `json:"grouper,omitempty"`
}
// === GROUPER MODELS ===
// GrouperRequest represents grouper processing request
type GrouperRequest struct {
models.BaseRequest
NoSep string `json:"nomor_sep" validate:"required"`
NoKartu string `json:"nomor_kartu" validate:"required"`
TglMasuk string `json:"tgl_masuk" validate:"required"`
TglPulang string `json:"tgl_pulang" validate:"required"`
JnsPelayanan string `json:"jenis_pelayanan" validate:"required,oneof=1 2"`
CaraPulang string `json:"cara_pulang" validate:"required"`
DiagnosaPrimer string `json:"diagnosa_primer" validate:"required"`
DiagnosaSkunder []string `json:"diagnosa_skunder,omitempty"`
Procedure []string `json:"procedure,omitempty"`
AdlScore int `json:"adl_score,omitempty"`
AgeAtAdmission int `json:"age_at_admission" validate:"min=0"`
}
// GrouperResult represents grouper processing result
type GrouperResult struct {
KodeDRG string `json:"kode_drg"`
NamaDRG string `json:"nama_drg"`
TarifDRG float64 `json:"tarif_drg"`
KodeCMG string `json:"kode_cmg,omitempty"`
NamaCMG string `json:"nama_cmg,omitempty"`
TarifCMG float64 `json:"tarif_cmg,omitempty"`
Severity string `json:"severity"`
SubAcute bool `json:"sub_acute"`
Chronic bool `json:"chronic"`
TopUp *TopUpInfo `json:"top_up,omitempty"`
}
// TopUpInfo represents top-up information
type TopUpInfo struct {
Eligible bool `json:"eligible"`
Percentage float64 `json:"percentage"`
Amount float64 `json:"amount"`
}
// GrouperResponse represents grouper API response
type GrouperResponse struct {
models.BaseResponse
Data *GrouperResult `json:"data,omitempty"`
}

View File

@@ -0,0 +1,87 @@
package qris
import (
"api-service/internal/models"
"database/sql"
"encoding/json"
"time"
)
// Qris represents the data structure for the qris table
// with proper null handling and optimized JSON marshaling
type Qris struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
UpdatedAt sql.NullTime `json:"updated_at,omitempty" db:"updated_at"`
DisplayName sql.NullString `json:"display_name,omitempty" db:"display_name"`
DisplayAmount float64 `json:"display_amount" db:"display_amount"`
QrValue string `json:"qrvalue" db:"qrvalue"`
IP string `json:"ip" db:"ip"`
}
// Custom JSON marshaling untuk Qris agar NULL values tidak muncul di response
func (r Qris) MarshalJSON() ([]byte, error) {
type Alias Qris
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
DisplayName *string `json:"display_name,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.UpdatedAt.Valid {
aux.UpdatedAt = &r.UpdatedAt.Time
}
if r.DisplayName.Valid {
aux.DisplayName = &r.DisplayName.String
}
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *Qris) GetName() string {
if r.DisplayName.Valid {
return r.DisplayName.String
}
return ""
}
// Response struct untuk GET by ID
type QrisGetByIDResponse struct {
Message string `json:"message"`
Data *Qris `json:"data"`
}
// Enhanced GET response dengan pagination dan aggregation
type QrisGetResponse struct {
Message string `json:"message"`
Data []Qris `json:"data"`
Meta models.MetaResponse `json:"meta"`
Summary *models.AggregateData `json:"summary,omitempty"`
}
// Filter struct untuk query parameters
type QrisFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
}

View File

@@ -1,70 +0,0 @@
package peserta
import "api-service/internal/models"
// === PESERTA MODELS ===
// PesertaRequest represents peserta lookup request
type PesertaRequest struct {
models.BaseRequest
NoKartu string `json:"nokartu" validate:"required,min=13,max=13"`
NIK string `json:"nik,omitempty" validate:"omitempty,min=16,max=16"`
TanggalSEP string `json:"tglsep" validate:"required" example:"2024-01-15"`
NoTelepon string `json:"notelp,omitempty" validate:"omitempty,max=15"`
}
// PesertaData represents peserta information from BPJS
type PesertaData struct {
NoKartu string `json:"noKartu"`
NIK string `json:"nik"`
Nama string `json:"nama"`
Pisa string `json:"pisa"`
Sex string `json:"sex"`
TanggalLahir string `json:"tglLahir"`
TglCetakKartu string `json:"tglCetakKartu"`
TglTAT string `json:"tglTAT"`
TglTMT string `json:"tglTMT"`
StatusPeserta struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"statusPeserta"`
ProvUmum struct {
KdProvider string `json:"kdProvider"`
NmProvider string `json:"nmProvider"`
} `json:"provUmum"`
JenisPeserta struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"jenisPeserta"`
HakKelas struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"hakKelas"`
Umur struct {
UmurSekarang string `json:"umurSekarang"`
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
} `json:"umur"`
Informasi struct {
Dinsos interface{} `json:"dinsos"`
ProlanisPRB string `json:"prolanisPRB"`
NoSKTM interface{} `json:"noSKTM"`
ESEP interface{} `json:"eSEP"`
} `json:"informasi"`
Cob struct {
NoAsuransi interface{} `json:"noAsuransi"`
NmAsuransi interface{} `json:"nmAsuransi"`
TglTMT interface{} `json:"tglTMT"`
TglTAT interface{} `json:"tglTAT"`
} `json:"cob"`
MR struct {
NoMR string `json:"noMR"`
NoTelepon string `json:"noTelepon"`
} `json:"mr,omitempty"`
}
// PesertaResponse represents peserta API response
type PesertaResponse struct {
models.BaseResponse
Data *PesertaData `json:"data,omitempty"`
MetaData interface{} `json:"metaData,omitempty"`
}

View File

@@ -1,99 +0,0 @@
package rujukan
import "api-service/internal/models"
// === RUJUKAN MODELS ===
// RujukanRequest represents rujukan lookup request
type RujukanRequest struct {
models.BaseRequest
NoRujukan string `json:"noRujukan" validate:"required"`
NoKartu string `json:"noKartu,omitempty"`
}
// RujukanData represents rujukan information
type RujukanData struct {
Diagnosa DiagnosaData `json:"diagnosa"`
Keluhan string `json:"keluhan"`
NoKunjungan string `json:"noKunjungan"`
Pelayanan PelayananData `json:"pelayanan"`
Peserta PesertaData `json:"peserta"`
PoliRujukan PoliRujukanData `json:"poliRujukan"`
ProvPerujuk ProvPerujukData `json:"provPerujuk"`
TglKunjungan string `json:"tglKunjungan"`
}
type DiagnosaData struct {
Kode string `json:"kode"`
Nama string `json:"nama"`
}
type PelayananData struct {
Kode string `json:"kode"`
Nama string `json:"nama"`
}
type PoliRujukanData struct {
Kode string `json:"kode"`
Nama string `json:"nama"`
}
type ProvPerujukData struct {
Kode string `json:"kode"`
Nama string `json:"nama"`
}
type PesertaData struct {
NoKartu string `json:"noKartu"`
NIK string `json:"nik"`
Nama string `json:"nama"`
Pisa string `json:"pisa"`
Sex string `json:"sex"`
TanggalLahir string `json:"tglLahir"`
TglCetakKartu string `json:"tglCetakKartu"`
TglTAT string `json:"tglTAT"`
TglTMT string `json:"tglTMT"`
StatusPeserta struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"statusPeserta"`
ProvUmum struct {
KdProvider string `json:"kdProvider"`
NmProvider string `json:"nmProvider"`
} `json:"provUmum"`
JenisPeserta struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"jenisPeserta"`
HakKelas struct {
Kode string `json:"kode"`
Keterangan string `json:"keterangan"`
} `json:"hakKelas"`
Umur struct {
UmurSekarang string `json:"umurSekarang"`
UmurSaatPelayanan string `json:"umurSaatPelayanan"`
} `json:"umur"`
Informasi struct {
Dinsos interface{} `json:"dinsos"`
ProlanisPRB interface{} `json:"prolanisPRB"`
NoSKTM interface{} `json:"noSKTM"`
} `json:"informasi"`
Cob struct {
NoAsuransi interface{} `json:"noAsuransi"`
NmAsuransi interface{} `json:"nmAsuransi"`
TglTMT interface{} `json:"tglTMT"`
TglTAT interface{} `json:"tglTAT"`
} `json:"cob"`
MR struct {
NoMR string `json:"noMR"`
NoTelepon interface{} `json:"noTelepon"`
} `json:"mr"`
}
// RujukanResponse represents rujukan API response
type RujukanResponse struct {
models.BaseResponse
Data *RujukanData `json:"data,omitempty"`
List []RujukanData `json:"list,omitempty"`
MetaData interface{} `json:"metaData,omitempty"`
}

View File

@@ -1,59 +0,0 @@
package sep
import (
"api-service/internal/models"
"api-service/internal/models/vclaim/peserta"
)
// === SEP (Surat Eligibilitas Peserta) MODELS ===
// SEPRequest represents SEP creation/update request
type SepRequest struct {
models.BaseRequest
NoKartu string `json:"noKartu" validate:"required"`
TglSep string `json:"tglSep" validate:"required"`
PPKPelayanan string `json:"ppkPelayanan" validate:"required"`
JnsPelayanan string `json:"jnsPelayanan" validate:"required,oneof=1 2"`
KlsRawat string `json:"klsRawat" validate:"required,oneof=1 2 3"`
NoMR string `json:"noMR" validate:"required"`
Rujukan *SepRujukan `json:"rujukan"`
Catatan string `json:"catatan,omitempty"`
Diagnosa string `json:"diagnosa" validate:"required"`
PoliTujuan string `json:"poli" validate:"required"`
ExternalUser string `json:"user" validate:"required"`
NoTelp string `json:"noTelp,omitempty"`
}
// SEPRujukan represents rujukan information in SEP
type SepRujukan struct {
AsalRujukan string `json:"asalRujukan" validate:"required,oneof=1 2"`
TglRujukan string `json:"tglRujukan" validate:"required"`
NoRujukan string `json:"noRujukan" validate:"required"`
PPKRujukan string `json:"ppkRujukan" validate:"required"`
}
// SEPData represents SEP response data
type SepData struct {
NoSep string `json:"noSep"`
TglSep string `json:"tglSep"`
JnsPelayanan string `json:"jnsPelayanan"`
PoliTujuan string `json:"poli"`
KlsRawat string `json:"klsRawat"`
NoMR string `json:"noMR"`
Rujukan SepRujukan `json:"rujukan"`
Catatan string `json:"catatan"`
Diagnosa string `json:"diagnosa"`
Peserta peserta.PesertaData `json:"peserta"`
Informasi struct {
NoSKDP string `json:"noSKDP,omitempty"`
DPJPLayan string `json:"dpjpLayan"`
NoTelepon string `json:"noTelp"`
SubSpesialis string `json:"subSpesialis,omitempty"`
} `json:"informasi"`
}
// SEPResponse represents SEP API response
type SepResponse struct {
models.BaseResponse
Data *SepData `json:"data,omitempty"`
}

View File

@@ -5,10 +5,8 @@ import (
"api-service/internal/database"
authHandlers "api-service/internal/handlers/auth"
healthcheckHandlers "api-service/internal/handlers/healthcheck"
qrisQrisHandlers "api-service/internal/handlers/qris"
retribusiHandlers "api-service/internal/handlers/retribusi"
"api-service/internal/handlers/vclaim/peserta"
"api-service/internal/handlers/vclaim/rujukan"
"api-service/internal/handlers/vclaim/sep"
"api-service/internal/middleware"
services "api-service/internal/services/auth"
"api-service/pkg/logger"
@@ -69,37 +67,14 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
// ============= PUBLISHED ROUTES ===============================================
// Rujukan routes
rujukanHandler := rujukan.NewVClaimHandler(rujukan.VClaimHandlerConfig{
BpjsConfig: cfg.Bpjs,
Logger: *logger.Default(),
Validator: nil,
})
rujukanGroup := v1.Group("/rujukan")
rujukanGroup.GET("/nokartu/:nokartu", rujukanHandler.GetRujukanBynokartu)
rujukanGroup.GET("/norujukan/:norujukan", rujukanHandler.GetRujukanBynorujukan)
// Peserta routes
pesertaHandler := peserta.NewVClaimHandler(peserta.VClaimHandlerConfig{
BpjsConfig: cfg.Bpjs,
Logger: *logger.Default(),
Validator: nil,
})
pesertaGroup := v1.Group("/peserta")
pesertaGroup.GET("/nokartu/:nokartu/tglSEP/:tglsep", pesertaHandler.GetPesertaBynokartu)
pesertaGroup.GET("/nik/:nik/tglSEP/:tglsep", pesertaHandler.GetPesertaBynik)
// Sep routes
sepHandler := sep.NewVClaimHandler(sep.VClaimHandlerConfig{
BpjsConfig: cfg.Bpjs,
Logger: *logger.Default(),
Validator: nil,
})
sepGroup := v1.Group("/sep")
sepGroup.GET("/sep/:nosep", sepHandler.GetSepSep)
sepGroup.POST("/sep", sepHandler.CreateSepSep)
sepGroup.PUT("/sep/:nosep", sepHandler.UpdateSepSep)
sepGroup.DELETE("/sep/:nosep", sepHandler.DeleteSepSep)
// Qris endpoints
qrisQrisHandler := qrisQrisHandlers.NewQrisHandler()
qrisQrisGroup := v1.Group("/qris")
{
qrisQrisGroup.GET("", qrisQrisHandler.GetQris)
qrisQrisGroup.GET("/:id", qrisQrisHandler.GetQrisByID)
qrisQrisGroup.GET("/stats", qrisQrisHandler.GetQrisStats)
}
// // Retribusi endpoints
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()

View File

@@ -1,210 +0,0 @@
package services
import (
helper "api-service/internal/helpers/bpjs"
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"errors"
"io"
"log"
"unicode/utf16"
lzstring "github.com/daku10/go-lz-string"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
// ResponseVclaim decrypts the encrypted response from VClaim API
func ResponseVclaim(encrypted string, key string) (string, error) {
log.Println("ResponseVclaim: Starting decryption process")
log.Printf("ResponseVclaim: Encrypted string length: %d", len(encrypted))
// Pad the base64 string if needed
if len(encrypted)%4 != 0 {
padding := (4 - len(encrypted)%4) % 4
for i := 0; i < padding; i++ {
encrypted += "="
}
}
// Decode base64
cipherText, err := base64.StdEncoding.DecodeString(encrypted)
if err != nil {
log.Printf("ResponseVclaim: Failed to decode base64: %v", err)
return "", err
}
if len(cipherText) < aes.BlockSize {
return "", errors.New("cipherText too short")
}
// Create AES cipher
hash := sha256.Sum256([]byte(key))
block, err := aes.NewCipher(hash[:])
if err != nil {
return "", err
}
// Try both IV methods
// Method 1: IV from hash (current approach)
if result, err := tryDecryptWithHashIV(cipherText, block, hash[:aes.BlockSize]); err == nil {
log.Printf("ResponseVclaim: Success with hash IV method")
return result, nil
}
// Method 2: IV from cipherText (standard approach)
if result, err := tryDecryptWithCipherIV(cipherText, block); err == nil {
log.Printf("ResponseVclaim: Success with cipher IV method")
return result, nil
}
return "", errors.New("all decryption methods failed")
}
func tryDecryptWithHashIV(cipherText []byte, block cipher.Block, iv []byte) (string, error) {
if len(cipherText)%aes.BlockSize != 0 {
return "", errors.New("cipherText is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherText))
mode.CryptBlocks(decrypted, cipherText)
// Remove PKCS7 padding
decrypted = helper.RemovePKCS7Padding(decrypted)
log.Printf("tryDecryptWithHashIV: Decryption completed, length: %d", len(decrypted))
return tryAllDecompressionMethods(decrypted)
}
func tryDecryptWithCipherIV(cipherText []byte, block cipher.Block) (string, error) {
if len(cipherText) < aes.BlockSize {
return "", errors.New("cipherText too short for IV extraction")
}
// Extract IV from first block
iv := cipherText[:aes.BlockSize]
cipherData := cipherText[aes.BlockSize:]
if len(cipherData)%aes.BlockSize != 0 {
return "", errors.New("cipher data is not a multiple of the block size")
}
mode := cipher.NewCBCDecrypter(block, iv)
decrypted := make([]byte, len(cipherData))
mode.CryptBlocks(decrypted, cipherData)
// Remove PKCS7 padding
decrypted = helper.RemovePKCS7Padding(decrypted)
log.Printf("tryDecryptWithCipherIV: Decryption completed, length: %d", len(decrypted))
return tryAllDecompressionMethods(decrypted)
}
func tryAllDecompressionMethods(data []byte) (string, error) {
log.Printf("tryAllDecompressionMethods: Attempting decompression, data length: %d", len(data))
// Method 1: Check if it's already valid JSON
if isValidJSON(data) {
log.Println("tryAllDecompressionMethods: Data is valid JSON, returning as-is")
return string(data), nil
}
// Method 2: Try gzip decompression
if result, err := tryGzipDecompression(data); err == nil && len(result) > 0 {
log.Println("tryAllDecompressionMethods: Gzip decompression successful")
return result, nil
}
// Method 3: Try LZ-string decompression methods
if result, err := tryLZStringMethods(data); err == nil && len(result) > 0 {
log.Println("tryAllDecompressionMethods: LZ-string decompression successful")
return result, nil
}
// Method 4: Return as plain text
result := string(data)
if len(result) > 0 {
log.Printf("tryAllDecompressionMethods: Using decrypted data as plain text, length: %d", len(result))
return result, nil
}
return "", errors.New("all decompression methods failed")
}
func isValidJSON(data []byte) bool {
if len(data) == 0 {
return false
}
firstChar := data[0]
return firstChar == '{' || firstChar == '['
}
func tryGzipDecompression(data []byte) (string, error) {
reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil {
return "", err
}
defer reader.Close()
decompressed, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return string(decompressed), nil
}
func tryLZStringMethods(data []byte) (string, error) {
dataStr := string(data)
// Method 1: DecompressFromEncodedURIComponent
if result, err := lzstring.DecompressFromEncodedURIComponent(dataStr); err == nil && len(result) > 0 {
return result, nil
}
// Method 2: DecompressFromBase64
if result, err := lzstring.DecompressFromBase64(dataStr); err == nil && len(result) > 0 {
return result, nil
}
// Method 3: DecompressFromUTF16 (with proper conversion)
if utf16Data, err := stringToUTF16(dataStr); err == nil {
if result, err := lzstring.DecompressFromUTF16(utf16Data); err == nil && len(result) > 0 {
return result, nil
}
}
// Method 4: Try with base64 decoding first
if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil {
if result, err := lzstring.DecompressFromEncodedURIComponent(string(decoded)); err == nil && len(result) > 0 {
return result, nil
}
}
return "", errors.New("all LZ-string methods failed")
}
// stringToUTF16 converts string to []uint16 for UTF16 decompression
func stringToUTF16(s string) ([]uint16, error) {
if len(s) == 0 {
return nil, errors.New("empty string")
}
// Convert string to runes first
runes := []rune(s)
// Convert runes to UTF16
utf16Data := utf16.Encode(runes)
return utf16Data, nil
}

View File

@@ -1,458 +0,0 @@
package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"api-service/internal/config"
"api-service/internal/models/vclaim/peserta"
"github.com/mashingan/smapping"
"github.com/rs/zerolog/log"
)
// VClaimService interface for VClaim operations
type VClaimService interface {
Get(ctx context.Context, endpoint string, result interface{}) error
Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
Patch(ctx context.Context, endpoint string, payload interface{}, result interface{}) error
Delete(ctx context.Context, endpoint string, result interface{}) error
GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error)
PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error)
DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error)
}
// Service struct for VClaim service
type Service struct {
config config.BpjsConfig
httpClient *http.Client
}
// Response structures
type ResponMentahDTOVclaim struct {
MetaData struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"metaData"`
Response string `json:"response"`
}
type ResponDTOVclaim struct {
MetaData struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"metaData"`
Response interface{} `json:"response"`
}
// NewService creates a new VClaim service instance
func NewService(cfg config.BpjsConfig) VClaimService {
log.Info().
Str("base_url", cfg.BaseURL).
Dur("timeout", cfg.Timeout).
Msg("Creating new VClaim service instance")
service := &Service{
config: cfg,
httpClient: &http.Client{
Timeout: cfg.Timeout,
},
}
return service
}
// NewServiceFromConfig creates service from main config
func NewServiceFromConfig(cfg *config.Config) VClaimService {
return NewService(cfg.Bpjs)
}
// NewServiceFromInterface creates service from interface (for backward compatibility)
func NewServiceFromInterface(cfg interface{}) (VClaimService, error) {
var bpjsConfig config.BpjsConfig
// Try to map from interface
err := smapping.FillStruct(&bpjsConfig, smapping.MapFields(&cfg))
if err != nil {
return nil, fmt.Errorf("failed to map config: %w", err)
}
if bpjsConfig.Timeout == 0 {
bpjsConfig.Timeout = 30 * time.Second
}
return NewService(bpjsConfig), nil
}
// SetHTTPClient allows custom http client configuration
func (s *Service) SetHTTPClient(client *http.Client) {
s.httpClient = client
}
// prepareRequest prepares HTTP request with required headers
func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
fullURL := s.config.BaseURL + endpoint
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)
}
// Set headers using the SetHeader method
consID, _, userKey, tstamp, xSignature := s.config.SetHeader()
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-cons-id", consID)
req.Header.Set("X-timestamp", tstamp)
req.Header.Set("X-signature", xSignature)
req.Header.Set("user_key", userKey)
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
}
// processResponse processes response from VClaim API
func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, 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)
}
if res.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body))
}
// Parse raw response
var respMentah ResponMentahDTOVclaim
if err := json.Unmarshal(body, &respMentah); err != nil {
return nil, fmt.Errorf("failed to unmarshal raw response: %w", err)
}
// Create final response
finalResp := &ResponDTOVclaim{
MetaData: respMentah.MetaData,
}
// Check if response needs decryption
if respMentah.Response == "" {
return finalResp, nil
}
// Try to parse as JSON first (unencrypted response)
var tempResp interface{}
if json.Unmarshal([]byte(respMentah.Response), &tempResp) == nil {
finalResp.Response = tempResp
return finalResp, nil
}
// Decrypt response
consID, secretKey, _, tstamp, _ := s.config.SetHeader()
decryptionKey := consID + secretKey + tstamp
log.Debug().
Str("consID", consID).
Str("tstamp", tstamp).
Int("key_length", len(decryptionKey)).
Msg("Decryption key components")
respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey)
if err != nil {
log.Error().Err(err).Msg("Failed to decrypt response")
return nil, fmt.Errorf("failed to decrypt response: %w", err)
}
// Try to unmarshal decrypted response as JSON
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
}
}
return finalResp, nil
}
// Get performs HTTP GET request
func (s *Service) Get(ctx context.Context, endpoint string, result interface{}) error {
resp, err := s.GetRawResponse(ctx, endpoint)
if err != nil {
return err
}
return mapToResult(resp, result)
}
// Post performs HTTP POST request
func (s *Service) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
resp, err := s.PostRawResponse(ctx, endpoint, payload)
if err != nil {
return err
}
return mapToResult(resp, result)
}
// Put performs HTTP PUT request
func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error {
var buf bytes.Buffer
if payload != nil {
if err := json.NewEncoder(&buf).Encode(payload); err != nil {
return fmt.Errorf("failed to encode payload: %w", err)
}
}
req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf)
if err != nil {
return err
}
res, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute PUT request: %w", err)
}
resp, err := s.processResponse(res)
if err != nil {
return err
}
return mapToResult(resp, result)
}
// Delete performs HTTP DELETE request
func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error {
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return err
}
res, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute DELETE request: %w", err)
}
resp, err := s.processResponse(res)
if err != nil {
return err
}
return mapToResult(resp, result)
}
// Patch performs HTTP PATCH request
func (s *Service) Patch(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.MethodPatch, endpoint, &buf)
if err != nil {
return err
}
res, err := s.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to execute PATCH request: %w", err)
}
resp, err := s.processResponse(res)
if err != nil {
return err
}
return mapToResult(resp, result)
}
// GetRawResponse returns raw response without mapping
func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
res, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute GET request: %w", err)
}
return s.processResponse(res)
}
// PostRawResponse returns raw response without mapping
func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, 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)
}
// PatchRawResponse returns raw response without mapping
func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, 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.MethodPatch, endpoint, &buf)
if err != nil {
return nil, err
}
res, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute PATCH request: %w", err)
}
return s.processResponse(res)
}
// PutRawResponse returns raw response without mapping
func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, 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.MethodPut, endpoint, &buf)
if err != nil {
return nil, err
}
res, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute PUT request: %w", err)
}
return s.processResponse(res)
}
// DeleteRawResponse returns raw response without mapping
func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil)
if err != nil {
return nil, err
}
res, err := s.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute DELETE request: %w", err)
}
return s.processResponse(res)
}
// mapToResult maps the final response to the result interface
func mapToResult(resp *ResponDTOVclaim, 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)
}
// Handle BPJS peserta response structure
if pesertaResp, ok := result.(*peserta.PesertaResponse); ok {
if resp.Response != nil {
if responseMap, ok := resp.Response.(map[string]interface{}); ok {
if pesertaMap, ok := responseMap["peserta"]; ok {
pesertaBytes, _ := json.Marshal(pesertaMap)
var pd peserta.PesertaData
json.Unmarshal(pesertaBytes, &pd)
pesertaResp.Data = &pd
}
}
}
}
return nil
}
// Backward compatibility functions
func GetRequest(endpoint string, cfg interface{}) interface{} {
service, err := NewServiceFromInterface(cfg)
if err != nil {
fmt.Printf("Failed to create service: %v\n", err)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.GetRawResponse(ctx, endpoint)
if err != nil {
fmt.Printf("Failed to get response: %v\n", err)
return nil
}
return resp
}
func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
service, err := NewServiceFromInterface(cfg)
if err != nil {
fmt.Printf("Failed to create service: %v\n", err)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := service.PostRawResponse(ctx, endpoint, data)
if err != nil {
fmt.Printf("Failed to post response: %v\n", err)
return nil
}
return resp
}

View File

@@ -1,676 +0,0 @@
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
}