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

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
}