Files
service-vclaim/internal/handlers/peserta/peserta.go
2025-09-24 20:47:09 +07:00

606 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package peserta handles Peserta BPJS services
// Generated on: 2025-09-07 11:01:18
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"sync"
"time"
"api-service/internal/config"
"api-service/internal/database"
"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"
)
// PesertaHandler handles Peserta BPJS services
type PesertaHandler struct {
service services.VClaimService
db database.Service
validator *validator.Validate
logger logger.Logger
config config.BpjsConfig
}
// PesertaHandlerConfig contains configuration for PesertaHandler
type PesertaHandlerConfig struct {
Config *config.Config
Logger logger.Logger
Validator *validator.Validate
}
// NewPesertaHandler creates a new PesertaHandler
func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler {
return &PesertaHandler{
db: database.New(cfg.Config),
service: services.NewService(cfg.Config.Bpjs),
validator: cfg.Validator,
logger: cfg.Logger,
config: cfg.Config.Bpjs,
}
}
// min returns the minimum of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}
// cleanResponse removes invalid characters and BOM from the response string
func cleanResponse(resp string) string {
// Remove UTF-8 BOM
// Konversi string ke byte slice untuk pengecekan BOM
data := []byte(resp)
// Cek dan hapus semua jenis representasi UTF-8 BOM
// 1. Byte sequence: EF BB BF
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 2. Unicode character: U+FEFF
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 3. Zero Width No-Break Space (Unicode)
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// 4. Representasi heksadesimal lainnya
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
data = data[3:]
}
// Konversi kembali ke string
resp = string(data)
// Hapus karakter null
// Hapus semua karakter kontrol ASCII (0-31) kecuali whitespace yang valid
controlChars := []string{
"\x00", // Null character
"\x01", // Start of Heading
"\x02", // Start of Text
"\x03", // End of Text
"\x04", // End of Transmission (EOT)
"\x05", // Enquiry
"\x06", // Acknowledge
"\x07", // Bell
"\x08", // Backspace
"\x0B", // Vertical Tab
"\x0C", // Form Feed
"\x0E", // Shift Out
"\x0F", // Shift In
"\x10", // Data Link Escape
"\x11", // Device Control 1
"\x12", // Device Control 2
"\x13", // Device Control 3
"\x14", // Device Control 4
"\x15", // Negative Acknowledge
"\x16", // Synchronous Idle
"\x17", // End of Transmission Block
"\x18", // Cancel
"\x19", // End of Medium
"\x1A", // Substitute
"\x1B", // Escape
"\x1C", // File Separator
"\x1D", // Group Separator
"\x1E", // Record Separator
"\x1F", // Unit Separator
}
for _, char := range controlChars {
resp = strings.ReplaceAll(resp, char, "")
}
// Hapus karakter invalid termasuk backtick
invalidChars := []string{
"¢", // Cent sign
"\u00a2", // Cent sign Unicode
"\u0080", // Control character
"`", // Backtick
"´", // Acute accent
"", // Left single quote
"", // Right single quote
"“", // Left double quote
"”", // Right double quote
}
for _, char := range invalidChars {
resp = strings.ReplaceAll(resp, char, "")
}
// Gunakan buffer pool untuk efisiensi memori
var bufPool = sync.Pool{
New: func() interface{} {
return &strings.Builder{}
},
}
buf := bufPool.Get().(*strings.Builder)
defer func() {
buf.Reset()
bufPool.Put(buf)
}()
// Definisikan karakter yang diperbolehkan
allowedChars := map[rune]bool{
'\n': true, '\r': true, '\t': true,
// Tambahkan karakter non-ASCII yang diperbolehkan jika adafalse
// Contoh:
// Latin-1 Supplement
// ASCII printable (32-126) kecuali backtick (96)
'!': true, '"': true, '#': true, '$': true, '%': true, '&': true,
'\'': true, '(': true, ')': true, '*': true, '+': true, ',': true,
'-': true, '.': true, '/': true, '0': true, '1': true, '2': true,
'3': true, '4': true, '5': true, '6': true, '7': true, '8': true,
'9': true, ':': true, ';': true, '<': true, '=': true, '>': true,
'?': true, '@': true, 'A': true, 'B': true, 'C': true, 'D': true,
'E': true, 'F': true, 'G': true, 'H': true, 'I': true, 'J': true,
'K': true, 'L': true, 'M': true, 'N': true, 'O': true, 'P': true,
'Q': true, 'R': true, 'S': true, 'T': true, 'U': true, 'V': true,
'W': true, 'X': true, 'Y': true, 'Z': true, '[': true, '\\': true,
']': true, '^': true, '_': true, 'a': true, 'b': true, 'c': true,
'd': true, 'e': true, 'f': true, 'g': true, 'h': true, 'i': true,
'j': true, 'k': true, 'l': true, 'm': true, 'n': true, 'o': true,
'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true,
'v': true, 'w': true, 'x': true, 'y': true, 'z': true, '{': true,
'|': true, '}': true, '~': true,
// Latin-1 Supplement
'¡': true, '¢': true, '£': true, '¤': true, '¥': true, '¦': true,
'§': true, '¨': true, '©': true, 'ª': true, '«': true, '¬': true,
'®': true, '¯': true, '°': true, '±': true, '²': true, '³': true,
'´': true, 'µ': true, '¶': true, '·': true, '¸': true, '¹': true,
'º': true, '»': true, '¼': true, '½': true, '¾': true, '¿': true,
// Huruf Latin dengan diakritik (Lowercase)
'á': true, 'é': true, 'í': true, 'ó': true, 'ú': true, 'ý': true, 'þ': true,
'à': true, 'è': true, 'ì': true, 'ò': true, 'ù': true,
'â': true, 'ê': true, 'î': true, 'ô': true, 'û': true,
'ä': true, 'ë': true, 'ï': true, 'ö': true, 'ü': true, 'ÿ': true,
'ã': true, 'õ': true, 'ñ': true, 'ç': true,
'ā': true, 'ē': true, 'ī': true, 'ō': true, 'ū': true,
'ă': true, 'đ': true, 'ħ': true, 'ij': true, 'ĸ': true, 'ł': true,
'ŋ': true, 'œ': true, 'ŧ': true, 'ß': true,
// Huruf Latin dengan diakritik (Uppercase)
'Á': true, 'É': true, 'Í': true, 'Ó': true, 'Ú': true, 'Ý': true, 'Þ': true,
'À': true, 'È': true, 'Ì': true, 'Ò': true, 'Ù': true,
'Â': true, 'Ê': true, 'Î': true, 'Ô': true, 'Û': true,
'Ä': true, 'Ë': true, 'Ï': true, 'Ö': true, 'Ü': true,
'Ã': true, 'Õ': true, 'Ñ': true, 'Ç': true,
'Ā': true, 'Ē': true, 'Ī': true, 'Ō': true, 'Ū': true,
'Ă': true, 'Đ': true, 'Ħ': true, 'IJ': true, 'Ł': true,
'Ŋ': true, 'Œ': true, 'Ŧ': true, 'ẞ': true,
// Karakter Nordik dan lainnya
'Å': true, 'å': true, 'Æ': true, 'æ': true, 'Ø': true, 'ø': true,
'ſ': true, 'ʼn': true, 'ŀ': true,
// Tanda baca dan simbol matematika
'': true, '': true, '—': true, '―': true, '‖': true, '‗': true,
'†': true, '‡': true, '•': true, '‣': true, '': true, '‥': true,
'…': true, '‧': true, '‰': true, '': true, '″': true, '‴': true,
'': true, '‶': true, '‷': true, '‸': true, '': true, '': true,
'※': true,
// Simbol mata uang (hanya yang umum)
'€': true, '₹': true,
// Karakter lain yang mungkin diperlukan
}
// Filter karakter menggunakan buffer pool
for _, r := range resp {
if r < 128 || allowedChars[r] {
buf.WriteRune(r)
}
}
// Trim whitespace
result := strings.TrimSpace(buf.String())
return result
}
// extractCode extracts the code field from metaData using reflection
func extractCode(metaData interface{}) interface{} {
v := reflect.ValueOf(metaData)
switch v.Kind() {
case reflect.Struct:
codeField := v.FieldByName("Code")
if codeField.IsValid() {
return codeField.Interface()
}
case reflect.Map:
if m, ok := metaData.(map[string]interface{}); ok {
return m["code"]
}
case reflect.String:
var metaMap map[string]interface{}
if err := json.Unmarshal([]byte(metaData.(string)), &metaMap); err == nil {
return metaMap["code"]
}
}
return nil
}
// parseHTTPStatusCode extracts HTTP status code from error message
func parseHTTPStatusCode(errMsg string) int {
if strings.Contains(errMsg, "HTTP error:") {
parts := strings.Split(errMsg, "HTTP error:")
if len(parts) > 1 {
statusPart := strings.TrimSpace(parts[1])
if statusCode, err := strconv.Atoi(strings.Fields(statusPart)[0]); err == nil {
return statusCode
}
}
}
return 500 // Default to internal server error
}
func (h *PesertaHandler) isValidJSON(str string) bool {
var js interface{}
return json.Unmarshal([]byte(str), &js) == nil
}
// GetBynik godoc
// @Summary Get Bynik 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 Bynik 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 - Bynik not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Peserta/nik/:nik [get]
func (h *PesertaHandler) GetBynik(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)
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logger.Error("Database connection failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Database connection failed",
RequestID: requestID,
})
return
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
// Context Paramaeter
now := time.Now()
dateStr := now.Format("2006-01-02")
fmt.Println("Date (YYYY-MM-DD):", dateStr)
h.logger.Info("Processing GetBynik request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/Peserta/nik/:nik/tglSEP/" + dateStr,
"nik": c.Param("nik"),
})
// Extract path parameters
nik := c.Param("nik")
if nik == "" || nik == ":nik" {
h.logger.Error("Missing required parameter nik", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Parameter NIK Masih Kosong / Isi Dahulu NIK!",
RequestID: requestID,
})
return
}
var response peserta.PesertaResponse
endpoint := "/Peserta/nik/:nik/tglSEP/" + dateStr
endpoint = strings.Replace(endpoint, ":nik", nik, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
// Check if error message contains 404 status code
if strings.Contains(err.Error(), "HTTP error: 404") {
h.logger.Error("Bynik not found", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
Status: "error",
Message: "Bynik not found",
RequestID: requestID,
})
return
}
h.logger.Error("Failed to get Bynik", 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 {
// Clean the decrypted response
cleanedResp := cleanResponse(decryptedResp)
if h.isValidJSON(cleanedResp) {
// Unmarshal kembali setelah dibersihkan
err = json.Unmarshal([]byte(cleanedResp), response.Data)
if err != nil {
h.logger.Warn("Failed to unmarshal decrypted response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
"response_preview": cleanedResp[:min(100, len(cleanedResp))], // Log first 100 chars for debugging
})
// Set Data to nil if unmarshal fails to avoid sending empty struct
response.Data = nil
}
} else {
h.logger.Warn("Invalid JSON in data, storing as string", map[string]interface{}{
"request_id": requestID,
"response": cleanedResp,
})
response.Data.RawResponse = cleanedResp
}
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if dataMap, exists := respMap["peserta"]; exists {
dataBytes, _ := json.Marshal(dataMap)
json.Unmarshal(dataBytes, 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
// Ambil status code dari metaData.code
var statusCode int
code := extractCode(response.MetaData)
if code != nil {
statusCode = models.GetStatusCodeFromMeta(code)
} else {
statusCode = 200
}
c.JSON(statusCode, response)
}
// GetBynokartu godoc
// @Summary Get Bynokartu 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 Bynokartu 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 - Bynokartu not found"
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
// @Router /Peserta/nokartu/:nokartu [get]
func (h *PesertaHandler) GetBynokartu(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)
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
if err != nil {
h.logger.Error("Database connection failed", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
Status: "error",
Message: "Database connection failed",
RequestID: requestID,
})
return
}
// Note: dbConn is available for future database operations (e.g., caching, logging)
_ = dbConn // Prevent unused variable warning
// Context Paramaeter
now := time.Now()
dateStr := now.Format("2006-01-02")
fmt.Println("Date (YYYY-MM-DD):", dateStr)
h.logger.Info("Processing GetBynokartu request", map[string]interface{}{
"request_id": requestID,
"endpoint": "/Peserta/nokartu/:nokartu/tglSEP/" + dateStr,
"nik": c.Param("nokartu"),
})
// Extract path parameters
nokartu := c.Param("nokartu")
if nokartu == "" || nokartu == ":nokartu" {
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
"request_id": requestID,
})
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
Status: "error",
Message: "Parameter Nomor Kartu Bpjs Masih Kosong / Isi Dahulu Nomor Kartu!",
RequestID: requestID,
})
return
}
var response peserta.PesertaResponse
endpoint := "/Peserta/nokartu/:nokartu/tglSEP/" + dateStr
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
resp, err := h.service.GetRawResponse(ctx, endpoint)
if err != nil {
// Check if error message contains 404 status code
if strings.Contains(err.Error(), "HTTP error: 404") {
h.logger.Error("ByNoKartu not found", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
})
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
Status: "error",
Message: "ByNoKartu not found",
RequestID: requestID,
})
return
}
h.logger.Error("Failed to get ByNoKartu", 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 {
// Clean the decrypted response
cleanedResp := cleanResponse(decryptedResp)
err = json.Unmarshal([]byte(cleanedResp), response.Data)
if err != nil {
h.logger.Warn("Failed to unmarshal decrypted response", map[string]interface{}{
"error": err.Error(),
"request_id": requestID,
"response_preview": cleanedResp[:min(100, len(cleanedResp))], // Log first 100 chars for debugging
})
// Set Data to nil if unmarshal fails to avoid sending empty struct
response.Data = nil
}
}
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
// Response is already unmarshaled JSON
if dataMap, exists := respMap["peserta"]; exists {
dataBytes, _ := json.Marshal(dataMap)
json.Unmarshal(dataBytes, 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
// Ambil status code dari metaData.code
var statusCode int
code := extractCode(response.MetaData)
if code != nil {
statusCode = models.GetStatusCodeFromMeta(code)
} else {
statusCode = 200
}
c.JSON(statusCode, response)
}