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
+83
View File
@@ -0,0 +1,83 @@
-- Tabel Utama Produk KFA
CREATE TABLE kfa_products (
kfa_code VARCHAR(50) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
display_name VARCHAR(255),
nama_dagang VARCHAR(255),
active BOOLEAN DEFAULT true,
state VARCHAR(50),
generik BOOLEAN,
-- Klasifikasi & Atribut Dasar (Disimpan sebagai JSONB untuk efisiensi)
dosage_form JSONB, -- { "code": "BS066", "name": "Tablet" }
farmalkes_type JSONB, -- { "code": "medicine", "group": "farmasi", "name": "Obat" }
uom JSONB, -- { "name": "Tablet" }
rute_pemberian JSONB, -- { "code": "O", "name": "Oral" }
controlled_drug JSONB, -- { "code": "3", "name": "Obat Keras" }
-- Harga & Metrik Fisik
fix_price NUMERIC(12, 2),
het_price NUMERIC(12, 2),
dose_per_unit NUMERIC(10, 2),
net_weight NUMERIC(10, 2),
net_weight_uom_name VARCHAR(20),
volume NUMERIC(10, 2),
volume_uom_name VARCHAR(20),
score_tkdn NUMERIC(5, 2),
rxterm INT,
-- Data Legal & Manufaktur
manufacturer VARCHAR(255),
registrar VARCHAR(255),
nie VARCHAR(100),
farmalkes_hscode VARCHAR(100),
kode_lkpp VARCHAR(100),
tayang_lkpp BOOLEAN,
-- Teks Panjang (HTML String)
description TEXT,
indication TEXT,
side_effect TEXT,
warning TEXT,
-- Kompleks/Nested JSONB (Data yang jarang di-join tapi sering dibaca)
atc_info JSONB, -- Menggabungkan atc_l1 sampai atc_l5 & atc_ddd
fornas JSONB, -- Struktur dokumen fornas
dosage_usage JSONB, -- Array of dosage usage objects
tags JSONB, -- Array of tags [{code, name}]
identifier_ids JSONB, -- Array of identifiers [{code, name, source_name}]
product_template JSONB, -- Data template referensi KFA
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP
);
-- Indeks untuk pencarian teks yang cepat
CREATE INDEX idx_kfa_products_name ON kfa_products(name);
CREATE INDEX idx_kfa_products_kode_lkpp ON kfa_products(kode_lkpp);
-- Indeks GIN untuk pencarian dalam JSONB (misal mencari produk berdasarkan tags)
-- CREATE INDEX idx_kfa_products_tags ON kfa_products USING GIN (tags); -- Catatan: Buka komentar ini HANYA jika Anda menggunakan PostgreSQL
-- Tabel Relasi: Zat Aktif (Penting untuk dibuat tabel terpisah agar bisa query "Obat dengan kandungan X")
CREATE TABLE kfa_active_ingredients (
id SERIAL PRIMARY KEY,
product_kfa_code VARCHAR(50) REFERENCES kfa_products(kfa_code) ON DELETE CASCADE,
kfa_code VARCHAR(50),
zat_aktif VARCHAR(255) NOT NULL,
kekuatan_zat_aktif VARCHAR(100),
active BOOLEAN DEFAULT true,
state VARCHAR(50),
updated_at TIMESTAMP
);
CREATE INDEX idx_ingredient_zat_aktif ON kfa_active_ingredients(zat_aktif);
-- Tabel Relasi: Varian Kemasan
CREATE TABLE kfa_packaging (
id SERIAL PRIMARY KEY,
product_kfa_code VARCHAR(50) REFERENCES kfa_products(kfa_code) ON DELETE CASCADE,
kfa_code VARCHAR(50),
name VARCHAR(255),
pack_price NUMERIC(12, 2),
qty INT,
uom_id VARCHAR(50)
);
+15
View File
@@ -0,0 +1,15 @@
package kyc
// GenerateURLRequest merepresentasikan payload untuk membuat link validasi KYC
type GenerateURLRequest struct {
AgentName string `json:"agent_name" binding:"required"`
AgentNIK string `json:"agent_nik" binding:"required,len=16"`
PublicKey string `json:"public_key"` // Diisi otomatis oleh backend
}
// CallbackRequest merepresentasikan payload webhook/callback dari Satu Sehat setelah KYC selesai.
type CallbackRequest struct {
Status string `json:"status"`
Message string `json:"message"`
Data map[string]interface{} `json:"data"`
}
@@ -0,0 +1,73 @@
package kyc
import (
"context"
"encoding/json"
"service/internal/interfaces/satusehat"
"service/pkg/crypto"
"service/pkg/errors"
)
type Repository interface {
// Menerima privKeyPEM untuk keperluan decrypt hasil response
GenerateURL(ctx context.Context, payload GenerateURLRequest, privKeyPEM string) (*satusehat.FHIRResponse, error)
}
type repository struct {
client satusehat.SatuSehatClient
}
func NewRepository(client satusehat.SatuSehatClient) Repository {
return &repository{client: client}
}
// hiddenCtx membungkus context untuk mencegah *HTTP client*
// melakukan type-assertion ke *gin.Context dan melakukan auto-print error.
type hiddenCtx struct {
context.Context
}
func (r *repository) GenerateURL(ctx context.Context, payload GenerateURLRequest, privKeyPEM string) (*satusehat.FHIRResponse, error) {
// 1. Marshal JSON asli
jsonPayload, _ := json.Marshal(payload)
// 2. Enkripsi JSON Payload menggunakan fungsi AES/RSA yang baru dibuat
encryptedStringPayload, err := crypto.EncryptSatuSehatPayload(jsonPayload)
if err != nil {
return nil, errors.InternalError().Message("Gagal mengenkripsi payload").Cause(err).Build()
}
// Endpoint ini mengarah ke /generate-url dengan method POST
// NOTE: Pastikan r.client.DoKYC mendukung pengiriman string dan otomatis
// memberikan HTTP Header Content-Type: text/plain
respData, err := r.client.DoKYC(hiddenCtx{ctx}, "POST", "/generate-url", encryptedStringPayload)
if err != nil {
return nil, errors.InternalError().
Message("Failed to generate KYC URL to SatuSehat").
Cause(err).
Build()
}
// 3. Dekripsi response dari Kemenkes menggunakan Private Key kita
decryptedData, decErr := crypto.DecryptSatuSehatPayload(string(respData), privKeyPEM)
if decErr != nil {
// Terkadang jika validasi NIK gagal, Kemenkes membalas JSON plaintext berisi {"metadata": {"code":"400"...}}
// Jika gagal decrypt, kita asumsikan Kemenkes mengirim plaintext error, kita parse langsung.
decryptedData = respData
}
var result map[string]interface{}
if err := json.Unmarshal(decryptedData, &result); err != nil {
return nil, errors.InternalError().
Message("Failed to parse KYC response").
Cause(err).
Metadata("raw_response", string(decryptedData)).
Build()
}
return &satusehat.FHIRResponse{
FullResponse: result,
RawResponse: respData,
}, nil
}
@@ -0,0 +1,80 @@
package kyc
import (
"context"
"encoding/base64"
"encoding/json"
"service/internal/infrastructure/config"
"service/internal/interfaces/satusehat"
"service/pkg/crypto"
"service/pkg/errors"
"service/pkg/logger"
)
type Service interface {
GenerateURL(ctx context.Context, req GenerateURLRequest) (*satusehat.FHIRResponse, error)
HandleCallback(ctx context.Context, encryptedPayload string, token string) error
}
type service struct {
repo Repository
cfg config.SatuSehatConfig
}
func NewService(repo Repository, cfg config.SatuSehatConfig) Service {
return &service{repo: repo, cfg: cfg}
}
func (s *service) GenerateURL(ctx context.Context, req GenerateURLRequest) (*satusehat.FHIRResponse, error) {
// Gunakan public key statis dari konfigurasi (disimpan dalam format Base64 di .env)
pubKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPublicKeyB64)
if err != nil {
return nil, errors.InternalError().Message("Gagal mendecode Public Key dari konfigurasi").Cause(err).Build()
}
req.PublicKey = string(pubKeyBytes)
// Decode private key untuk mendekripsi balasan generate URL dari Kemenkes
privKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPrivateKeyB64)
if err != nil {
return nil, errors.InternalError().Message("Gagal mendecode Private Key dari konfigurasi").Cause(err).Build()
}
privKeyPEM := string(privKeyBytes)
// Lempar privKeyPEM ke repo agar Kemenkes response dapat didekripsi
return s.repo.GenerateURL(ctx, req, privKeyPEM)
}
func (s *service) HandleCallback(ctx context.Context, encryptedPayload string, token string) error {
// Validasi Security Token dari Kemenkes
expectedToken := s.cfg.WebhookSecret
if expectedToken == "" {
logger.Default().Warn("SATUSEHAT_WEBHOOK_SECRET belum dikonfigurasi. Validasi token webhook dilewati.")
} else if token != expectedToken {
logger.Default().Warn("Webhook KYC gagal divalidasi: Token tidak cocok", logger.String("received_token", token))
return errors.UnauthorizedError().Message("Invalid security webhook token").Build()
}
// 1. Dapatkan Private Key statis Anda dari config/env var
privKeyBytes, err := base64.StdEncoding.DecodeString(s.cfg.KYCPrivateKeyB64)
if err != nil {
return errors.InternalError().Message("Gagal mendecode Private Key dari konfigurasi").Cause(err).Build()
}
privKeyPEM := string(privKeyBytes)
// 2. Dekripsi payload yang masuk
decryptedBytes, err := crypto.DecryptSatuSehatPayload(encryptedPayload, privKeyPEM)
if err != nil {
return errors.InternalError().Message("Gagal mendekripsi payload webhook dari SatuSehat").Cause(err).Build()
}
// 3. Unmarshal ke bentuk Struct
var payload CallbackRequest
if err := json.Unmarshal(decryptedBytes, &payload); err != nil {
return errors.InternalError().Message("Gagal memparsing JSON hasil dekripsi").Cause(err).Build()
}
// TODO: Tambahkan logika update ke database lokal (misal: tabel user/agent) berdasarkan hasil KYC.
// Saat ini, kita log payload yang diterima dari Kemenkes.
logger.Default().Info("Menerima Webhook KYC dari SatuSehat", logger.Any("payload", payload))
return nil
}