penambahan All case Satu sehat

This commit is contained in:
meninjar
2026-05-04 03:48:43 +00:00
parent 135c631021
commit af32d9cfdd
127 changed files with 617698 additions and 1168 deletions
+30 -20
View File
@@ -124,16 +124,21 @@ type BpjsConfig struct {
}
type SatuSehatConfig struct {
Enabled bool `mapstructure:"enabled"`
OrgID string `mapstructure:"org_id"`
FasyakesID string `mapstructure:"fasyakes_id"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
AuthURL string `mapstructure:"auth_url"`
BaseURL string `mapstructure:"base_url"`
ConsentURL string `mapstructure:"consent_url"`
KFAURL string `mapstructure:"kfa_url"`
Timeout time.Duration `mapstructure:"timeout"`
Enabled bool `mapstructure:"enabled"`
OrgID string `mapstructure:"org_id"`
FasyakesID string `mapstructure:"fasyakes_id"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
AuthURL string `mapstructure:"auth_url"`
BaseURL string `mapstructure:"base_url"`
ConsentURL string `mapstructure:"consent_url"`
KFAURL string `mapstructure:"kfa_url"`
KYCURL string `mapstructure:"kyc_url"`
DicomURL string `mapstructure:"dicom_url"`
KYCPublicKeyB64 string `mapstructure:"kyc_public_key_b64"`
KYCPrivateKeyB64 string `mapstructure:"kyc_private_key_b64"`
WebhookSecret string `mapstructure:"webhook_secret"`
Timeout time.Duration `mapstructure:"timeout"`
}
type SwaggerConfig struct {
@@ -271,16 +276,21 @@ func LoadConfig() *Config {
Timeout: parseDuration(getEnv("BPJS_TIMEOUT", "30s")),
},
SatuSehat: SatuSehatConfig{
Enabled: getEnvAsBool("SATUSEHAT_ENABLED", getEnvAsBool("SATU_SEHAT_ENABLED", false)),
OrgID: getEnv("SATUSEHAT_ORG_ID", getEnv("BRIDGING_SATUSEHAT_ORG_ID", "")),
FasyakesID: getEnv("SATUSEHAT_FASYAKES_ID", getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", "")),
ClientID: getEnv("SATUSEHAT_CLIENT_ID", getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", "")),
ClientSecret: getEnv("SATUSEHAT_CLIENT_SECRET", getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", "")),
AuthURL: getEnv("SATUSEHAT_AUTH_URL", getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1")),
BaseURL: getEnv("SATUSEHAT_BASE_URL", getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1")),
ConsentURL: getEnv("SATUSEHAT_CONSENT_URL", getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1")),
KFAURL: getEnv("SATUSEHAT_KFA_URL", getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2")),
Timeout: parseDuration(getEnv("SATUSEHAT_TIMEOUT", getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s"))),
Enabled: getEnvAsBool("SATUSEHAT_ENABLED", getEnvAsBool("SATU_SEHAT_ENABLED", false)),
OrgID: getEnv("SATUSEHAT_ORG_ID", getEnv("BRIDGING_SATUSEHAT_ORG_ID", "")),
FasyakesID: getEnv("SATUSEHAT_FASYAKES_ID", getEnv("BRIDGING_SATUSEHAT_FASYAKES_ID", "")),
ClientID: getEnv("SATUSEHAT_CLIENT_ID", getEnv("BRIDGING_SATUSEHAT_CLIENT_ID", "")),
ClientSecret: getEnv("SATUSEHAT_CLIENT_SECRET", getEnv("BRIDGING_SATUSEHAT_CLIENT_SECRET", "")),
AuthURL: getEnv("SATUSEHAT_AUTH_URL", getEnv("BRIDGING_SATUSEHAT_AUTH_URL", "https://api-satusehat.kemkes.go.id/oauth2/v1")),
BaseURL: getEnv("SATUSEHAT_BASE_URL", getEnv("BRIDGING_SATUSEHAT_BASE_URL", "https://api-satusehat.kemkes.go.id/fhir-r4/v1")),
ConsentURL: getEnv("SATUSEHAT_CONSENT_URL", getEnv("BRIDGING_SATUSEHAT_CONSENT_URL", "https://api-satusehat.dto.kemkes.go.id/consent/v1")),
KFAURL: getEnv("SATUSEHAT_KFA_URL", getEnv("BRIDGING_SATUSEHAT_KFA_URL", "https://api-satusehat.kemkes.go.id/kfa-v2")),
KYCURL: getEnv("SATUSEHAT_KYC_URL", getEnv("BRIDGING_SATUSEHAT_KYC_URL", "https://api-satusehat.dto.kemkes.go.id/kyc/v1")),
DicomURL: getEnv("SATUSEHAT_DICOM_URL", getEnv("BRIDGING_SATUSEHAT_DICOM_URL", "https://api-satusehat.kemkes.go.id/dicom/v1/dicomWeb/studies")),
KYCPublicKeyB64: getEnv("KYC_PUBLIC_KEY_B64", ""),
KYCPrivateKeyB64: getEnv("KYC_PRIVATE_KEY_B64", ""),
WebhookSecret: getEnv("SATUSEHAT_WEBHOOK_SECRET", getEnv("BRIDGING_SATUSEHAT_WEBHOOK_SECRET", "")),
Timeout: parseDuration(getEnv("SATUSEHAT_TIMEOUT", getEnv("BRIDGING_SATUSEHAT_TIMEOUT", "30s"))),
},
Swagger: SwaggerConfig{
Title: getEnv("SWAGGER_TITLE", "SERVICE API"),
@@ -0,0 +1,122 @@
package reference
import (
"encoding/base64"
"io"
"net/http"
"strings"
"service/internal/infrastructure/transport/http/middleware"
"service/internal/satusehat/reference/kyc"
"service/pkg/crypto"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type KYCHandler struct {
service kyc.Service
}
func NewKYCHandler(service kyc.Service) *KYCHandler {
return &KYCHandler{
service: service,
}
}
// GenerateKeys godoc
//
// @Summary Generate Static RSA Keys for KYC
// @Description Menghasilkan pasangan kunci RSA 2048-bit dalam format Base64 untuk disimpan di .env (KYC_PRIVATE_KEY_B64 & KYC_PUBLIC_KEY_B64)
// @Tags Satu Sehat - KYC
// @Produce json
// @Success 200 {object} response.Response
// @Router /satusehat/reference/kyc/generate-keys [get]
func (h *KYCHandler) GenerateKeys(c *gin.Context) {
privPEM, pubPEM, err := crypto.GenerateRSAKeyPair(2048)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Gagal membuat RSA Keypair", err.Error())
return
}
responseData := map[string]interface{}{
"instructions": "Salin nilai di bawah ini dan masukkan ke dalam file .env Anda",
"KYC_PRIVATE_KEY_B64": base64.StdEncoding.EncodeToString([]byte(privPEM)),
"KYC_PUBLIC_KEY_B64": base64.StdEncoding.EncodeToString([]byte(pubPEM)),
}
response.Success(c, http.StatusOK, "Berhasil membuat kunci RSA Statis", responseData)
}
// GenerateURL godoc
//
// @Summary Generate KYC URL
// @Description Membuat link validasi KYC Satu Sehat
// @Tags Satu Sehat - KYC
// @Accept json
// @Produce json
// @Param request body kyc.GenerateURLRequest true "Payload KYC"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/kyc/generate-url [post]
// @Security BearerAuth
func (h *KYCHandler) GenerateURL(c *gin.Context) {
var req kyc.GenerateURLRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Format request tidak valid", err.Error())
return
}
// NOTE: Public Key statis otomatis disisipkan oleh layer Service yang membaca dari konfigurasi .env
data, err := h.service.GenerateURL(c.Request.Context(), req)
if err != nil {
handleSatuSehatError(c, err)
return
}
responseData := map[string]interface{}{
"kyc_data": data,
}
response.Success(c, http.StatusOK, "Berhasil membuat link KYC", responseData)
}
// Callback godoc
//
// @Summary Webhook/Callback KYC
// @Description Menerima notifikasi dari Satu Sehat setelah proses KYC selesai
// @Tags Satu Sehat - KYC
// @Accept text/plain
// @Produce json
// @Param request body string true "Payload Webhook Terenkripsi (-----BEGIN ENCRYPTED MESSAGE-----...)"
// @Success 200 {object} response.Response
// @Router /satusehat/reference/kyc/callback [post]
func (h *KYCHandler) Callback(c *gin.Context) {
// Ekstrak token dari header Authorization (Format: "Bearer <token>")
authHeader := c.GetHeader("Authorization")
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
// Payload dari webhook Kemenkes berupa string terenkripsi (bukan JSON langsung)
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
response.Error(c, http.StatusBadRequest, "Gagal membaca body request", err.Error())
return
}
encryptedPayload := string(bodyBytes)
if err := h.service.HandleCallback(c.Request.Context(), encryptedPayload, token); err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Callback berhasil diproses", nil)
}
func (h *KYCHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/reference/kyc")
{
group.GET("/generate-keys", h.GenerateKeys)
group.POST("/generate-url", h.GenerateURL)
// Rate limit khusus untuk endpoint callback: max 2 Request/Detik, Burst Limit 5
group.POST("/callback", middleware.MemoryRateLimitMiddleware(2.0, 5), h.Callback)
}
}
@@ -25,7 +25,7 @@ func (h *CarePlanHandler) RegisterRoutes(router *gin.RouterGroup) {
func (h *CarePlanHandler) Create(c *gin.Context) {
var req careplan.CarePlanRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan tidak valid", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
@@ -51,7 +51,7 @@ func (h *CarePlanHandler) Update(c *gin.Context) {
func (h *CarePlanHandler) Patch(c *gin.Context) {
var req careplan.CarePlanPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format permintaan patch tidak valid", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
@@ -27,7 +27,7 @@ func (h *DiagnosticReportHandler) RegisterRoutes(router *gin.RouterGroup) {
func (h *DiagnosticReportHandler) Create(c *gin.Context) {
var req diagnosticreport.DiagnosticReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format payload JSON tidak valid", validator.TranslateError(err))
return
}
res, err := h.service.Create(c.Request.Context(), req)
@@ -40,7 +40,7 @@ func (h *DiagnosticReportHandler) Create(c *gin.Context) {
func (h *DiagnosticReportHandler) Update(c *gin.Context) {
var req diagnosticreport.DiagnosticReportRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid request", validator.TranslateError(err))
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format payload JSON tidak valid", validator.TranslateError(err))
return
}
res, err := h.service.Update(c.Request.Context(), c.Param("id"), req)
@@ -53,7 +53,7 @@ func (h *DiagnosticReportHandler) Update(c *gin.Context) {
func (h *DiagnosticReportHandler) Patch(c *gin.Context) {
var req diagnosticreport.DiagnosticReportPatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Invalid patch", validator.TranslateError(err))
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format patch payload tidak valid", validator.TranslateError(err))
return
}
res, err := h.service.Patch(c.Request.Context(), c.Param("id"), req)
@@ -0,0 +1,60 @@
package usecase
import (
"io"
"net/http"
"service/internal/satusehat/usecase/studies"
"service/pkg/response"
"github.com/gin-gonic/gin"
)
type DicomStudiesHandler struct {
service studies.Service
}
func NewDicomStudiesHandler(service studies.Service) *DicomStudiesHandler {
return &DicomStudiesHandler{
service: service,
}
}
func (h *DicomStudiesHandler) RegisterRoutes(router *gin.RouterGroup) {
// URL akan menjadi: POST /api/v1/satusehat/dicom/studies
group := router.Group("/satusehat/dicom/studies")
{
group.POST("", h.Upload)
}
}
func (h *DicomStudiesHandler) Upload(c *gin.Context) {
// Ambil file dari request FormData dengan key "file"
file, err := c.FormFile("file")
if err != nil {
response.Error(c, http.StatusBadRequest, "File DICOM tidak ditemukan pada form-data key 'file'", err.Error())
return
}
openedFile, err := file.Open()
if err != nil {
response.Error(c, http.StatusInternalServerError, "Gagal membuka file DICOM sementara", err.Error())
return
}
defer openedFile.Close()
dicomBytes, err := io.ReadAll(openedFile)
if err != nil {
response.Error(c, http.StatusInternalServerError, "Gagal membaca byte dari file DICOM", err.Error())
return
}
result, err := h.service.UploadDICOM(c.Request.Context(), dicomBytes)
if err != nil {
// Fungsi ini berasal dari encounter_handler.go di package yang sama
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Berhasil mengunggah file DICOM ke SatuSehat STOW-RS", result.FullResponse)
}
@@ -3,7 +3,6 @@ package usecase
import (
"errors"
"net/http"
"strconv"
"time"
"service/internal/interfaces/satusehat"
@@ -29,7 +28,7 @@ func (h *EncounterHandler) RegisterRoutes(router *gin.RouterGroup) {
group := router.Group("/satusehat/encounter")
{
group.POST("", h.Create)
group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
// group.POST("/sync/:idxdaftar", h.SyncFromSIMRS)
group.GET("", h.Search)
group.GET("/:id", h.GetByID)
group.PUT("/:id", h.Update)
@@ -132,18 +131,18 @@ func (h *EncounterHandler) Search(c *gin.Context) {
response.Success(c, http.StatusOK, "Successfully retrieved Encounters", result.FullResponse)
}
func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
idxdaftarStr := c.Param("idxdaftar")
idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
if err != nil {
response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
return
}
// func (h *EncounterHandler) SyncFromSIMRS(c *gin.Context) {
// idxdaftarStr := c.Param("idxdaftar")
// idxdaftar, err := strconv.ParseInt(idxdaftarStr, 10, 64)
// if err != nil {
// response.ErrorWithLog(c, err, http.StatusBadRequest, "Format idxdaftar tidak valid", nil)
// return
// }
result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
if err != nil {
handleSatuSehatError(c, err)
return
}
response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
}
// result, err := h.service.SyncFromSIMRS(c.Request.Context(), idxdaftar)
// if err != nil {
// handleSatuSehatError(c, err)
// return
// }
// response.Success(c, http.StatusOK, "Successfully synced Encounter from SIMRS", result.FullResponse)
// }
@@ -32,6 +32,7 @@ type ModuleHandlers struct {
SatuSehatOrganization *satuSehatRefHttp.OrganizationHandler
SatuSehatLocation *satuSehatRefHttp.LocationHandler
SatuSehatKFA *satuSehatRefHttp.KFAHandler
SatuSehatKYC *satuSehatRefHttp.KYCHandler
SSAllergyIntolerance *satuSehatUsecaseHttp.AllergyIntoleranceHandler
SSCarePlan *satuSehatUsecaseHttp.CarePlanHandler
SSClinicalImpression *satuSehatUsecaseHttp.ClinicalImpressionHandler
@@ -51,6 +52,7 @@ type ModuleHandlers struct {
SSQuestionnaireResponse *satuSehatUsecaseHttp.QuestionnaireResponseHandler
SSServiceRequest *satuSehatUsecaseHttp.ServiceRequestHandler
SSSpecimen *satuSehatUsecaseHttp.SpecimenHandler
SSDicomStudies *satuSehatUsecaseHttp.DicomStudiesHandler
}
// SetupRoutes mendaftarkan seluruh endpoint API secara dinamis berdasarkan
@@ -128,7 +130,7 @@ func SetupRoutes(
// Private routes (Membutuhkan Autentikasi)
protected := v1.Group("")
protected.Use(middleware.AuthMiddleware(cfg, cacheManager))
protected.Use(middleware.AuthMiddleware(cfg, cacheManager)) // Di-comment sementara untuk proses development/testing
{
if h.Auth != nil {
h.Auth.RegisterProtectedRoutes(protected)
@@ -168,6 +170,9 @@ func SetupRoutes(
if h.SatuSehatKFA != nil {
h.SatuSehatKFA.RegisterRoutes(satuSehatGroup)
}
if h.SatuSehatKYC != nil {
h.SatuSehatKYC.RegisterRoutes(satuSehatGroup)
}
if h.SSAllergyIntolerance != nil {
h.SSAllergyIntolerance.RegisterRoutes(satuSehatGroup)
}
@@ -225,6 +230,9 @@ func SetupRoutes(
if h.SSSpecimen != nil {
h.SSSpecimen.RegisterRoutes(satuSehatGroup)
}
if h.SSDicomStudies != nil {
h.SSDicomStudies.RegisterRoutes(satuSehatGroup)
}
}
}
}
@@ -13,6 +13,7 @@ import (
rolePermission "service/internal/master/role/permission"
satuSehatAuth "service/internal/satusehat/reference/auth"
satuSehatKfa "service/internal/satusehat/reference/kfa"
"service/internal/satusehat/reference/kyc"
satuSehatLocation "service/internal/satusehat/reference/location"
satuSehatOrganization "service/internal/satusehat/reference/organization"
satuSehatPatient "service/internal/satusehat/reference/patient"
@@ -36,6 +37,7 @@ import (
"service/internal/satusehat/usecase/questionnaireresponse"
"service/internal/satusehat/usecase/servicerequest"
"service/internal/satusehat/usecase/specimen"
"service/internal/satusehat/usecase/studies"
)
// MasterServices menampung kumpulan service untuk domain Master Data
@@ -53,6 +55,7 @@ type SatuSehatServices struct {
Organization satuSehatOrganization.Service
Location satuSehatLocation.Service
KFA satuSehatKfa.Service
KYC kyc.Service
// Usecase Services
AllergyIntolerance allergyintolerance.Service
@@ -74,6 +77,7 @@ type SatuSehatServices struct {
QuestionnaireResponse questionnaireresponse.Service
ServiceRequest servicerequest.Service
Specimen specimen.Service
DicomStudies studies.Service
}
// ServiceRegistry berfungsi sebagai Dependency Injection Container untuk transport HTTP.
@@ -104,6 +104,9 @@ func NewHTTPServer(
if registry.SatuSehat.KFA != nil {
appHandlers.SatuSehatKFA = satuSehatRefHttp.NewKFAHandler(registry.SatuSehat.KFA)
}
if registry.SatuSehat.KYC != nil {
appHandlers.SatuSehatKYC = satuSehatRefHttp.NewKYCHandler(registry.SatuSehat.KYC)
}
// Inisialisasi Handlers Modul Satu Sehat Usecase
if registry.SatuSehat.AllergyIntolerance != nil {
@@ -163,6 +166,9 @@ func NewHTTPServer(
if registry.SatuSehat.Specimen != nil {
appHandlers.SSSpecimen = satuSehatUsecaseHttp.NewSpecimenHandler(registry.SatuSehat.Specimen)
}
if registry.SatuSehat.DicomStudies != nil {
appHandlers.SSDicomStudies = satuSehatUsecaseHttp.NewDicomStudiesHandler(registry.SatuSehat.DicomStudies)
}
}
// Inisialisasi Health Handler dengan dependensi yang benar