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

@@ -12,18 +12,11 @@ import (
type Config struct {
AppHost string
AppPort string
DBHost string
DBPort string
DBDatabase string
DBUsername string
DBPassword string
BearerToken string
DBSettingsToken string
DBMaxOpenConns int
DBMaxIdleConns int
DBConnMaxLifetime time.Duration
DBDefaultRSCode string
DBDefaultRSName string
ManagementDBHost string
ManagementDBPort string
ManagementDBDatabase string
@@ -40,35 +33,22 @@ func Load(path string) (Config, error) {
cfg := Config{
AppHost: env("APP_HOST", "0.0.0.0"),
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", ""),
DBSettingsToken: env("CPONE_DATABASE_SETTINGS_TOKEN", env("CPONE_BEARER_TOKEN", "")),
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
DBConnMaxLifetime: time.Duration(envInt("DB_CONN_MAX_LIFETIME_MINUTES", 5)) * time.Minute,
DBDefaultRSCode: strings.ToUpper(strings.TrimSpace(env("DB_DEFAULT_RS_CODE", "AWALBROS_DEV"))),
DBDefaultRSName: env("DB_DEFAULT_RS_NAME", "RS Dev Awalbros"),
ManagementDBHost: env("CPONE_DB_HOST", env("DB_HOST", "127.0.0.1")),
ManagementDBPort: env("CPONE_DB_PORT", env("DB_PORT", "3306")),
ManagementDBHost: env("CPONE_DB_HOST", "127.0.0.1"),
ManagementDBPort: env("CPONE_DB_PORT", "3306"),
ManagementDBDatabase: env("CPONE_DB_DATABASE", "cpone_middleware"),
ManagementDBUsername: env("CPONE_DB_USERNAME", env("DB_USERNAME", "root")),
ManagementDBPassword: env("CPONE_DB_PASSWORD", env("DB_PASSWORD", "")),
ManagementDBUsername: env("CPONE_DB_USERNAME", "root"),
ManagementDBPassword: env("CPONE_DB_PASSWORD", ""),
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 == "" {
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 == "" {
return Config{}, fmt.Errorf("CPONE_DB_DATABASE wajib diisi")
}
@@ -107,7 +87,9 @@ func loadDotEnv(path string) error {
value = strings.Trim(value, `"'`)
if key != "" {
_ = os.Setenv(key, value)
if _, alreadySet := os.LookupEnv(key); !alreadySet {
_ = os.Setenv(key, value)
}
}
}

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 {

View File

@@ -13,6 +13,7 @@ import (
var digitsOnly = regexp.MustCompile(`^\d+$`)
type LayananRepository interface {
ListLayanan(ctx context.Context, params repository.LayananListParams) (repository.LayananPage, 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)
}
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 {
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
labResultRepo LabResultRepository
paketRepo PaketRepository
tarifRepo TarifKerjasamaRepository
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
labResultRepo LabResultRepository
paketRepo PaketRepository
tarifRepo TarifKerjasamaRepository
perusahaanRepo PerusahaanRepository
}
type CPONERepository interface {
@@ -56,15 +64,18 @@ type CPONERepository interface {
LabResultRepository
PaketRepository
TarifKerjasamaRepository
PerusahaanRepository
}
func NewHandler(repo CPONERepository) Handler {
return Handler{
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 {
if repo, ok := selectedRepository(ctx); ok {
return repo
@@ -107,6 +118,13 @@ func (h Handler) tarifRepository(ctx context.Context) TarifKerjasamaRepository {
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) {
layananIDs := parseLayananIDs(r)
if len(layananIDs) == 0 {

65
internal/http/layanan.go Normal file
View 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
}

View 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())
}
}

View File

@@ -13,7 +13,6 @@ import (
type selectedRepositoryKey struct{}
type DatabaseResolver interface {
DefaultCode() string
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"))
}
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)

View File

@@ -12,28 +12,25 @@ import (
)
type fakeDatabaseResolver struct {
defaultCode string
resolvedCode string
err error
}
func (f *fakeDatabaseResolver) DefaultCode() string { return f.defaultCode }
func (f *fakeDatabaseResolver) Resolve(_ context.Context, code string) (repository.MySQLLayananRepository, error) {
f.resolvedCode = code
return repository.MySQLLayananRepository{}, f.err
}
func TestSelectDatabasePriorityAndDefault(t *testing.T) {
func TestSelectDatabasePriority(t *testing.T) {
tests := []struct {
name, target, header, want string
}{
{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: "default", target: "/resource", want: "AWALBROS_DEV"},
}
for _, test := range tests {
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) {
if _, ok := selectedRepository(r.Context()); !ok {
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) {
resolver := &fakeDatabaseResolver{defaultCode: "DEFAULT", err: databaseconfig.ErrSettingNotFound}
resolver := &fakeDatabaseResolver{err: databaseconfig.ErrSettingNotFound}
request := httptest.NewRequest(http.MethodGet, "/resource?kode_rs=unknown", nil)
recorder := httptest.NewRecorder()
SelectDatabase(resolver, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {

View File

@@ -171,7 +171,12 @@ func currentRequestURL(r *http.Request, page int) string {
if host == "" {
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) {

View 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})
}

View 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())
}
})
}
}

View 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
}

View 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
}