feat: require hospital code and add company APIs

This commit is contained in:
2026-08-28 10:30:08 +07:00
parent a59b2c6443
commit 8dd880003d
17 changed files with 867 additions and 142 deletions

View File

@@ -49,7 +49,6 @@ type PublicSetting struct {
Database string `json:"database"`
Username string `json:"username"`
HasPassword bool `json:"has_password"`
IsDefault bool `json:"is_default"`
Active bool `json:"active"`
}
@@ -62,7 +61,6 @@ type PoolConfig struct {
type Registry struct {
mu sync.RWMutex
managementDB *sql.DB
defaultCode string
poolConfig PoolConfig
credentialKey [32]byte
settings map[string]Setting
@@ -70,9 +68,9 @@ type Registry struct {
retiredPools []*sql.DB
}
// NewRegistry creates the management database and tables, then seeds the
// existing HIS connection as the default hospital.
func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode string, defaultSetting Setting, poolConfig PoolConfig, credentialSecret string) (*Registry, error) {
// NewRegistry creates the management database and tables, then loads all
// active hospital database settings.
func NewRegistry(ctx context.Context, managementSetting Setting, poolConfig PoolConfig, credentialSecret string) (*Registry, error) {
if !validDatabaseName.MatchString(managementSetting.Database) {
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{
managementDB: managementDB,
defaultCode: NormalizeRSCode(defaultCode),
poolConfig: poolConfig,
credentialKey: sha256.Sum256([]byte(credentialSecret)),
settings: make(map[string]Setting),
@@ -96,14 +93,6 @@ func NewRegistry(ctx context.Context, managementSetting Setting, defaultCode str
_ = managementDB.Close()
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 {
_ = managementDB.Close()
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 (r *Registry) DefaultCode() string { return r.defaultCode }
func (r *Registry) Resolve(ctx context.Context, code string) (repository.MySQLLayananRepository, error) {
code = NormalizeRSCode(code)
if code == "" {
code = r.defaultCode
}
r.mu.RLock()
setting, exists := r.settings[code]
db := r.pools[code]
@@ -174,7 +158,7 @@ func (r *Registry) Upsert(ctx context.Context, setting Setting) (PublicSetting,
return PublicSetting{}, false, err
}
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 {
_ = newDB.Close()
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.mu.Unlock()
return publicSetting(setting, r.defaultCode), created, nil
return publicSetting(setting), created, nil
}
func (r *Registry) List() []PublicSetting {
@@ -219,7 +203,7 @@ func (r *Registry) List() []PublicSetting {
defer r.mu.RUnlock()
result := make([]PublicSetting, 0, len(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 })
return result
@@ -230,7 +214,7 @@ func (r *Registry) Get(code string) (PublicSetting, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
setting, exists := r.settings[code]
return publicSetting(setting, r.defaultCode), exists
return publicSetting(setting), exists
}
func (r *Registry) Close() error {
@@ -287,7 +271,7 @@ func (r *Registry) migrate(ctx context.Context) error {
statements := []string{
`CREATE TABLE IF NOT EXISTS hospitals (
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,
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)
@@ -307,44 +291,19 @@ func (r *Registry) migrate(ctx context.Context) error {
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
}
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 {
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`)
@@ -464,10 +423,10 @@ func normalizeSetting(setting Setting) 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,
Database: setting.Database, Username: setting.Username, HasPassword: setting.Password != "",
IsDefault: setting.RSCode == defaultCode, Active: true}
Active: true}
}
func mysqlDSN(setting Setting) string {