diff --git a/internal/handlers/peserta/peserta.go b/internal/handlers/peserta/peserta.go index 14c17016..8ba77e89 100644 --- a/internal/handlers/peserta/peserta.go +++ b/internal/handlers/peserta/peserta.go @@ -5,8 +5,12 @@ package handlers import ( "context" "encoding/json" + "fmt" "net/http" + "reflect" + "strconv" "strings" + "sync" "time" "api-service/internal/config" @@ -48,6 +52,224 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler { } } +// 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 @@ -64,6 +286,9 @@ func NewPesertaHandler(cfg PesertaHandlerConfig) *PesertaHandler { // @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 == "" { @@ -87,20 +312,21 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) { } // Note: dbConn is available for future database operations (e.g., caching, logging) _ = dbConn // Prevent unused variable warning - ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) - defer cancel() + // 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", - - "nik": c.Param("nik"), + "endpoint": "/Peserta/nik/:nik/tglSEP/" + dateStr, + "nik": c.Param("nik"), }) // Extract path parameters nik := c.Param("nik") - if nik == "" { + if nik == "" || nik == ":nik" { h.logger.Error("Missing required parameter nik", map[string]interface{}{ "request_id": requestID, @@ -108,16 +334,14 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", - Message: "Missing required parameter nik", + Message: "Parameter NIK Masih Kosong / Isi Dahulu NIK!", RequestID: requestID, }) return } - - // Call service method var response peserta.PesertaResponse - endpoint := "/peserta/nik/:nik" + endpoint := "/Peserta/nik/:nik/tglSEP/" + dateStr endpoint = strings.Replace(endpoint, ":nik", nik, 1) @@ -168,7 +392,28 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) { }) } else { - json.Unmarshal([]byte(decryptedResp), response.Data) + // 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 @@ -186,7 +431,15 @@ func (h *PesertaHandler) GetBynik(c *gin.Context) { // Ensure response has proper fields response.Status = "success" response.RequestID = requestID - c.JSON(http.StatusOK, response) + // 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 @@ -215,17 +468,37 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) { 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": c.Param("nokartu"), + "endpoint": "/Peserta/nokartu/:nokartu/tglSEP/" + dateStr, + "nik": c.Param("nokartu"), }) // Extract path parameters nokartu := c.Param("nokartu") - if nokartu == "" { + if nokartu == "" || nokartu == ":nokartu" { h.logger.Error("Missing required parameter nokartu", map[string]interface{}{ "request_id": requestID, @@ -233,24 +506,36 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{ Status: "error", - Message: "Missing required parameter nokartu", + Message: "Parameter Nomor Kartu Bpjs Masih Kosong / Isi Dahulu Nomor Kartu!", RequestID: requestID, }) return } - - // Call service method var response peserta.PesertaResponse - endpoint := "/peserta/:nokartu" + 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, + }) - h.logger.Error("Failed to get Bynokartu", map[string]interface{}{ + 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, }) @@ -279,7 +564,18 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) { }) } else { - json.Unmarshal([]byte(decryptedResp), response.Data) + // 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 @@ -297,30 +593,13 @@ func (h *PesertaHandler) GetBynokartu(c *gin.Context) { // Ensure response has proper fields response.Status = "success" response.RequestID = requestID - c.JSON(http.StatusOK, response) -} - -// Enhanced error handling -func (h *PesertaHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { - requestID := c.GetHeader("X-Request-ID") - h.logger.Error(message, map[string]interface{}{ - "error": err.Error(), - "status_code": statusCode, - "request_id": requestID, - }) - h.respondError(c, message, err, statusCode) -} - -func (h *PesertaHandler) respondError(c *gin.Context, message string, err error, statusCode int) { - requestID := c.GetHeader("X-Request-ID") - errorMessage := message - if gin.Mode() == gin.ReleaseMode { - errorMessage = "Internal server error" + // 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, models.ErrorResponseBpjs{ - Status: "error", - Message: errorMessage, - RequestID: requestID, - }) + c.JSON(statusCode, response) } diff --git a/internal/models/models.go b/internal/models/models.go index dea08e4e..2643ef8e 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -3,6 +3,8 @@ package models import ( "database/sql" "database/sql/driver" + "net/http" + "strconv" "time" ) @@ -161,6 +163,42 @@ type MetaInfo struct { ServerTime string `json:"server_time"` } +func GetStatusCodeFromMeta(metaCode interface{}) int { + statusCode := http.StatusOK + + if metaCode != nil { + switch v := metaCode.(type) { + case string: + if code, err := strconv.Atoi(v); err == nil { + if code >= 100 && code <= 599 { + statusCode = code + } else { + statusCode = http.StatusInternalServerError + } + } else { + statusCode = http.StatusInternalServerError + } + case int: + if v >= 100 && v <= 599 { + statusCode = v + } else { + statusCode = http.StatusInternalServerError + } + case float64: + code := int(v) + if code >= 100 && code <= 599 { + statusCode = code + } else { + statusCode = http.StatusInternalServerError + } + default: + statusCode = http.StatusInternalServerError + } + } + + return statusCode +} + // Validation constants const ( StatusDraft = "draft" diff --git a/internal/models/vclaim/peserta/peserta.go b/internal/models/vclaim/peserta/peserta.go index 9fde2767..8c871fb5 100644 --- a/internal/models/vclaim/peserta/peserta.go +++ b/internal/models/vclaim/peserta/peserta.go @@ -60,6 +60,7 @@ type PesertaData struct { NoMR string `json:"noMR"` NoTelepon string `json:"noTelepon"` } `json:"mr,omitempty"` + RawResponse string `json:"raw_response,omitempty"` } // PesertaResponse represents peserta API response diff --git a/internal/services/bpjs/vclaimBridge.go b/internal/services/bpjs/vclaimBridge.go index 9dfd7645..a3ad8a19 100644 --- a/internal/services/bpjs/vclaimBridge.go +++ b/internal/services/bpjs/vclaimBridge.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" "api-service/internal/config" @@ -16,6 +17,18 @@ import ( "github.com/rs/zerolog/log" ) +// cleanResponse removes invalid characters and BOM from the response string +func cleanResponse(resp string) string { + // Remove UTF-8 BOM + resp = strings.TrimPrefix(resp, "\xef\xbb\xbf") + resp = strings.TrimPrefix(resp, "\ufeff") + // Remove null characters + resp = strings.ReplaceAll(resp, "\x00", "") + // Trim whitespace + resp = strings.TrimSpace(resp) + return resp +} + // VClaimService interface for VClaim operations type VClaimService interface { Get(ctx context.Context, endpoint string, result interface{}) error @@ -172,6 +185,12 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) return finalResp, nil } + // Check if response looks like HTML or error message (don't try to decrypt) + if strings.HasPrefix(respMentah.Response, "<") || strings.Contains(respMentah.Response, "error") { + finalResp.Response = respMentah.Response + return finalResp, nil + } + // Decrypt response consID, secretKey, _, tstamp, _ := s.config.SetHeader() decryptionKey := consID + secretKey + tstamp @@ -190,6 +209,8 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTOVclaim, error) // Try to unmarshal decrypted response as JSON if respDecrypt != "" { + // Clean the decrypted response + respDecrypt = cleanResponse(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")