feat: require hospital code and add company APIs
This commit is contained in:
11
.env.example
11
.env.example
@@ -1,16 +1,7 @@
|
|||||||
APP_HOST=0.0.0.0
|
APP_HOST=0.0.0.0
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
# Database khusus master CPONE middleware.
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=primaya
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_DEFAULT_RS_CODE=AWALBROS_DEV
|
|
||||||
DB_DEFAULT_RS_NAME=RS Dev Awalbros
|
|
||||||
|
|
||||||
# Database khusus master CPONE middleware. Jika host/user/password dikosongkan,
|
|
||||||
# gunakan nilai DB_* di atas.
|
|
||||||
CPONE_DB_HOST=127.0.0.1
|
CPONE_DB_HOST=127.0.0.1
|
||||||
CPONE_DB_PORT=3306
|
CPONE_DB_PORT=3306
|
||||||
CPONE_DB_DATABASE=cpone_middleware
|
CPONE_DB_DATABASE=cpone_middleware
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -7,6 +7,16 @@ GET /api/cpone/paket
|
|||||||
GET /api/cpone/paket/{id}
|
GET /api/cpone/paket/{id}
|
||||||
Authorization: Bearer <your-secure-token>
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
|
GET /api/cpone/layanan?page=1
|
||||||
|
Authorization: Bearer <your-secure-token>
|
||||||
|
X-RS-Code: <kode-rs>
|
||||||
|
|
||||||
|
GET /api/cpone/perusahaan?search=&page=1
|
||||||
|
GET /api/cpone/perusahaan/{id}
|
||||||
|
GET /api/cpone/perusahaan/{id}/group-tarif?group_tarif_id=
|
||||||
|
Authorization: Bearer <your-secure-token>
|
||||||
|
X-RS-Code: <kode-rs>
|
||||||
|
|
||||||
GET /api/cpone/tarif-kerjasama?search=&perusahaan_id=&tarif_id=&page=1&per_page=15
|
GET /api/cpone/tarif-kerjasama?search=&perusahaan_id=&tarif_id=&page=1&per_page=15
|
||||||
Authorization: Bearer <your-secure-token>
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
@@ -134,14 +144,6 @@ Copy `.env.example` menjadi `.env`, lalu isi koneksi MySQL:
|
|||||||
```text
|
```text
|
||||||
APP_HOST=0.0.0.0
|
APP_HOST=0.0.0.0
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=primaya
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_DEFAULT_RS_CODE=AWALBROS_DEV
|
|
||||||
DB_DEFAULT_RS_NAME=RS Dev Awalbros
|
|
||||||
|
|
||||||
CPONE_DB_HOST=127.0.0.1
|
CPONE_DB_HOST=127.0.0.1
|
||||||
CPONE_DB_PORT=3306
|
CPONE_DB_PORT=3306
|
||||||
CPONE_DB_DATABASE=cpone_middleware
|
CPONE_DB_DATABASE=cpone_middleware
|
||||||
@@ -152,9 +154,6 @@ CPONE_DATABASE_SETTINGS_TOKEN=replace-with-a-different-admin-token
|
|||||||
CPONE_DATABASE_CREDENTIAL_KEY=replace-with-a-long-random-encryption-key
|
CPONE_DATABASE_CREDENTIAL_KEY=replace-with-a-long-random-encryption-key
|
||||||
```
|
```
|
||||||
|
|
||||||
`DB_*` tetap menjadi koneksi default (saat ini database dev Awalbros). Request
|
|
||||||
lama yang tidak mengirim kode RS akan tetap menggunakan koneksi ini.
|
|
||||||
|
|
||||||
## Multi-database per kode RS
|
## Multi-database per kode RS
|
||||||
|
|
||||||
Tambahkan atau perbarui koneksi RS melalui API berikut. Koneksi akan dites
|
Tambahkan atau perbarui koneksi RS melalui API berikut. Koneksi akan dites
|
||||||
@@ -185,10 +184,10 @@ GET /api/cpone/database-settings/{kodeRs}
|
|||||||
```
|
```
|
||||||
|
|
||||||
Saat startup, service otomatis membuat database `CPONE_DB_DATABASE` beserta
|
Saat startup, service otomatis membuat database `CPONE_DB_DATABASE` beserta
|
||||||
tabel `hospitals` dan `hospital_databases`. Koneksi HIS lama dari `DB_*` otomatis
|
tabel `hospitals` dan `hospital_databases`. Tidak ada database RS default; semua
|
||||||
menjadi RS default. Akun `CPONE_DB_USERNAME` harus memiliki izin `CREATE
|
koneksi HIS harus ditambahkan lewat API. Akun `CPONE_DB_USERNAME` harus memiliki
|
||||||
DATABASE` pada bootstrap pertama; alternatifnya jalankan migration
|
izin `CREATE DATABASE` pada bootstrap pertama; alternatifnya jalankan migration
|
||||||
`database/migrations/001_create_cpone_management.sql` secara manual.
|
di direktori `database/migrations` secara manual.
|
||||||
|
|
||||||
Response list/detail tidak pernah menampilkan password, hanya field
|
Response list/detail tidak pernah menampilkan password, hanya field
|
||||||
`has_password`. Password koneksi RS disimpan terenkripsi AES-GCM menggunakan
|
`has_password`. Password koneksi RS disimpan terenkripsi AES-GCM menggunakan
|
||||||
@@ -210,8 +209,9 @@ X-RS-Code: PRIMAYA_BEKASI_BARAT
|
|||||||
|
|
||||||
Sebagai alternatif dapat memakai query `?kode_rs=PRIMAYA_BEKASI_BARAT`.
|
Sebagai alternatif dapat memakai query `?kode_rs=PRIMAYA_BEKASI_BARAT`.
|
||||||
Header memiliki prioritas lebih tinggi daripada query. Response juga memuat
|
Header memiliki prioritas lebih tinggi daripada query. Response juga memuat
|
||||||
header `X-RS-Code` agar koneksi yang terpilih mudah diaudit. Jika keduanya tidak
|
header `X-RS-Code` agar koneksi yang terpilih mudah diaudit. Salah satu dari
|
||||||
ada, service memakai `DB_DEFAULT_RS_CODE`.
|
header atau query tersebut wajib dikirim; jika tidak ada, API mengembalikan
|
||||||
|
status `422` dan tidak mengakses database HIS.
|
||||||
|
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
|
|||||||
@@ -25,10 +25,6 @@ func main() {
|
|||||||
registry, err := databaseconfig.NewRegistry(bootstrapCtx, databaseconfig.Setting{
|
registry, err := databaseconfig.NewRegistry(bootstrapCtx, databaseconfig.Setting{
|
||||||
Host: cfg.ManagementDBHost, Port: cfg.ManagementDBPort, Database: cfg.ManagementDBDatabase,
|
Host: cfg.ManagementDBHost, Port: cfg.ManagementDBPort, Database: cfg.ManagementDBDatabase,
|
||||||
Username: cfg.ManagementDBUsername, Password: cfg.ManagementDBPassword,
|
Username: cfg.ManagementDBUsername, Password: cfg.ManagementDBPassword,
|
||||||
}, cfg.DBDefaultRSCode, databaseconfig.Setting{
|
|
||||||
RSCode: cfg.DBDefaultRSCode, Name: cfg.DBDefaultRSName,
|
|
||||||
Host: cfg.DBHost, Port: cfg.DBPort, Database: cfg.DBDatabase,
|
|
||||||
Username: cfg.DBUsername, Password: cfg.DBPassword,
|
|
||||||
}, databaseconfig.PoolConfig{
|
}, databaseconfig.PoolConfig{
|
||||||
MaxOpenConns: cfg.DBMaxOpenConns, MaxIdleConns: cfg.DBMaxIdleConns,
|
MaxOpenConns: cfg.DBMaxOpenConns, MaxIdleConns: cfg.DBMaxIdleConns,
|
||||||
ConnMaxLifetime: cfg.DBConnMaxLifetime,
|
ConnMaxLifetime: cfg.DBConnMaxLifetime,
|
||||||
@@ -39,14 +35,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer registry.Close()
|
defer registry.Close()
|
||||||
|
|
||||||
defaultDBCtx, defaultDBCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
handler := apphttp.NewDynamicHandler()
|
||||||
repo, err := registry.Resolve(defaultDBCtx, cfg.DBDefaultRSCode)
|
|
||||||
defaultDBCancel()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("ping mysql: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := apphttp.NewHandler(repo)
|
|
||||||
settingsHandler := apphttp.NewDatabaseSettingsHandler(registry)
|
settingsHandler := apphttp.NewDatabaseSettingsHandler(registry)
|
||||||
protected := func(handler http.Handler) http.Handler {
|
protected := func(handler http.Handler) http.Handler {
|
||||||
return apphttp.BearerAuth(cfg.BearerToken, apphttp.SelectDatabase(registry, handler))
|
return apphttp.BearerAuth(cfg.BearerToken, apphttp.SelectDatabase(registry, handler))
|
||||||
@@ -58,6 +47,10 @@ func main() {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("GET /api/cpone/paket", protected(http.HandlerFunc(handler.ListPaket)))
|
mux.Handle("GET /api/cpone/paket", protected(http.HandlerFunc(handler.ListPaket)))
|
||||||
mux.Handle("GET /api/cpone/paket/{id}", protected(http.HandlerFunc(handler.GetPaket)))
|
mux.Handle("GET /api/cpone/paket/{id}", protected(http.HandlerFunc(handler.GetPaket)))
|
||||||
|
mux.Handle("GET /api/cpone/layanan", protected(http.HandlerFunc(handler.ListLayanan)))
|
||||||
|
mux.Handle("GET /api/cpone/perusahaan", protected(http.HandlerFunc(handler.ListPerusahaan)))
|
||||||
|
mux.Handle("GET /api/cpone/perusahaan/{id}", protected(http.HandlerFunc(handler.GetPerusahaan)))
|
||||||
|
mux.Handle("GET /api/cpone/perusahaan/{id}/group-tarif", protected(http.HandlerFunc(handler.GetPerusahaanGroupTarif)))
|
||||||
mux.Handle("GET /api/cpone/tarif-kerjasama", protected(http.HandlerFunc(handler.ListTarifKerjasama)))
|
mux.Handle("GET /api/cpone/tarif-kerjasama", protected(http.HandlerFunc(handler.ListTarifKerjasama)))
|
||||||
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", protected(http.HandlerFunc(handler.GetTarifKerjasama)))
|
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", protected(http.HandlerFunc(handler.GetTarifKerjasama)))
|
||||||
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
|
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS hospitals (
|
|||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
code VARCHAR(50) NOT NULL,
|
code VARCHAR(50) NOT NULL,
|
||||||
name VARCHAR(150) NOT NULL,
|
name VARCHAR(150) NOT NULL,
|
||||||
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
|||||||
17
database/migrations/002_remove_default_hospital.sql
Normal file
17
database/migrations/002_remove_default_hospital.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
USE `cpone_middleware`;
|
||||||
|
|
||||||
|
SET @drop_default_column = IF(
|
||||||
|
EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE()
|
||||||
|
AND table_name = 'hospitals'
|
||||||
|
AND column_name = 'is_default'
|
||||||
|
),
|
||||||
|
'ALTER TABLE hospitals DROP COLUMN is_default',
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE drop_default_statement FROM @drop_default_column;
|
||||||
|
EXECUTE drop_default_statement;
|
||||||
|
DEALLOCATE PREPARE drop_default_statement;
|
||||||
@@ -12,18 +12,11 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
AppHost string
|
AppHost string
|
||||||
AppPort string
|
AppPort string
|
||||||
DBHost string
|
|
||||||
DBPort string
|
|
||||||
DBDatabase string
|
|
||||||
DBUsername string
|
|
||||||
DBPassword string
|
|
||||||
BearerToken string
|
BearerToken string
|
||||||
DBSettingsToken string
|
DBSettingsToken string
|
||||||
DBMaxOpenConns int
|
DBMaxOpenConns int
|
||||||
DBMaxIdleConns int
|
DBMaxIdleConns int
|
||||||
DBConnMaxLifetime time.Duration
|
DBConnMaxLifetime time.Duration
|
||||||
DBDefaultRSCode string
|
|
||||||
DBDefaultRSName string
|
|
||||||
ManagementDBHost string
|
ManagementDBHost string
|
||||||
ManagementDBPort string
|
ManagementDBPort string
|
||||||
ManagementDBDatabase string
|
ManagementDBDatabase string
|
||||||
@@ -40,35 +33,22 @@ func Load(path string) (Config, error) {
|
|||||||
cfg := Config{
|
cfg := Config{
|
||||||
AppHost: env("APP_HOST", "0.0.0.0"),
|
AppHost: env("APP_HOST", "0.0.0.0"),
|
||||||
AppPort: env("APP_PORT", "8080"),
|
AppPort: env("APP_PORT", "8080"),
|
||||||
DBHost: env("DB_HOST", "127.0.0.1"),
|
|
||||||
DBPort: env("DB_PORT", "3306"),
|
|
||||||
DBDatabase: env("DB_DATABASE", ""),
|
|
||||||
DBUsername: env("DB_USERNAME", "root"),
|
|
||||||
DBPassword: env("DB_PASSWORD", ""),
|
|
||||||
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
||||||
DBSettingsToken: env("CPONE_DATABASE_SETTINGS_TOKEN", env("CPONE_BEARER_TOKEN", "")),
|
DBSettingsToken: env("CPONE_DATABASE_SETTINGS_TOKEN", env("CPONE_BEARER_TOKEN", "")),
|
||||||
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
||||||
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
||||||
DBConnMaxLifetime: time.Duration(envInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
|
DBConnMaxLifetime: time.Duration(envInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
|
||||||
DBDefaultRSCode: strings.ToUpper(strings.TrimSpace(env("DB_DEFAULT_RS_CODE", "AWALBROS_DEV"))),
|
ManagementDBHost: env("CPONE_DB_HOST", "127.0.0.1"),
|
||||||
DBDefaultRSName: env("DB_DEFAULT_RS_NAME", "RS Dev Awalbros"),
|
ManagementDBPort: env("CPONE_DB_PORT", "3306"),
|
||||||
ManagementDBHost: env("CPONE_DB_HOST", env("DB_HOST", "127.0.0.1")),
|
|
||||||
ManagementDBPort: env("CPONE_DB_PORT", env("DB_PORT", "3306")),
|
|
||||||
ManagementDBDatabase: env("CPONE_DB_DATABASE", "cpone_middleware"),
|
ManagementDBDatabase: env("CPONE_DB_DATABASE", "cpone_middleware"),
|
||||||
ManagementDBUsername: env("CPONE_DB_USERNAME", env("DB_USERNAME", "root")),
|
ManagementDBUsername: env("CPONE_DB_USERNAME", "root"),
|
||||||
ManagementDBPassword: env("CPONE_DB_PASSWORD", env("DB_PASSWORD", "")),
|
ManagementDBPassword: env("CPONE_DB_PASSWORD", ""),
|
||||||
DBCredentialSecret: env("CPONE_DATABASE_CREDENTIAL_KEY", env("CPONE_DATABASE_SETTINGS_TOKEN", env("CPONE_BEARER_TOKEN", ""))),
|
DBCredentialSecret: env("CPONE_DATABASE_CREDENTIAL_KEY", env("CPONE_DATABASE_SETTINGS_TOKEN", env("CPONE_BEARER_TOKEN", ""))),
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.DBDatabase == "" {
|
|
||||||
return Config{}, fmt.Errorf("DB_DATABASE wajib diisi")
|
|
||||||
}
|
|
||||||
if cfg.BearerToken == "" {
|
if cfg.BearerToken == "" {
|
||||||
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
||||||
}
|
}
|
||||||
if cfg.DBDefaultRSCode == "" {
|
|
||||||
return Config{}, fmt.Errorf("DB_DEFAULT_RS_CODE wajib diisi")
|
|
||||||
}
|
|
||||||
if cfg.ManagementDBDatabase == "" {
|
if cfg.ManagementDBDatabase == "" {
|
||||||
return Config{}, fmt.Errorf("CPONE_DB_DATABASE wajib diisi")
|
return Config{}, fmt.Errorf("CPONE_DB_DATABASE wajib diisi")
|
||||||
}
|
}
|
||||||
@@ -107,7 +87,9 @@ func loadDotEnv(path string) error {
|
|||||||
value = strings.Trim(value, `"'`)
|
value = strings.Trim(value, `"'`)
|
||||||
|
|
||||||
if key != "" {
|
if key != "" {
|
||||||
_ = os.Setenv(key, value)
|
if _, alreadySet := os.LookupEnv(key); !alreadySet {
|
||||||
|
_ = os.Setenv(key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ type PublicSetting struct {
|
|||||||
Database string `json:"database"`
|
Database string `json:"database"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
HasPassword bool `json:"has_password"`
|
HasPassword bool `json:"has_password"`
|
||||||
IsDefault bool `json:"is_default"`
|
|
||||||
Active bool `json:"active"`
|
Active bool `json:"active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ type PoolConfig struct {
|
|||||||
type Registry struct {
|
type Registry struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
managementDB *sql.DB
|
managementDB *sql.DB
|
||||||
defaultCode string
|
|
||||||
poolConfig PoolConfig
|
poolConfig PoolConfig
|
||||||
credentialKey [32]byte
|
credentialKey [32]byte
|
||||||
settings map[string]Setting
|
settings map[string]Setting
|
||||||
@@ -70,9 +68,9 @@ type Registry struct {
|
|||||||
retiredPools []*sql.DB
|
retiredPools []*sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRegistry creates the management database and tables, then seeds the
|
// NewRegistry creates the management database and tables, then loads all
|
||||||
// existing HIS connection as the default hospital.
|
// active hospital database settings.
|
||||||
func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode string, defaultSetting Setting, poolConfig PoolConfig, credentialSecret string) (*Registry, error) {
|
func NewRegistry(ctx context.Context, managementSetting Setting, poolConfig PoolConfig, credentialSecret string) (*Registry, error) {
|
||||||
if !validDatabaseName.MatchString(managementSetting.Database) {
|
if !validDatabaseName.MatchString(managementSetting.Database) {
|
||||||
return nil, errors.New("nama database manajemen hanya boleh berisi huruf, angka, dan underscore")
|
return nil, errors.New("nama database manajemen hanya boleh berisi huruf, angka, dan underscore")
|
||||||
}
|
}
|
||||||
@@ -86,7 +84,6 @@ func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode str
|
|||||||
}
|
}
|
||||||
r := &Registry{
|
r := &Registry{
|
||||||
managementDB: managementDB,
|
managementDB: managementDB,
|
||||||
defaultCode: NormalizeRSCode(defaultCode),
|
|
||||||
poolConfig: poolConfig,
|
poolConfig: poolConfig,
|
||||||
credentialKey: sha256.Sum256([]byte(credentialSecret)),
|
credentialKey: sha256.Sum256([]byte(credentialSecret)),
|
||||||
settings: make(map[string]Setting),
|
settings: make(map[string]Setting),
|
||||||
@@ -96,14 +93,6 @@ func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode str
|
|||||||
_ = managementDB.Close()
|
_ = managementDB.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defaultSetting.RSCode = r.defaultCode
|
|
||||||
if strings.TrimSpace(defaultSetting.Name) == "" {
|
|
||||||
defaultSetting.Name = "RS Dev Awalbros"
|
|
||||||
}
|
|
||||||
if err := r.seedDefault(ctx, defaultSetting); err != nil {
|
|
||||||
_ = managementDB.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := r.load(ctx); err != nil {
|
if err := r.load(ctx); err != nil {
|
||||||
_ = managementDB.Close()
|
_ = managementDB.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -113,13 +102,8 @@ func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode str
|
|||||||
|
|
||||||
func NormalizeRSCode(code string) string { return strings.ToUpper(strings.TrimSpace(code)) }
|
func NormalizeRSCode(code string) string { return strings.ToUpper(strings.TrimSpace(code)) }
|
||||||
|
|
||||||
func (r *Registry) DefaultCode() string { return r.defaultCode }
|
|
||||||
|
|
||||||
func (r *Registry) Resolve(ctx context.Context, code string) (repository.MySQLLayananRepository, error) {
|
func (r *Registry) Resolve(ctx context.Context, code string) (repository.MySQLLayananRepository, error) {
|
||||||
code = NormalizeRSCode(code)
|
code = NormalizeRSCode(code)
|
||||||
if code == "" {
|
|
||||||
code = r.defaultCode
|
|
||||||
}
|
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
setting, exists := r.settings[code]
|
setting, exists := r.settings[code]
|
||||||
db := r.pools[code]
|
db := r.pools[code]
|
||||||
@@ -174,7 +158,7 @@ func (r *Registry) Upsert(ctx context.Context, setting Setting) (PublicSetting,
|
|||||||
return PublicSetting{}, false, err
|
return PublicSetting{}, false, err
|
||||||
}
|
}
|
||||||
if created {
|
if created {
|
||||||
result, execErr := tx.ExecContext(ctx, `INSERT INTO hospitals (code, name, is_default, active) VALUES (?, ?, 0, 1)`, setting.RSCode, setting.Name)
|
result, execErr := tx.ExecContext(ctx, `INSERT INTO hospitals (code, name, active) VALUES (?, ?, 1)`, setting.RSCode, setting.Name)
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
_ = newDB.Close()
|
_ = newDB.Close()
|
||||||
return PublicSetting{}, false, execErr
|
return PublicSetting{}, false, execErr
|
||||||
@@ -211,7 +195,7 @@ func (r *Registry) Upsert(ctx context.Context, setting Setting) (PublicSetting,
|
|||||||
r.retiredPools = append(r.retiredPools, oldPool)
|
r.retiredPools = append(r.retiredPools, oldPool)
|
||||||
}
|
}
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
return publicSetting(setting, r.defaultCode), created, nil
|
return publicSetting(setting), created, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) List() []PublicSetting {
|
func (r *Registry) List() []PublicSetting {
|
||||||
@@ -219,7 +203,7 @@ func (r *Registry) List() []PublicSetting {
|
|||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
result := make([]PublicSetting, 0, len(r.settings))
|
result := make([]PublicSetting, 0, len(r.settings))
|
||||||
for _, setting := range r.settings {
|
for _, setting := range r.settings {
|
||||||
result = append(result, publicSetting(setting, r.defaultCode))
|
result = append(result, publicSetting(setting))
|
||||||
}
|
}
|
||||||
sort.Slice(result, func(i, j int) bool { return result[i].RSCode < result[j].RSCode })
|
sort.Slice(result, func(i, j int) bool { return result[i].RSCode < result[j].RSCode })
|
||||||
return result
|
return result
|
||||||
@@ -230,7 +214,7 @@ func (r *Registry) Get(code string) (PublicSetting, bool) {
|
|||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
defer r.mu.RUnlock()
|
||||||
setting, exists := r.settings[code]
|
setting, exists := r.settings[code]
|
||||||
return publicSetting(setting, r.defaultCode), exists
|
return publicSetting(setting), exists
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) Close() error {
|
func (r *Registry) Close() error {
|
||||||
@@ -287,7 +271,7 @@ func (r *Registry) migrate(ctx context.Context) error {
|
|||||||
statements := []string{
|
statements := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS hospitals (
|
`CREATE TABLE IF NOT EXISTS hospitals (
|
||||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, code VARCHAR(50) NOT NULL, name VARCHAR(150) NOT NULL,
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, code VARCHAR(50) NOT NULL, name VARCHAR(150) NOT NULL,
|
||||||
is_default TINYINT(1) NOT NULL DEFAULT 0, active TINYINT(1) NOT NULL DEFAULT 1,
|
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id), UNIQUE KEY uq_hospitals_code (code), KEY idx_hospitals_active (active)
|
PRIMARY KEY (id), UNIQUE KEY uq_hospitals_code (code), KEY idx_hospitals_active (active)
|
||||||
@@ -307,44 +291,19 @@ func (r *Registry) migrate(ctx context.Context) error {
|
|||||||
return fmt.Errorf("migrasi database manajemen: %w", err)
|
return fmt.Errorf("migrasi database manajemen: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var defaultColumnExists int
|
||||||
|
if err := r.managementDB.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.columns
|
||||||
|
WHERE table_schema = DATABASE() AND table_name = 'hospitals' AND column_name = 'is_default'`).Scan(&defaultColumnExists); err != nil {
|
||||||
|
return fmt.Errorf("periksa schema database manajemen: %w", err)
|
||||||
|
}
|
||||||
|
if defaultColumnExists > 0 {
|
||||||
|
if _, err := r.managementDB.ExecContext(ctx, `ALTER TABLE hospitals DROP COLUMN is_default`); err != nil {
|
||||||
|
return fmt.Errorf("hapus kolom default rumah sakit: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Registry) seedDefault(ctx context.Context, setting Setting) error {
|
|
||||||
setting = normalizeSetting(setting)
|
|
||||||
if err := Validate(setting); err != nil {
|
|
||||||
return fmt.Errorf("setting database default tidak valid: %w", err)
|
|
||||||
}
|
|
||||||
encryptedPassword, err := r.encrypt(setting.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tx, err := r.managementDB.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
if _, err := tx.ExecContext(ctx, `UPDATE hospitals SET is_default = 0 WHERE code <> ?`, setting.RSCode); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.ExecContext(ctx, `INSERT INTO hospitals (code, name, is_default, active) VALUES (?, ?, 1, 1)
|
|
||||||
ON DUPLICATE KEY UPDATE is_default = 1, active = 1`, setting.RSCode, setting.Name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var hospitalID int64
|
|
||||||
if err := tx.QueryRowContext(ctx, `SELECT id FROM hospitals WHERE code = ?`, setting.RSCode).Scan(&hospitalID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = tx.ExecContext(ctx, `INSERT INTO hospital_databases (hospital_id, host, port, database_name, username, encrypted_password)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE hospital_id = hospital_id`, hospitalID, setting.Host,
|
|
||||||
setting.Port, setting.Database, setting.Username, encryptedPassword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Registry) load(ctx context.Context) error {
|
func (r *Registry) load(ctx context.Context) error {
|
||||||
rows, err := r.managementDB.QueryContext(ctx, `SELECT h.code, h.name, d.host, d.port, d.database_name, d.username, d.encrypted_password
|
rows, err := r.managementDB.QueryContext(ctx, `SELECT h.code, h.name, d.host, d.port, d.database_name, d.username, d.encrypted_password
|
||||||
FROM hospitals h INNER JOIN hospital_databases d ON d.hospital_id = h.id WHERE h.active = 1 ORDER BY h.code`)
|
FROM hospitals h INNER JOIN hospital_databases d ON d.hospital_id = h.id WHERE h.active = 1 ORDER BY h.code`)
|
||||||
@@ -464,10 +423,10 @@ func normalizeSetting(setting Setting) Setting {
|
|||||||
return setting
|
return setting
|
||||||
}
|
}
|
||||||
|
|
||||||
func publicSetting(setting Setting, defaultCode string) PublicSetting {
|
func publicSetting(setting Setting) PublicSetting {
|
||||||
return PublicSetting{RSCode: setting.RSCode, Name: setting.Name, Host: setting.Host, Port: setting.Port,
|
return PublicSetting{RSCode: setting.RSCode, Name: setting.Name, Host: setting.Host, Port: setting.Port,
|
||||||
Database: setting.Database, Username: setting.Username, HasPassword: setting.Password != "",
|
Database: setting.Database, Username: setting.Username, HasPassword: setting.Password != "",
|
||||||
IsDefault: setting.RSCode == defaultCode, Active: true}
|
Active: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mysqlDSN(setting Setting) string {
|
func mysqlDSN(setting Setting) string {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
var digitsOnly = regexp.MustCompile(`^\d+$`)
|
var digitsOnly = regexp.MustCompile(`^\d+$`)
|
||||||
|
|
||||||
type LayananRepository interface {
|
type LayananRepository interface {
|
||||||
|
ListLayanan(ctx context.Context, params repository.LayananListParams) (repository.LayananPage, error)
|
||||||
MappingHarga(ctx context.Context, params repository.MappingHargaParams) ([]repository.TarifLayanan, error)
|
MappingHarga(ctx context.Context, params repository.MappingHargaParams) ([]repository.TarifLayanan, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +41,20 @@ type TarifKerjasamaRepository interface {
|
|||||||
GetTarifKerjasama(ctx context.Context, id int64, params repository.TarifKerjasamaDetailParams) (*repository.TarifKerjasamaDetail, error)
|
GetTarifKerjasama(ctx context.Context, id int64, params repository.TarifKerjasamaDetailParams) (*repository.TarifKerjasamaDetail, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PerusahaanRepository interface {
|
||||||
|
ListPerusahaan(ctx context.Context, params repository.PerusahaanListParams) (repository.PerusahaanPage, error)
|
||||||
|
GetPerusahaan(ctx context.Context, id string) (*repository.PerusahaanDetail, error)
|
||||||
|
GetPerusahaanGroupTarif(ctx context.Context, id, groupTarifID string) (*repository.PerusahaanGroupTarifDetail, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
layananRepo LayananRepository
|
layananRepo LayananRepository
|
||||||
labRepo LabRegistrationRepository
|
labRepo LabRegistrationRepository
|
||||||
patientRepo PatientRepository
|
patientRepo PatientRepository
|
||||||
labResultRepo LabResultRepository
|
labResultRepo LabResultRepository
|
||||||
paketRepo PaketRepository
|
paketRepo PaketRepository
|
||||||
tarifRepo TarifKerjasamaRepository
|
tarifRepo TarifKerjasamaRepository
|
||||||
|
perusahaanRepo PerusahaanRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type CPONERepository interface {
|
type CPONERepository interface {
|
||||||
@@ -56,15 +64,18 @@ type CPONERepository interface {
|
|||||||
LabResultRepository
|
LabResultRepository
|
||||||
PaketRepository
|
PaketRepository
|
||||||
TarifKerjasamaRepository
|
TarifKerjasamaRepository
|
||||||
|
PerusahaanRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(repo CPONERepository) Handler {
|
func NewHandler(repo CPONERepository) Handler {
|
||||||
return Handler{
|
return Handler{
|
||||||
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
||||||
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
labResultRepo: repo, paketRepo: repo, tarifRepo: repo, perusahaanRepo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewDynamicHandler() Handler { return Handler{} }
|
||||||
|
|
||||||
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
||||||
if repo, ok := selectedRepository(ctx); ok {
|
if repo, ok := selectedRepository(ctx); ok {
|
||||||
return repo
|
return repo
|
||||||
@@ -107,6 +118,13 @@ func (h Handler) tarifRepository(ctx context.Context) TarifKerjasamaRepository {
|
|||||||
return h.tarifRepo
|
return h.tarifRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h Handler) perusahaanRepository(ctx context.Context) PerusahaanRepository {
|
||||||
|
if repo, ok := selectedRepository(ctx); ok {
|
||||||
|
return repo
|
||||||
|
}
|
||||||
|
return h.perusahaanRepo
|
||||||
|
}
|
||||||
|
|
||||||
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
|
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
|
||||||
layananIDs := parseLayananIDs(r)
|
layananIDs := parseLayananIDs(r)
|
||||||
if len(layananIDs) == 0 {
|
if len(layananIDs) == 0 {
|
||||||
|
|||||||
65
internal/http/layanan.go
Normal file
65
internal/http/layanan.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const layananPerPage = 15
|
||||||
|
|
||||||
|
type LayananPaginationResponse struct {
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
Data []repository.LayananDepartemen `json:"data"`
|
||||||
|
FirstPageURL string `json:"first_page_url"`
|
||||||
|
From *int `json:"from"`
|
||||||
|
LastPage int `json:"last_page"`
|
||||||
|
LastPageURL string `json:"last_page_url"`
|
||||||
|
Links map[string]any `json:"links"`
|
||||||
|
NextPageURL *string `json:"next_page_url"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) ListLayanan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pageNumber := layananPageNumber(r)
|
||||||
|
page, err := h.layananRepository(r.Context()).ListLayanan(r.Context(), repository.LayananListParams{
|
||||||
|
Page: pageNumber, PerPage: layananPerPage,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, Response{
|
||||||
|
Success: false, Message: "internal server error", Data: nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPage := (page.Total + layananPerPage - 1) / layananPerPage
|
||||||
|
if lastPage < 1 {
|
||||||
|
lastPage = 1
|
||||||
|
}
|
||||||
|
var from *int
|
||||||
|
if len(page.Items) > 0 {
|
||||||
|
value := (pageNumber-1)*layananPerPage + 1
|
||||||
|
from = &value
|
||||||
|
}
|
||||||
|
var nextPageURL *string
|
||||||
|
if pageNumber < lastPage {
|
||||||
|
value := currentRequestURL(r, pageNumber+1)
|
||||||
|
nextPageURL = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, LayananPaginationResponse{
|
||||||
|
CurrentPage: pageNumber, Data: page.Items, FirstPageURL: "", From: from,
|
||||||
|
LastPage: lastPage, LastPageURL: "", Links: map[string]any{},
|
||||||
|
NextPageURL: nextPageURL, Total: page.Total, PerPage: layananPerPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func layananPageNumber(r *http.Request) int {
|
||||||
|
page, err := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return page
|
||||||
|
}
|
||||||
82
internal/http/layanan_test.go
Normal file
82
internal/http/layanan_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeLayananRepository struct {
|
||||||
|
page repository.LayananPage
|
||||||
|
err error
|
||||||
|
params repository.LayananListParams
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLayananRepository) ListLayanan(_ context.Context, params repository.LayananListParams) (repository.LayananPage, error) {
|
||||||
|
f.params = params
|
||||||
|
return f.page, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeLayananRepository) MappingHarga(context.Context, repository.MappingHargaParams) ([]repository.TarifLayanan, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLayananResponseParity(t *testing.T) {
|
||||||
|
group, department := "MCU", "MEDICAL CHECK UP"
|
||||||
|
layananID, name, price := "6413", "DARAH LENGKAP", "125000.00"
|
||||||
|
repo := &fakeLayananRepository{page: repository.LayananPage{
|
||||||
|
Total: 16,
|
||||||
|
Items: []repository.LayananDepartemen{{
|
||||||
|
GroupLayananID: &group, Departemen: &department, LayananDepartemenID: 987,
|
||||||
|
LayananID: &layananID, NamaLayanan: &name, Harga: &price,
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
handler := Handler{layananRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://localhost/api/cpone/layanan?page=1&kode_rs=AWALBROS_DEV", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ListLayanan(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if repo.params.Page != 1 || repo.params.PerPage != 15 {
|
||||||
|
t.Fatalf("params = %+v", repo.params)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`"GroupLayananID":"MCU"`, `"Departemen":"MEDICAL CHECK UP"`,
|
||||||
|
`"LayananDepartemenID":987`, `"LayananID":"6413"`,
|
||||||
|
`"NamaLayanan":"DARAH LENGKAP"`, `"Harga":"125000.00"`,
|
||||||
|
`"last_page":2`, `"per_page":15`,
|
||||||
|
`"next_page_url":"http://localhost/api/cpone/layanan?kode_rs=AWALBROS_DEV\u0026page=2"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLayananUsesLaravelPageFallback(t *testing.T) {
|
||||||
|
repo := &fakeLayananRepository{page: repository.LayananPage{Items: []repository.LayananDepartemen{}}}
|
||||||
|
handler := Handler{layananRepo: repo}
|
||||||
|
for _, target := range []string{"/api/cpone/layanan", "/api/cpone/layanan?page=0", "/api/cpone/layanan?page=invalid"} {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ListLayanan(recorder, httptest.NewRequest(http.MethodGet, target, nil))
|
||||||
|
if recorder.Code != http.StatusOK || repo.params.Page != 1 || repo.params.PerPage != layananPerPage {
|
||||||
|
t.Fatalf("target=%s status=%d params=%+v", target, recorder.Code, repo.params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListLayananInternalError(t *testing.T) {
|
||||||
|
handler := Handler{layananRepo: &fakeLayananRepository{err: errors.New("query failed")}}
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ListLayanan(recorder, httptest.NewRequest(http.MethodGet, "/api/cpone/layanan", nil))
|
||||||
|
if recorder.Code != http.StatusInternalServerError {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
type selectedRepositoryKey struct{}
|
type selectedRepositoryKey struct{}
|
||||||
|
|
||||||
type DatabaseResolver interface {
|
type DatabaseResolver interface {
|
||||||
DefaultCode() string
|
|
||||||
Resolve(ctx context.Context, code string) (repository.MySQLLayananRepository, error)
|
Resolve(ctx context.Context, code string) (repository.MySQLLayananRepository, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +38,9 @@ func SelectDatabase(resolver DatabaseResolver, next http.Handler) http.Handler {
|
|||||||
code = strings.TrimSpace(r.URL.Query().Get("kode_rs"))
|
code = strings.TrimSpace(r.URL.Query().Get("kode_rs"))
|
||||||
}
|
}
|
||||||
if code == "" {
|
if code == "" {
|
||||||
code = resolver.DefaultCode()
|
WriteJSON(w, http.StatusUnprocessableEntity, Response{Success: false,
|
||||||
|
Message: "Kode RS wajib diisi melalui header X-RS-Code atau parameter kode_rs", Data: nil})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
code = databaseconfig.NormalizeRSCode(code)
|
code = databaseconfig.NormalizeRSCode(code)
|
||||||
|
|
||||||
|
|||||||
@@ -12,28 +12,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type fakeDatabaseResolver struct {
|
type fakeDatabaseResolver struct {
|
||||||
defaultCode string
|
|
||||||
resolvedCode string
|
resolvedCode string
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeDatabaseResolver) DefaultCode() string { return f.defaultCode }
|
|
||||||
func (f *fakeDatabaseResolver) Resolve(_ context.Context, code string) (repository.MySQLLayananRepository, error) {
|
func (f *fakeDatabaseResolver) Resolve(_ context.Context, code string) (repository.MySQLLayananRepository, error) {
|
||||||
f.resolvedCode = code
|
f.resolvedCode = code
|
||||||
return repository.MySQLLayananRepository{}, f.err
|
return repository.MySQLLayananRepository{}, f.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSelectDatabasePriorityAndDefault(t *testing.T) {
|
func TestSelectDatabasePriority(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name, target, header, want string
|
name, target, header, want string
|
||||||
}{
|
}{
|
||||||
{name: "header", target: "/resource?kode_rs=QUERY", header: " rs_header ", want: "RS_HEADER"},
|
{name: "header", target: "/resource?kode_rs=QUERY", header: " rs_header ", want: "RS_HEADER"},
|
||||||
{name: "query", target: "/resource?kode_rs=rs_query", want: "RS_QUERY"},
|
{name: "query", target: "/resource?kode_rs=rs_query", want: "RS_QUERY"},
|
||||||
{name: "default", target: "/resource", want: "AWALBROS_DEV"},
|
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
resolver := &fakeDatabaseResolver{defaultCode: "AWALBROS_DEV"}
|
resolver := &fakeDatabaseResolver{}
|
||||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, ok := selectedRepository(r.Context()); !ok {
|
if _, ok := selectedRepository(r.Context()); !ok {
|
||||||
t.Fatal("selected repository missing from context")
|
t.Fatal("selected repository missing from context")
|
||||||
@@ -54,8 +51,23 @@ func TestSelectDatabasePriorityAndDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelectDatabaseRequiresRSCode(t *testing.T) {
|
||||||
|
resolver := &fakeDatabaseResolver{}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/resource", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
SelectDatabase(resolver, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
|
t.Fatal("next handler should not be called")
|
||||||
|
})).ServeHTTP(recorder, request)
|
||||||
|
if recorder.Code != http.StatusUnprocessableEntity {
|
||||||
|
t.Fatalf("status = %d, body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if resolver.resolvedCode != "" {
|
||||||
|
t.Fatalf("resolver called with %q", resolver.resolvedCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelectDatabaseUnknownCode(t *testing.T) {
|
func TestSelectDatabaseUnknownCode(t *testing.T) {
|
||||||
resolver := &fakeDatabaseResolver{defaultCode: "DEFAULT", err: databaseconfig.ErrSettingNotFound}
|
resolver := &fakeDatabaseResolver{err: databaseconfig.ErrSettingNotFound}
|
||||||
request := httptest.NewRequest(http.MethodGet, "/resource?kode_rs=unknown", nil)
|
request := httptest.NewRequest(http.MethodGet, "/resource?kode_rs=unknown", nil)
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
SelectDatabase(resolver, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
SelectDatabase(resolver, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||||
|
|||||||
@@ -171,7 +171,12 @@ func currentRequestURL(r *http.Request, page int) string {
|
|||||||
if host == "" {
|
if host == "" {
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
}
|
}
|
||||||
return (&url.URL{Scheme: scheme, Host: host, Path: r.URL.Path, RawQuery: "page=" + strconv.Itoa(page)}).String()
|
query := url.Values{}
|
||||||
|
if code := r.URL.Query().Get("kode_rs"); code != "" {
|
||||||
|
query.Set("kode_rs", code)
|
||||||
|
}
|
||||||
|
query.Set("page", strconv.Itoa(page))
|
||||||
|
return (&url.URL{Scheme: scheme, Host: host, Path: r.URL.Path, RawQuery: query.Encode()}).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func writePaketValidationError(w http.ResponseWriter, message string) {
|
func writePaketValidationError(w http.ResponseWriter, message string) {
|
||||||
|
|||||||
98
internal/http/perusahaan.go
Normal file
98
internal/http/perusahaan.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
const perusahaanPerPage = 15
|
||||||
|
|
||||||
|
type PerusahaanPaginationResponse struct {
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
Data []repository.PerusahaanSummary `json:"data"`
|
||||||
|
FirstPageURL string `json:"first_page_url"`
|
||||||
|
From *int `json:"from"`
|
||||||
|
LastPage int `json:"last_page"`
|
||||||
|
LastPageURL string `json:"last_page_url"`
|
||||||
|
Links map[string]any `json:"links"`
|
||||||
|
NextPageURL *string `json:"next_page_url"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) ListPerusahaan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pageNumber := perusahaanPageNumber(r)
|
||||||
|
page, err := h.perusahaanRepository(r.Context()).ListPerusahaan(r.Context(), repository.PerusahaanListParams{
|
||||||
|
Search: strings.TrimSpace(r.URL.Query().Get("search")), Page: pageNumber, PerPage: perusahaanPerPage,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writePerusahaanInternalError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastPage := (page.Total + perusahaanPerPage - 1) / perusahaanPerPage
|
||||||
|
if lastPage < 1 {
|
||||||
|
lastPage = 1
|
||||||
|
}
|
||||||
|
var from *int
|
||||||
|
if len(page.Items) > 0 {
|
||||||
|
value := (pageNumber-1)*perusahaanPerPage + 1
|
||||||
|
from = &value
|
||||||
|
}
|
||||||
|
var nextPageURL *string
|
||||||
|
if pageNumber < lastPage {
|
||||||
|
value := currentRequestURL(r, pageNumber+1)
|
||||||
|
nextPageURL = &value
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, PerusahaanPaginationResponse{
|
||||||
|
CurrentPage: pageNumber, Data: page.Items, FirstPageURL: "", From: from,
|
||||||
|
LastPage: lastPage, LastPageURL: "", Links: map[string]any{}, NextPageURL: nextPageURL,
|
||||||
|
Total: page.Total, PerPage: perusahaanPerPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) GetPerusahaan(w http.ResponseWriter, r *http.Request) {
|
||||||
|
detail, err := h.perusahaanRepository(r.Context()).GetPerusahaan(r.Context(), strings.TrimSpace(r.PathValue("id")))
|
||||||
|
if err != nil {
|
||||||
|
writePerusahaanInternalError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if detail == nil {
|
||||||
|
writePerusahaanNotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, Response{Success: true, Message: "ok", Data: detail})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) GetPerusahaanGroupTarif(w http.ResponseWriter, r *http.Request) {
|
||||||
|
detail, err := h.perusahaanRepository(r.Context()).GetPerusahaanGroupTarif(
|
||||||
|
r.Context(), strings.TrimSpace(r.PathValue("id")), strings.TrimSpace(r.URL.Query().Get("group_tarif_id")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writePerusahaanInternalError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if detail == nil {
|
||||||
|
writePerusahaanNotFound(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
WriteJSON(w, http.StatusOK, Response{Success: true, Message: "ok", Data: detail})
|
||||||
|
}
|
||||||
|
|
||||||
|
func perusahaanPageNumber(r *http.Request) int {
|
||||||
|
page, err := strconv.Atoi(r.URL.Query().Get("page"))
|
||||||
|
if err != nil || page < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return page
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePerusahaanNotFound(w http.ResponseWriter) {
|
||||||
|
WriteJSON(w, http.StatusNotFound, Response{Success: false, Message: "Data tidak ditemukan", Data: nil})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePerusahaanInternalError(w http.ResponseWriter) {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, Response{Success: false, Message: "internal server error", Data: nil})
|
||||||
|
}
|
||||||
142
internal/http/perusahaan_test.go
Normal file
142
internal/http/perusahaan_test.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakePerusahaanRepository struct {
|
||||||
|
page repository.PerusahaanPage
|
||||||
|
detail *repository.PerusahaanDetail
|
||||||
|
groupDetail *repository.PerusahaanGroupTarifDetail
|
||||||
|
err error
|
||||||
|
listParams repository.PerusahaanListParams
|
||||||
|
detailID string
|
||||||
|
groupID string
|
||||||
|
groupTarifID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePerusahaanRepository) ListPerusahaan(_ context.Context, params repository.PerusahaanListParams) (repository.PerusahaanPage, error) {
|
||||||
|
f.listParams = params
|
||||||
|
return f.page, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePerusahaanRepository) GetPerusahaan(_ context.Context, id string) (*repository.PerusahaanDetail, error) {
|
||||||
|
f.detailID = id
|
||||||
|
return f.detail, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePerusahaanRepository) GetPerusahaanGroupTarif(_ context.Context, id, groupTarifID string) (*repository.PerusahaanGroupTarifDetail, error) {
|
||||||
|
f.groupID = id
|
||||||
|
f.groupTarifID = groupTarifID
|
||||||
|
return f.groupDetail, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListPerusahaanResponseParity(t *testing.T) {
|
||||||
|
repo := &fakePerusahaanRepository{page: repository.PerusahaanPage{
|
||||||
|
Total: 16,
|
||||||
|
Items: []repository.PerusahaanSummary{{
|
||||||
|
PerusahaanID: "AAL", KodePerusahaan: "INS-114", Nama: "ASURANSI ALLIANZ LIFE INDONESIA, PT",
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
handler := Handler{perusahaanRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://localhost/api/cpone/perusahaan?search=allianz&page=1&kode_rs=AWALBROS_DEV", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ListPerusahaan(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if repo.listParams.Search != "allianz" || repo.listParams.Page != 1 || repo.listParams.PerPage != 15 {
|
||||||
|
t.Fatalf("params = %+v", repo.listParams)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`"PerusahaanID":"AAL"`, `"KodePerusahaan":"INS-114"`,
|
||||||
|
`"Nama":"ASURANSI ALLIANZ LIFE INDONESIA, PT"`, `"last_page":2`,
|
||||||
|
`"next_page_url":"http://localhost/api/cpone/perusahaan?kode_rs=AWALBROS_DEV\u0026page=2"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPerusahaanByCode(t *testing.T) {
|
||||||
|
detail := &repository.PerusahaanDetail{
|
||||||
|
PerusahaanID: "AAL", KodePerusahaan: "INS-114", Nama: "ALLIANZ",
|
||||||
|
JenisPerusahaanID: 1, Penjamin: "Y", Asuransi: "Y", Provider: "N",
|
||||||
|
COB: "N", MataUangID: "IDR", DefaultDiskon: "0.00", PPN: "0.00",
|
||||||
|
FlagDiskon: "N", VerifikasiBilling: "N", NA: "N",
|
||||||
|
}
|
||||||
|
repo := &fakePerusahaanRepository{detail: detail}
|
||||||
|
handler := Handler{perusahaanRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/perusahaan/INS-114", nil)
|
||||||
|
request.SetPathValue("id", "INS-114")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.GetPerusahaan(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK || repo.detailID != "INS-114" {
|
||||||
|
t.Fatalf("status=%d id=%q body=%s", recorder.Code, repo.detailID, recorder.Body.String())
|
||||||
|
}
|
||||||
|
for _, expected := range []string{`"success":true`, `"PerusahaanID":"AAL"`, `"ppn":"0.00"`, `"nomerRekening":null`} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPerusahaanGroupTarif(t *testing.T) {
|
||||||
|
name := "DARAH LENGKAP"
|
||||||
|
detail := &repository.PerusahaanGroupTarifDetail{
|
||||||
|
PerusahaanID: "AAL", KodePerusahaan: "INS-114", NamaPerusahaan: "ALLIANZ",
|
||||||
|
GroupTarif: []repository.PerusahaanGroupTarif{{
|
||||||
|
GroupTarifID: 27, NamaGroupTarif: "Tarif 2026", TanggalBerlaku: "2026-01-01",
|
||||||
|
TanggalBerakhir: "2026-12-31", PeriodeTarifID: 10, Keterangan: "Tarif 2026",
|
||||||
|
DetailLayanan: []repository.PerusahaanGroupTarifLayanan{{
|
||||||
|
TarifLayananID: 1, LayananID: "6413", NamaLayanan: &name,
|
||||||
|
JenisLayananID: 1, Kelas: "Default", NamaKelas: "Default", Harga: "100000.00",
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
repo := &fakePerusahaanRepository{groupDetail: detail}
|
||||||
|
handler := Handler{perusahaanRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/perusahaan/AAL/group-tarif?group_tarif_id=27", nil)
|
||||||
|
request.SetPathValue("id", "AAL")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.GetPerusahaanGroupTarif(recorder, request)
|
||||||
|
if recorder.Code != http.StatusOK || repo.groupID != "AAL" || repo.groupTarifID != "27" {
|
||||||
|
t.Fatalf("status=%d company=%q group=%q body=%s", recorder.Code, repo.groupID, repo.groupTarifID, recorder.Body.String())
|
||||||
|
}
|
||||||
|
for _, expected := range []string{`"GroupTarifID":27`, `"NamaGroupTarif":"Tarif 2026"`, `"DetailLayanan":[`, `"NamaKelas":"Default"`} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerusahaanNotFoundAndInternalError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
repo *fakePerusahaanRepository
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{name: "not found", repo: &fakePerusahaanRepository{}, want: http.StatusNotFound},
|
||||||
|
{name: "error", repo: &fakePerusahaanRepository{err: errors.New("query failed")}, want: http.StatusInternalServerError},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
handler := Handler{perusahaanRepo: test.repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/perusahaan/UNKNOWN", nil)
|
||||||
|
request.SetPathValue("id", "UNKNOWN")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.GetPerusahaan(recorder, request)
|
||||||
|
if recorder.Code != test.want {
|
||||||
|
t.Fatalf("status=%d want=%d body=%s", recorder.Code, test.want, recorder.Body.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
71
internal/repository/layanan.go
Normal file
71
internal/repository/layanan.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayananListParams struct {
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayananPage struct {
|
||||||
|
Items []LayananDepartemen
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayananDepartemen struct {
|
||||||
|
GroupLayananID *string `json:"GroupLayananID"`
|
||||||
|
Departemen *string `json:"Departemen"`
|
||||||
|
LayananDepartemenID int64 `json:"LayananDepartemenID"`
|
||||||
|
LayananID *string `json:"LayananID"`
|
||||||
|
NamaLayanan *string `json:"NamaLayanan"`
|
||||||
|
Harga *string `json:"Harga"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) ListLayanan(ctx context.Context, params LayananListParams) (LayananPage, error) {
|
||||||
|
var total int
|
||||||
|
if err := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM layanandepartemen
|
||||||
|
WHERE NA = 'N' AND DepartemenID = 'MCU'
|
||||||
|
`).Scan(&total); err != nil {
|
||||||
|
return LayananPage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT ld.GroupLayananID, d.Nama, ld.LayananDepartemenID,
|
||||||
|
ld.LayananID, ml.Nama, CAST(ld.Harga AS CHAR)
|
||||||
|
FROM layanandepartemen ld
|
||||||
|
LEFT JOIN departemen d ON d.DepartemenID = ld.DepartemenID
|
||||||
|
LEFT JOIN masterlayanan ml ON ml.LayananID = ld.LayananID
|
||||||
|
WHERE ld.NA = 'N' AND ld.DepartemenID = 'MCU'
|
||||||
|
ORDER BY ld.TanggalBuat DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`, params.PerPage, (params.Page-1)*params.PerPage)
|
||||||
|
if err != nil {
|
||||||
|
return LayananPage{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]LayananDepartemen, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item LayananDepartemen
|
||||||
|
var groupLayananID, departemen, layananID, namaLayanan, harga sql.NullString
|
||||||
|
if err := rows.Scan(&groupLayananID, &departemen, &item.LayananDepartemenID,
|
||||||
|
&layananID, &namaLayanan, &harga); err != nil {
|
||||||
|
return LayananPage{}, err
|
||||||
|
}
|
||||||
|
item.GroupLayananID = nullStringPtr(groupLayananID)
|
||||||
|
item.Departemen = nullStringPtr(departemen)
|
||||||
|
item.LayananID = nullStringPtr(layananID)
|
||||||
|
item.NamaLayanan = nullStringPtr(namaLayanan)
|
||||||
|
item.Harga = nullStringPtr(harga)
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return LayananPage{}, err
|
||||||
|
}
|
||||||
|
return LayananPage{Items: items, Total: total}, nil
|
||||||
|
}
|
||||||
290
internal/repository/perusahaan.go
Normal file
290
internal/repository/perusahaan.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PerusahaanListParams struct {
|
||||||
|
Search string
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanPage struct {
|
||||||
|
Items []PerusahaanSummary
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanSummary struct {
|
||||||
|
PerusahaanID string `json:"PerusahaanID"`
|
||||||
|
KodePerusahaan string `json:"KodePerusahaan"`
|
||||||
|
Nama string `json:"Nama"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanDetail struct {
|
||||||
|
PerusahaanID string `json:"PerusahaanID"`
|
||||||
|
KodePerusahaan string `json:"KodePerusahaan"`
|
||||||
|
Nama string `json:"Nama"`
|
||||||
|
JenisPerusahaanID int64 `json:"JenisPerusahaanID"`
|
||||||
|
Penjamin string `json:"Penjamin"`
|
||||||
|
Asuransi string `json:"Asuransi"`
|
||||||
|
Provider string `json:"Provider"`
|
||||||
|
COB string `json:"COB"`
|
||||||
|
MataUangID string `json:"MataUangID"`
|
||||||
|
Penanggungjawab *string `json:"Penanggungjawab"`
|
||||||
|
TeleponPenanggungjawab *string `json:"TeleponPenanggungjawab"`
|
||||||
|
EmailPenanggungjawab *string `json:"EmailPenanggungjawab"`
|
||||||
|
Alamat *string `json:"Alamat"`
|
||||||
|
Kota *string `json:"Kota"`
|
||||||
|
KodePos *string `json:"KodePos"`
|
||||||
|
Propinsi *string `json:"Propinsi"`
|
||||||
|
Negara *string `json:"Negara"`
|
||||||
|
Telepon *string `json:"Telepon"`
|
||||||
|
Fax *string `json:"Fax"`
|
||||||
|
Email *string `json:"Email"`
|
||||||
|
Website *string `json:"Website"`
|
||||||
|
NamaBank *string `json:"NamaBank"`
|
||||||
|
NamaRekening *string `json:"NamaRekening"`
|
||||||
|
NomerRekening *string `json:"nomerRekening"`
|
||||||
|
DefaultLamaPembayaran int64 `json:"DefaultLamaPembayaran"`
|
||||||
|
DefaultDiskon string `json:"DefaultDiskon"`
|
||||||
|
PPN string `json:"ppn"`
|
||||||
|
Catatan *string `json:"Catatan"`
|
||||||
|
NPWP *string `json:"NPWP"`
|
||||||
|
FlagDiskon string `json:"FlagDiskon"`
|
||||||
|
VerifikasiBilling string `json:"VerifikasiBilling"`
|
||||||
|
PICMarketing *string `json:"PIC_Marketing"`
|
||||||
|
NA string `json:"NA"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanGroupTarifDetail struct {
|
||||||
|
PerusahaanID string `json:"PerusahaanID"`
|
||||||
|
KodePerusahaan string `json:"KodePerusahaan"`
|
||||||
|
NamaPerusahaan string `json:"NamaPerusahaan"`
|
||||||
|
GroupTarif []PerusahaanGroupTarif `json:"GroupTarif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanGroupTarif struct {
|
||||||
|
GroupTarifID int64 `json:"GroupTarifID"`
|
||||||
|
NamaGroupTarif string `json:"NamaGroupTarif"`
|
||||||
|
TanggalBerlaku string `json:"TanggalBerlaku"`
|
||||||
|
TanggalBerakhir string `json:"TanggalBerakhir"`
|
||||||
|
PeriodeTarifID int64 `json:"PeriodeTarifID"`
|
||||||
|
Keterangan string `json:"Keterangan"`
|
||||||
|
DetailLayanan []PerusahaanGroupTarifLayanan `json:"DetailLayanan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PerusahaanGroupTarifLayanan struct {
|
||||||
|
TarifLayananID int64 `json:"TarifLayananID"`
|
||||||
|
LayananID string `json:"LayananID"`
|
||||||
|
NamaLayanan *string `json:"NamaLayanan"`
|
||||||
|
JenisLayananID int64 `json:"JenisLayananID"`
|
||||||
|
NamaJenisLayanan *string `json:"NamaJenisLayanan"`
|
||||||
|
Kelas string `json:"Kelas"`
|
||||||
|
NamaKelas string `json:"NamaKelas"`
|
||||||
|
Harga string `json:"Harga"`
|
||||||
|
TipeDiskonRJ string `json:"TipeDiskonRJ"`
|
||||||
|
DiskonRJ string `json:"DiskonRJ"`
|
||||||
|
TipeDiskonRI string `json:"TipeDiskonRI"`
|
||||||
|
DiskonRI string `json:"DiskonRI"`
|
||||||
|
TipeShare string `json:"TipeShare"`
|
||||||
|
ShareDokter string `json:"ShareDokter"`
|
||||||
|
ShareRS string `json:"ShareRS"`
|
||||||
|
ShareExt string `json:"ShareExt"`
|
||||||
|
ShareLain string `json:"ShareLain"`
|
||||||
|
AsalTarif string `json:"AsalTarif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) ListPerusahaan(ctx context.Context, params PerusahaanListParams) (PerusahaanPage, error) {
|
||||||
|
where := "WHERE NA = 'N'"
|
||||||
|
args := make([]any, 0, 5)
|
||||||
|
if params.Search != "" {
|
||||||
|
where += " AND (Nama LIKE ? OR KodePerusahaan LIKE ? OR PerusahaanID LIKE ?)"
|
||||||
|
like := "%" + params.Search + "%"
|
||||||
|
args = append(args, like, like, like)
|
||||||
|
}
|
||||||
|
var total int
|
||||||
|
if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM perusahaan "+where, args...).Scan(&total); err != nil {
|
||||||
|
return PerusahaanPage{}, err
|
||||||
|
}
|
||||||
|
queryArgs := append(append([]any{}, args...), params.PerPage, (params.Page-1)*params.PerPage)
|
||||||
|
rows, err := r.db.QueryContext(ctx, `SELECT PerusahaanID, KodePerusahaan, Nama
|
||||||
|
FROM perusahaan `+where+` ORDER BY Nama LIMIT ? OFFSET ?`, queryArgs...)
|
||||||
|
if err != nil {
|
||||||
|
return PerusahaanPage{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := make([]PerusahaanSummary, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PerusahaanSummary
|
||||||
|
if err := rows.Scan(&item.PerusahaanID, &item.KodePerusahaan, &item.Nama); err != nil {
|
||||||
|
return PerusahaanPage{}, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return PerusahaanPage{}, err
|
||||||
|
}
|
||||||
|
return PerusahaanPage{Items: items, Total: total}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) GetPerusahaan(ctx context.Context, id string) (*PerusahaanDetail, error) {
|
||||||
|
row := r.db.QueryRowContext(ctx, `SELECT PerusahaanID, KodePerusahaan, Nama, JenisPerusahaanID,
|
||||||
|
Penjamin, Asuransi, Provider, COB, MataUangID, Penanggungjawab, TeleponPenanggungjawab,
|
||||||
|
EmailPenanggungjawab, Alamat, Kota, KodePos, Propinsi, Negara, Telepon, Fax, Email,
|
||||||
|
Website, NamaBank, NamaRekening, NomerRekening, DefaultLamaPembayaran,
|
||||||
|
CAST(DefaultDiskon AS CHAR), CAST(ppn AS CHAR), Catatan, NPWP, FlagDiskon,
|
||||||
|
VerifikasiBilling, PIC_Marketing, NA
|
||||||
|
FROM perusahaan WHERE NA = 'N' AND (PerusahaanID = ? OR KodePerusahaan = ?) LIMIT 1`, id, id)
|
||||||
|
detail, err := scanPerusahaanDetail(row)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &detail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) GetPerusahaanGroupTarif(ctx context.Context, id, groupTarifID string) (*PerusahaanGroupTarifDetail, error) {
|
||||||
|
var result PerusahaanGroupTarifDetail
|
||||||
|
err := r.db.QueryRowContext(ctx, `SELECT PerusahaanID, KodePerusahaan, Nama FROM perusahaan
|
||||||
|
WHERE NA = 'N' AND (PerusahaanID = ? OR KodePerusahaan = ?) LIMIT 1`, id, id).
|
||||||
|
Scan(&result.PerusahaanID, &result.KodePerusahaan, &result.NamaPerusahaan)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT pg.GroupTarifID, CAST(pg.TanggalBerlaku AS CHAR), CAST(pg.TanggalBerakhir AS CHAR),
|
||||||
|
pg.PeriodeTarifID, pg.Keterangan
|
||||||
|
FROM perusahaan_grouptarif pg
|
||||||
|
INNER JOIN perusahaan_tarif pt ON pt.Perusahaan_TarifID = pg.Perusahaan_TarifID
|
||||||
|
WHERE pt.PerusahaanID = ? AND pg.NA = 'N'
|
||||||
|
AND CURDATE() BETWEEN pg.TanggalBerlaku AND pg.TanggalBerakhir`
|
||||||
|
args := []any{result.PerusahaanID}
|
||||||
|
if groupTarifID != "" {
|
||||||
|
query += " AND pg.GroupTarifID = ?"
|
||||||
|
args = append(args, groupTarifID)
|
||||||
|
}
|
||||||
|
query += " ORDER BY pg.TanggalBerlaku DESC, pg.GroupTarifID DESC"
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result.GroupTarif = make([]PerusahaanGroupTarif, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var group PerusahaanGroupTarif
|
||||||
|
if err := rows.Scan(&group.GroupTarifID, &group.TanggalBerlaku, &group.TanggalBerakhir,
|
||||||
|
&group.PeriodeTarifID, &group.Keterangan); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if group.Keterangan != "" {
|
||||||
|
group.NamaGroupTarif = group.Keterangan
|
||||||
|
} else {
|
||||||
|
group.NamaGroupTarif = fmt.Sprintf("Group Tarif %d", group.GroupTarifID)
|
||||||
|
}
|
||||||
|
group.DetailLayanan, err = r.perusahaanGroupTarifLayanan(ctx, group.GroupTarifID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.GroupTarif = append(result.GroupTarif, group)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) perusahaanGroupTarifLayanan(ctx context.Context, groupTarifID int64) ([]PerusahaanGroupTarifLayanan, error) {
|
||||||
|
tableName := fmt.Sprintf("perusahaan_tarif_layanan_%d", groupTarifID)
|
||||||
|
exists, err := r.tableExists(ctx, tableName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return []PerusahaanGroupTarifLayanan{}, nil
|
||||||
|
}
|
||||||
|
query := fmt.Sprintf(`SELECT tarif.TarifLayananID, tarif.LayananID, layanan.Nama,
|
||||||
|
tarif.JenisLayananID, jenis.Nama, tarif.KelasID, kelas.Nama,
|
||||||
|
CAST(tarif.Harga AS CHAR), tarif.TipeDiskon, CAST(tarif.Diskon AS CHAR),
|
||||||
|
tarif.TipeDiskonRI, CAST(tarif.DiskonRI AS CHAR), tarif.TipeShare,
|
||||||
|
CAST(tarif.ShareDokter AS CHAR), CAST(tarif.ShareRS AS CHAR),
|
||||||
|
CAST(tarif.ShareExternal AS CHAR), CAST(tarif.ShareLain AS CHAR), tarif.AsalTarif
|
||||||
|
FROM %s tarif
|
||||||
|
LEFT JOIN masterlayanan layanan ON layanan.LayananID = tarif.LayananID
|
||||||
|
LEFT JOIN jenislayanan jenis ON jenis.JenisLayananID = tarif.JenisLayananID
|
||||||
|
LEFT JOIN kelas kelas ON kelas.KelasID = tarif.KelasID
|
||||||
|
WHERE tarif.NA = 'N'
|
||||||
|
ORDER BY layanan.Nama, tarif.KelasID`, tableName)
|
||||||
|
rows, err := r.db.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result := make([]PerusahaanGroupTarifLayanan, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PerusahaanGroupTarifLayanan
|
||||||
|
var namaLayanan, namaJenisLayanan, namaKelas sql.NullString
|
||||||
|
if err := rows.Scan(&item.TarifLayananID, &item.LayananID, &namaLayanan,
|
||||||
|
&item.JenisLayananID, &namaJenisLayanan, &item.Kelas, &namaKelas,
|
||||||
|
&item.Harga, &item.TipeDiskonRJ, &item.DiskonRJ, &item.TipeDiskonRI,
|
||||||
|
&item.DiskonRI, &item.TipeShare, &item.ShareDokter, &item.ShareRS,
|
||||||
|
&item.ShareExt, &item.ShareLain, &item.AsalTarif); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.NamaLayanan = nullStringPtr(namaLayanan)
|
||||||
|
item.NamaJenisLayanan = nullStringPtr(namaJenisLayanan)
|
||||||
|
if namaKelas.Valid && namaKelas.String != "" {
|
||||||
|
item.NamaKelas = namaKelas.String
|
||||||
|
} else {
|
||||||
|
item.NamaKelas = item.Kelas
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanPerusahaanDetail(row interface{ Scan(...any) error }) (PerusahaanDetail, error) {
|
||||||
|
var result PerusahaanDetail
|
||||||
|
var penanggungjawab, teleponPJ, emailPJ, alamat, kota, kodePos, propinsi, negara sql.NullString
|
||||||
|
var telepon, fax, email, website, namaBank, namaRekening, nomerRekening sql.NullString
|
||||||
|
var catatan, npwp, picMarketing sql.NullString
|
||||||
|
err := row.Scan(&result.PerusahaanID, &result.KodePerusahaan, &result.Nama,
|
||||||
|
&result.JenisPerusahaanID, &result.Penjamin, &result.Asuransi, &result.Provider,
|
||||||
|
&result.COB, &result.MataUangID, &penanggungjawab, &teleponPJ, &emailPJ,
|
||||||
|
&alamat, &kota, &kodePos, &propinsi, &negara, &telepon, &fax, &email, &website,
|
||||||
|
&namaBank, &namaRekening, &nomerRekening, &result.DefaultLamaPembayaran,
|
||||||
|
&result.DefaultDiskon, &result.PPN, &catatan, &npwp, &result.FlagDiskon,
|
||||||
|
&result.VerifikasiBilling, &picMarketing, &result.NA)
|
||||||
|
if err != nil {
|
||||||
|
return PerusahaanDetail{}, err
|
||||||
|
}
|
||||||
|
result.Penanggungjawab = nullStringPtr(penanggungjawab)
|
||||||
|
result.TeleponPenanggungjawab = nullStringPtr(teleponPJ)
|
||||||
|
result.EmailPenanggungjawab = nullStringPtr(emailPJ)
|
||||||
|
result.Alamat = nullStringPtr(alamat)
|
||||||
|
result.Kota = nullStringPtr(kota)
|
||||||
|
result.KodePos = nullStringPtr(kodePos)
|
||||||
|
result.Propinsi = nullStringPtr(propinsi)
|
||||||
|
result.Negara = nullStringPtr(negara)
|
||||||
|
result.Telepon = nullStringPtr(telepon)
|
||||||
|
result.Fax = nullStringPtr(fax)
|
||||||
|
result.Email = nullStringPtr(email)
|
||||||
|
result.Website = nullStringPtr(website)
|
||||||
|
result.NamaBank = nullStringPtr(namaBank)
|
||||||
|
result.NamaRekening = nullStringPtr(namaRekening)
|
||||||
|
result.NomerRekening = nullStringPtr(nomerRekening)
|
||||||
|
result.Catatan = nullStringPtr(catatan)
|
||||||
|
result.NPWP = nullStringPtr(npwp)
|
||||||
|
result.PICMarketing = nullStringPtr(picMarketing)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user