penambahan All case Satu sehat
This commit is contained in:
@@ -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)
|
||||
);
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user