Compare commits
4 Commits
9afd5b792c
...
94c3dd1a70
| Author | SHA1 | Date | |
|---|---|---|---|
| 94c3dd1a70 | |||
| d42d97d066 | |||
| c5a959742b | |||
| aae3c733ae |
14
.env.example
14
.env.example
@@ -1,11 +1,17 @@
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8080
|
||||
|
||||
# Database khusus master CPONE middleware.
|
||||
# Kode instance rumah sakit. Dipakai hanya untuk validasi X-RS-Code.
|
||||
CPONE_RS_CODE=AWALBROS_DEV
|
||||
|
||||
# Departemen HIS yang dipakai endpoint registrasi laboratorium.
|
||||
CPONE_DEPARTEMEN_ID=08_01
|
||||
|
||||
# Database HIS rumah sakit untuk instance ini.
|
||||
CPONE_DB_HOST=127.0.0.1
|
||||
CPONE_DB_PORT=3306
|
||||
CPONE_DB_DATABASE=cpone_middleware
|
||||
CPONE_DB_USERNAME=root
|
||||
CPONE_DB_DATABASE=nama_database_his
|
||||
CPONE_DB_USERNAME=cpone
|
||||
CPONE_DB_PASSWORD=
|
||||
|
||||
DB_MAX_OPEN_CONNS=25
|
||||
@@ -13,5 +19,3 @@ DB_MAX_IDLE_CONNS=25
|
||||
DB_CONN_MAX_LIFETIME_MINUTES=5
|
||||
|
||||
CPONE_BEARER_TOKEN=replace-with-a-secure-token
|
||||
CPONE_DATABASE_SETTINGS_TOKEN=replace-with-a-different-admin-token
|
||||
CPONE_DATABASE_CREDENTIAL_KEY=replace-with-a-long-random-encryption-key
|
||||
|
||||
131
README.md
131
README.md
@@ -17,6 +17,14 @@ GET /api/cpone/perusahaan/{id}/group-tarif?group_tarif_id=
|
||||
Authorization: Bearer <your-secure-token>
|
||||
X-RS-Code: <kode-rs>
|
||||
|
||||
GET /api/cpone/doctors?search=&DokterID_in=&page=1&per_page=15
|
||||
GET /api/doctors?search=&DokterID_in=&page=1&per_page=15
|
||||
Authorization: Bearer <your-secure-token>
|
||||
X-RS-Code: <kode-rs>
|
||||
|
||||
Endpoint ini menampilkan daftar dokter aktif dari HIS. Parameter `search` hanya
|
||||
mencari berdasarkan nama dokter atau kode dokter (`DokterID`).
|
||||
|
||||
GET /api/cpone/tarif-kerjasama?search=&perusahaan_id=&tarif_id=&page=1&per_page=15
|
||||
Authorization: Bearer <your-secure-token>
|
||||
|
||||
@@ -64,7 +72,6 @@ Contoh payload pembuatan pasien HIS:
|
||||
|
||||
```json
|
||||
{
|
||||
"rs": "PRIMAYA",
|
||||
"nama": "BUDI SANTOS",
|
||||
"jenis_kelamin": "L",
|
||||
"tgl_lahir": "1990-01-15",
|
||||
@@ -91,6 +98,40 @@ Contoh payload pembuatan pasien HIS:
|
||||
Endpoint mengembalikan `created`, `already_exists`, atau `possible_duplicate`
|
||||
sesuai hasil pemeriksaan identitas dan kemiripan data pasien di HIS.
|
||||
|
||||
Registrasi pasien umum/MCU menggunakan endpoint berikut. Jika `medrec_id` belum
|
||||
diisi dan pasien tidak ditemukan, caller dapat membuat pasien melalui endpoint
|
||||
medrec lalu mengulangi request registrasi. `departemen_id` dapat dikosongkan;
|
||||
middleware akan memakai `CPONE_DEPARTEMEN_ID` dari `.env`.
|
||||
|
||||
```http
|
||||
POST /api/cpone/patients/registration
|
||||
Authorization: Bearer <your-secure-token>
|
||||
X-RS-Code: <kode-rs>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"medrec_id": "00001234",
|
||||
"nik": "317xxxxxxxxxxxx",
|
||||
"tgl_lahir": "1988-03-03",
|
||||
"tanggal": "2026-09-07",
|
||||
"dokter_id": "D260900003",
|
||||
"jenis_pasien_id": "1",
|
||||
"jenis_reg_id": 2,
|
||||
"periode_tarif_id": "10",
|
||||
"perusahaan_id": "",
|
||||
"asuransi_id": "",
|
||||
"group_tarif_id": "0",
|
||||
"nomer_polis": "",
|
||||
"kelas_id": "Default",
|
||||
"channel": "CPONE"
|
||||
}
|
||||
```
|
||||
|
||||
Endpoint ini idempoten untuk pasien, tanggal, departemen, dan dokter yang sama;
|
||||
registrasi aktif yang sudah ada dikembalikan dengan status `already_registered_visit`.
|
||||
|
||||
Contoh payload registrasi laboratorium:
|
||||
|
||||
```json
|
||||
@@ -154,89 +195,45 @@ bersifat idempoten dan tidak membuat transaksi ganda.
|
||||
|
||||
## Config
|
||||
|
||||
Copy `.env.example` menjadi `.env`, lalu isi koneksi MySQL:
|
||||
Setiap instalasi CPONE hanya terhubung ke satu database HIS. Copy `.env.example`
|
||||
menjadi `.env`, lalu isi kode instance dan koneksi MySQL HIS:
|
||||
|
||||
```text
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8080
|
||||
CPONE_RS_CODE=AWALBROS_DEV
|
||||
CPONE_DEPARTEMEN_ID=08_01
|
||||
CPONE_DB_HOST=127.0.0.1
|
||||
CPONE_DB_PORT=3306
|
||||
CPONE_DB_DATABASE=cpone_middleware
|
||||
CPONE_DB_USERNAME=root
|
||||
CPONE_DB_DATABASE=nama_database_his
|
||||
CPONE_DB_USERNAME=cpone
|
||||
CPONE_DB_PASSWORD=
|
||||
CPONE_BEARER_TOKEN=replace-with-a-secure-token
|
||||
CPONE_DATABASE_SETTINGS_TOKEN=replace-with-a-different-admin-token
|
||||
CPONE_DATABASE_CREDENTIAL_KEY=replace-with-a-long-random-encryption-key
|
||||
```
|
||||
|
||||
## Multi-database per kode RS
|
||||
`CPONE_DB_*` adalah koneksi database HIS, bukan database management CPONE.
|
||||
Password hanya dibaca dari `.env`; aplikasi tidak menyimpan kredensial database
|
||||
ke database lain.
|
||||
|
||||
Tambahkan atau perbarui koneksi RS melalui API berikut. Koneksi akan dites
|
||||
sebelum disimpan; bila `Ping` gagal, setting tidak disimpan.
|
||||
`CPONE_DEPARTEMEN_ID` adalah `DepartemenID` HIS yang digunakan untuk registrasi
|
||||
laboratorium. Nilai ini diambil dari konfigurasi, sehingga tidak perlu dikirim
|
||||
di payload endpoint registrasi.
|
||||
|
||||
```http
|
||||
POST /api/cpone/database-settings
|
||||
Authorization: Bearer <your-database-settings-token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"kode_rs": "PRIMAYA_BEKASI_BARAT",
|
||||
"nama": "Primaya Hospital Bekasi Barat",
|
||||
"host": "10.10.10.20",
|
||||
"port": "3306",
|
||||
"database": "his_bekasi_barat",
|
||||
"username": "cpone",
|
||||
"password": "secret"
|
||||
}
|
||||
```
|
||||
|
||||
Endpoint pengelolaan yang tersedia:
|
||||
|
||||
```text
|
||||
POST /api/cpone/database-settings
|
||||
GET /api/cpone/database-settings
|
||||
GET /api/cpone/database-settings/{kodeRs}
|
||||
```
|
||||
|
||||
Daftar ringkas rumah sakit aktif dapat diambil tanpa memilih database HIS:
|
||||
|
||||
```http
|
||||
GET /api/cpone/hospitals
|
||||
Authorization: Bearer <your-secure-token>
|
||||
```
|
||||
|
||||
Response hanya memuat `kode_rs`, `nama`, dan `status`; rumah sakit nonaktif tidak
|
||||
ditampilkan.
|
||||
|
||||
Saat startup, service otomatis membuat database `CPONE_DB_DATABASE` beserta
|
||||
tabel `hospitals` dan `hospital_databases`. Tidak ada database RS default; semua
|
||||
koneksi HIS harus ditambahkan lewat API. Akun `CPONE_DB_USERNAME` harus memiliki
|
||||
izin `CREATE DATABASE` pada bootstrap pertama; alternatifnya jalankan migration
|
||||
di direktori `database/migrations` secara manual.
|
||||
|
||||
Response list/detail tidak pernah menampilkan password, hanya field
|
||||
`has_password`. Password koneksi RS disimpan terenkripsi AES-GCM menggunakan
|
||||
`CPONE_DATABASE_CREDENTIAL_KEY`. Jangan mengubah key tersebut setelah data mulai
|
||||
disimpan karena password lama tidak akan bisa didekripsi. Jika key belum diisi,
|
||||
service memakai `CPONE_DATABASE_SETTINGS_TOKEN`, lalu `CPONE_BEARER_TOKEN`
|
||||
sebagai fallback; token fallback itu juga tidak boleh diubah tanpa rotasi
|
||||
credential.
|
||||
|
||||
Endpoint pengelolaan memakai `CPONE_DATABASE_SETTINGS_TOKEN`. Jika variabel ini
|
||||
belum diisi, service memakai `CPONE_BEARER_TOKEN` untuk kompatibilitas; gunakan
|
||||
token admin yang berbeda di production.
|
||||
|
||||
Pilih database pada semua endpoint bisnis dengan header:
|
||||
Semua endpoint bisnis tetap membutuhkan header:
|
||||
|
||||
```http
|
||||
X-RS-Code: PRIMAYA_BEKASI_BARAT
|
||||
```
|
||||
|
||||
Sebagai alternatif dapat memakai query `?kode_rs=PRIMAYA_BEKASI_BARAT`.
|
||||
Header memiliki prioritas lebih tinggi daripada query. Response juga memuat
|
||||
header `X-RS-Code` agar koneksi yang terpilih mudah diaudit. Salah satu dari
|
||||
header atau query tersebut wajib dikirim; jika tidak ada, API mengembalikan
|
||||
status `422` dan tidak mengakses database HIS.
|
||||
Sebagai alternatif dapat memakai query `?kode_rs=AWALBROS_DEV`. Header memiliki
|
||||
prioritas lebih tinggi daripada query. Kode tersebut hanya divalidasi terhadap
|
||||
`CPONE_RS_CODE`; kode tidak digunakan untuk memilih database.
|
||||
|
||||
Request tanpa kode mengembalikan `422`, sedangkan kode untuk instance rumah sakit
|
||||
lain mengembalikan `403`. Response sukses tetap mengembalikan header `X-RS-Code`.
|
||||
|
||||
Payload `POST /api/cpone/patients/medrec` tidak lagi membutuhkan field `rs`.
|
||||
Nilai `KodeRS` untuk kebutuhan internal HIS diisi dari `CPONE_RS_CODE`.
|
||||
|
||||
## Run
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func main() {
|
||||
}
|
||||
defer resolver.Close()
|
||||
|
||||
handler := apphttp.NewDynamicHandler(cfg.RSCode)
|
||||
handler := apphttp.NewDynamicHandler(cfg.RSCode, cfg.DepartmentID)
|
||||
protected := func(handler http.Handler) http.Handler {
|
||||
return apphttp.BearerAuth(cfg.BearerToken, apphttp.SelectDatabase(resolver, handler))
|
||||
}
|
||||
@@ -47,12 +47,16 @@ func main() {
|
||||
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/doctors", protected(http.HandlerFunc(handler.ListDokter)))
|
||||
// Backward-compatible route matching the source Primaya API.
|
||||
mux.Handle("GET /api/doctors", protected(http.HandlerFunc(handler.ListDokter)))
|
||||
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/periode-tarif", protected(http.HandlerFunc(handler.ListPeriodeTarif)))
|
||||
mux.Handle("GET /api/cpone/periode-tarif/{periodeTarifID}", protected(http.HandlerFunc(handler.ListPeriodeTarifLayanan)))
|
||||
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
|
||||
mux.Handle("POST /api/cpone/patients/medrec", protected(http.HandlerFunc(handler.StoreMedrec)))
|
||||
mux.Handle("POST /api/cpone/patients/registration", protected(http.HandlerFunc(handler.RegisterPatient)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration", protected(http.HandlerFunc(handler.RegisterLab)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/services", protected(http.HandlerFunc(handler.AddLabServices)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/packages", protected(http.HandlerFunc(handler.AddLabPackage)))
|
||||
|
||||
@@ -13,6 +13,7 @@ type Config struct {
|
||||
AppHost string
|
||||
AppPort string
|
||||
RSCode string
|
||||
DepartmentID string
|
||||
BearerToken string
|
||||
DBMaxOpenConns int
|
||||
DBMaxIdleConns int
|
||||
@@ -33,6 +34,7 @@ func Load(path string) (Config, error) {
|
||||
AppHost: env("APP_HOST", "0.0.0.0"),
|
||||
AppPort: env("APP_PORT", "8080"),
|
||||
RSCode: env("CPONE_RS_CODE", ""),
|
||||
DepartmentID: env("CPONE_DEPARTEMEN_ID", ""),
|
||||
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
||||
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
||||
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
||||
@@ -47,6 +49,9 @@ func Load(path string) (Config, error) {
|
||||
if cfg.RSCode == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_RS_CODE wajib diisi")
|
||||
}
|
||||
if cfg.DepartmentID == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_DEPARTEMEN_ID wajib diisi")
|
||||
}
|
||||
if cfg.BearerToken == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
||||
}
|
||||
|
||||
103
internal/http/dokter.go
Normal file
103
internal/http/dokter.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDokterPerPage = 15
|
||||
maxDokterPerPage = 100
|
||||
)
|
||||
|
||||
type DokterPaginationResponse struct {
|
||||
CurrentPage int `json:"current_page"`
|
||||
Data []repository.Dokter `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) ListDokter(w http.ResponseWriter, r *http.Request) {
|
||||
page := parseDokterPage(r.URL.Query().Get("page"))
|
||||
perPage := parseDokterPerPage(r.URL.Query().Get("per_page"))
|
||||
|
||||
doctorIDs := queryValues(r, "DokterID_in", "DokterID_in[]")
|
||||
doctorIDs = splitDoctorIDs(doctorIDs)
|
||||
|
||||
result, err := h.dokterRepository(r.Context()).ListDokter(r.Context(), repository.DokterListParams{
|
||||
Search: strings.TrimSpace(r.URL.Query().Get("search")), DoctorIDs: doctorIDs,
|
||||
Page: page, PerPage: perPage,
|
||||
})
|
||||
if err != nil {
|
||||
WriteJSON(w, http.StatusInternalServerError, Response{Success: false, Message: "internal server error", Data: nil})
|
||||
return
|
||||
}
|
||||
|
||||
lastPage := (result.Total + perPage - 1) / perPage
|
||||
if lastPage < 1 {
|
||||
lastPage = 1
|
||||
}
|
||||
var from *int
|
||||
if len(result.Items) > 0 {
|
||||
value := (page-1)*perPage + 1
|
||||
from = &value
|
||||
}
|
||||
var nextPageURL *string
|
||||
if page < lastPage {
|
||||
value := currentRequestURL(r, page+1)
|
||||
nextPageURL = &value
|
||||
}
|
||||
|
||||
WriteJSON(w, http.StatusOK, DokterPaginationResponse{
|
||||
CurrentPage: page, Data: result.Items, FirstPageURL: "", From: from,
|
||||
LastPage: lastPage, LastPageURL: "", Links: map[string]any{},
|
||||
NextPageURL: nextPageURL, Total: result.Total, PerPage: perPage,
|
||||
})
|
||||
}
|
||||
|
||||
func parseDokterPage(raw string) int {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value < 1 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func parseDokterPerPage(raw string) int {
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value < 1 {
|
||||
return defaultDokterPerPage
|
||||
}
|
||||
if value > maxDokterPerPage {
|
||||
return maxDokterPerPage
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func splitDoctorIDs(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
for _, part := range strings.Split(value, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[part]; exists {
|
||||
continue
|
||||
}
|
||||
seen[part] = struct{}{}
|
||||
result = append(result, part)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
72
internal/http/dokter_test.go
Normal file
72
internal/http/dokter_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
type fakeDokterRepository struct {
|
||||
page repository.DokterPage
|
||||
params repository.DokterListParams
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeDokterRepository) ListDokter(_ context.Context, params repository.DokterListParams) (repository.DokterPage, error) {
|
||||
f.params = params
|
||||
return f.page, f.err
|
||||
}
|
||||
|
||||
func TestListDokterResponseParity(t *testing.T) {
|
||||
name := "DR. ANDI"
|
||||
repo := &fakeDokterRepository{page: repository.DokterPage{
|
||||
Total: 16,
|
||||
Items: []repository.Dokter{{DokterID: "D001", Nama: &name}},
|
||||
}}
|
||||
handler := Handler{dokterRepo: repo}
|
||||
request := httptest.NewRequest(http.MethodGet, "http://localhost/api/cpone/doctors?search=andi&DokterID_in=D001,D002&page=1&per_page=15&kode_rs=AWALBROS_DEV", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ListDokter(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.params.Search != "andi" || repo.params.Page != 1 || repo.params.PerPage != 15 || strings.Join(repo.params.DoctorIDs, ",") != "D001,D002" {
|
||||
t.Fatalf("params=%+v", repo.params)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
`"DokterID":"D001"`, `"Nama":"DR. ANDI"`,
|
||||
`"last_page":2`, `"per_page":15`,
|
||||
`"next_page_url":"http://localhost/api/cpone/doctors?kode_rs=AWALBROS_DEV\u0026page=2"`,
|
||||
} {
|
||||
if !strings.Contains(recorder.Body.String(), expected) {
|
||||
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
for _, excluded := range []string{`"spesialisasi"`, `"subspesialisasi"`, `"departementIDs"`, `"profileDokter"`} {
|
||||
if strings.Contains(recorder.Body.String(), excluded) {
|
||||
t.Fatalf("response should not contain %s: %s", excluded, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListDokterDefaultsAndPerPageLimit(t *testing.T) {
|
||||
repo := &fakeDokterRepository{page: repository.DokterPage{Items: []repository.Dokter{}}}
|
||||
handler := Handler{dokterRepo: repo}
|
||||
|
||||
for target, expected := range map[string]int{
|
||||
"/api/cpone/doctors": defaultDokterPerPage,
|
||||
"/api/cpone/doctors?page=0": defaultDokterPerPage,
|
||||
"/api/cpone/doctors?per_page=250": maxDokterPerPage,
|
||||
} {
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ListDokter(recorder, httptest.NewRequest(http.MethodGet, target, nil))
|
||||
if recorder.Code != http.StatusOK || repo.params.PerPage != expected || repo.params.Page != 1 {
|
||||
t.Fatalf("target=%s status=%d params=%+v", target, recorder.Code, repo.params)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@ type PatientRepository interface {
|
||||
StoreMedrec(ctx context.Context, params repository.MedrecParams) (repository.MedrecOutcome, error)
|
||||
}
|
||||
|
||||
type RegistrationRepository interface {
|
||||
RegisterPatient(ctx context.Context, params repository.RegistrationParams) (repository.RegistrationOutcome, error)
|
||||
}
|
||||
|
||||
type LabResultRepository interface {
|
||||
GetLabResult(ctx context.Context, labNumber string) (repository.LabResultOutcome, error)
|
||||
}
|
||||
@@ -55,41 +59,54 @@ type PerusahaanRepository interface {
|
||||
GetPerusahaanGroupTarif(ctx context.Context, id, groupTarifID string) (*repository.PerusahaanGroupTarifDetail, error)
|
||||
}
|
||||
|
||||
type DokterRepository interface {
|
||||
ListDokter(ctx context.Context, params repository.DokterListParams) (repository.DokterPage, error)
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
layananRepo LayananRepository
|
||||
labRepo LabRegistrationRepository
|
||||
patientRepo PatientRepository
|
||||
registrationRepo RegistrationRepository
|
||||
labResultRepo LabResultRepository
|
||||
paketRepo PaketRepository
|
||||
tarifRepo TarifKerjasamaRepository
|
||||
periodeTarifRepo PeriodeTarifRepository
|
||||
periodeTarifDetailRepo PeriodeTarifDetailRepository
|
||||
perusahaanRepo PerusahaanRepository
|
||||
dokterRepo DokterRepository
|
||||
hospitalCode string
|
||||
departmentID string
|
||||
}
|
||||
|
||||
type CPONERepository interface {
|
||||
LayananRepository
|
||||
LabRegistrationRepository
|
||||
PatientRepository
|
||||
RegistrationRepository
|
||||
LabResultRepository
|
||||
PaketRepository
|
||||
TarifKerjasamaRepository
|
||||
PeriodeTarifRepository
|
||||
PeriodeTarifDetailRepository
|
||||
PerusahaanRepository
|
||||
DokterRepository
|
||||
}
|
||||
|
||||
func NewHandler(repo CPONERepository) Handler {
|
||||
return Handler{
|
||||
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
||||
layananRepo: repo, labRepo: repo, patientRepo: repo, registrationRepo: repo,
|
||||
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
||||
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
|
||||
dokterRepo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDynamicHandler(hospitalCode string) Handler {
|
||||
return Handler{hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode))}
|
||||
func NewDynamicHandler(hospitalCode string, departmentID string) Handler {
|
||||
return Handler{
|
||||
hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode)),
|
||||
departmentID: strings.TrimSpace(departmentID),
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
||||
@@ -113,6 +130,13 @@ func (h Handler) patientRepository(ctx context.Context) PatientRepository {
|
||||
return h.patientRepo
|
||||
}
|
||||
|
||||
func (h Handler) registrationRepository(ctx context.Context) RegistrationRepository {
|
||||
if repo, ok := selectedRepository(ctx); ok {
|
||||
return repo
|
||||
}
|
||||
return h.registrationRepo
|
||||
}
|
||||
|
||||
func (h Handler) labResultRepository(ctx context.Context) LabResultRepository {
|
||||
if repo, ok := selectedRepository(ctx); ok {
|
||||
return repo
|
||||
@@ -155,6 +179,13 @@ func (h Handler) perusahaanRepository(ctx context.Context) PerusahaanRepository
|
||||
return h.perusahaanRepo
|
||||
}
|
||||
|
||||
func (h Handler) dokterRepository(ctx context.Context) DokterRepository {
|
||||
if repo, ok := selectedRepository(ctx); ok {
|
||||
return repo
|
||||
}
|
||||
return h.dokterRepo
|
||||
}
|
||||
|
||||
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
|
||||
layananIDs := parseLayananIDs(r)
|
||||
if len(layananIDs) == 0 {
|
||||
|
||||
@@ -35,6 +35,11 @@ func (h Handler) RegisterLab(w http.ResponseWriter, r *http.Request) {
|
||||
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", err.Error(), nil)
|
||||
return
|
||||
}
|
||||
// Departemen registrasi ditentukan per instance CPONE melalui .env.
|
||||
// Payload tetap didukung untuk handler/test lama yang tidak memakai config.
|
||||
if h.departmentID != "" {
|
||||
payload["departemen_id"] = h.departmentID
|
||||
}
|
||||
|
||||
validated, validationMessage := validateLabRegistration(payload)
|
||||
if validationMessage != "" {
|
||||
|
||||
@@ -103,6 +103,36 @@ func TestRegisterLabValidPayloadAndResponseParity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterLabUsesConfiguredDepartmentID(t *testing.T) {
|
||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
Success: true,
|
||||
Status: "registered",
|
||||
Message: "Registrasi laboratorium berhasil disimpan",
|
||||
}}
|
||||
handler := Handler{labRepo: repo, departmentID: "MED-PNJ-LAB"}
|
||||
body := `{
|
||||
"medrec_id":"00001234",
|
||||
"departemen_id":"DIABAIKAN",
|
||||
"dokter_id":"DR01",
|
||||
"dokter_pengirim_id":"DR02",
|
||||
"jenis_pasien_id":"0",
|
||||
"periode_tarif_id":10,
|
||||
"diagnosa_kerja":"Pemeriksaan laboratorium"
|
||||
}`
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/lab-registration", strings.NewReader(body))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterLab(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.params.DepartmentID != "MED-PNJ-LAB" {
|
||||
t.Fatalf("department ID = %q, want %q", repo.params.DepartmentID, "MED-PNJ-LAB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterLabCompanyPayload(t *testing.T) {
|
||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
|
||||
204
internal/http/registration.go
Normal file
204
internal/http/registration.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
func (h Handler) RegisterPatient(w http.ResponseWriter, r *http.Request) {
|
||||
payload, err := decodeJSONObject(r)
|
||||
if err != nil {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", "Payload harus berupa object JSON", nil)
|
||||
return
|
||||
}
|
||||
params, message := validateRegistration(payload, h.departmentID)
|
||||
if message != "" {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", message, nil)
|
||||
return
|
||||
}
|
||||
|
||||
outcome, err := h.registrationRepository(r.Context()).RegisterPatient(r.Context(), params)
|
||||
if err != nil {
|
||||
var validationErr repository.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationErr.Message, nil)
|
||||
return
|
||||
}
|
||||
writeRegistrationResponse(w, http.StatusInternalServerError, false, "error", err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeRegistrationResponse(w, outcome.HTTPStatus, outcome.Success, outcome.Status, outcome.Message, outcome.Data)
|
||||
}
|
||||
|
||||
func validateRegistration(payload map[string]any, configuredDepartment string) (repository.RegistrationParams, string) {
|
||||
medrecID, _, message := optionalString(payload, "medrec_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
nik, _, message := optionalString(payload, "nik", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
birthDateText, birthPresent, message := optionalString(payload, "tgl_lahir", 10)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if strings.TrimSpace(medrecID) == "" && (!birthPresent || strings.TrimSpace(nik) == "") {
|
||||
return repository.RegistrationParams{}, "nik dan tgl_lahir wajib diisi jika medrec_id belum tersedia"
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, err.Error()
|
||||
}
|
||||
var birthDate time.Time
|
||||
if birthPresent && strings.TrimSpace(birthDateText) != "" {
|
||||
birthDate, err = time.ParseInLocation("2006-01-02", birthDateText, jakarta)
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, "tgl_lahir harus format Y-m-d"
|
||||
}
|
||||
}
|
||||
|
||||
registrationDateText, present, message := optionalString(payload, "tanggal", 10)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
registrationDate := time.Now().In(jakarta)
|
||||
if present && strings.TrimSpace(registrationDateText) != "" {
|
||||
registrationDate, err = time.ParseInLocation("2006-01-02", registrationDateText, jakarta)
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, "tanggal harus format Y-m-d"
|
||||
}
|
||||
}
|
||||
|
||||
departmentID, present, message := optionalString(payload, "departemen_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(departmentID) == "" {
|
||||
departmentID = configuredDepartment
|
||||
}
|
||||
if strings.TrimSpace(departmentID) == "" {
|
||||
return repository.RegistrationParams{}, "departemen_id wajib diisi atau dikonfigurasi di middleware"
|
||||
}
|
||||
|
||||
doctorID, _, message := optionalString(payload, "dokter_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
patientType, message := optionalInteger(payload, "jenis_pasien_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
registrationType, message := optionalInteger(payload, "jenis_reg_id", 2)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
tariffPeriod, message := optionalInteger(payload, "periode_tarif_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
groupTariff, message := optionalInteger(payload, "group_tarif_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
|
||||
companyID, _, message := optionalString(payload, "perusahaan_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
insuranceID, _, message := optionalString(payload, "asuransi_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
providerID, _, message := optionalString(payload, "provider_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
policyNumber, _, message := optionalString(payload, "nomer_polis", 100)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
guaranteeNumber, _, message := optionalString(payload, "nomer_jaminan", 100)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
entryCode, _, message := optionalString(payload, "kode_masuk_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
classID, present, message := optionalString(payload, "kelas_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(classID) == "" {
|
||||
classID = "Default"
|
||||
}
|
||||
notes, _, message := optionalString(payload, "catatan", 500)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
channel, present, message := optionalString(payload, "channel", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(channel) == "" {
|
||||
channel = "CPONE"
|
||||
}
|
||||
referrer, _, message := optionalString(payload, "perujuk", 150)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
employeeID, _, message := optionalString(payload, "nik_karyawan", 100)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
employeeName, _, message := optionalString(payload, "nama_karyawan", 150)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
packageID, packagePresent, message := optionalString(payload, "kode_paket", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !packagePresent || strings.TrimSpace(packageID) == "" {
|
||||
packageID, _, message = optionalString(payload, "paket_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
}
|
||||
packageID = strings.ToUpper(strings.TrimSpace(packageID))
|
||||
if packageID != "" && !labIdentifier.MatchString(packageID) {
|
||||
return repository.RegistrationParams{}, "kode_paket hanya boleh berisi huruf, angka, dan tanda hubung"
|
||||
}
|
||||
|
||||
return repository.RegistrationParams{
|
||||
MedrecID: strings.TrimSpace(medrecID), NIK: normalizeIdentity(nik), BirthDate: birthDate,
|
||||
RegistrationDate: registrationDate, DepartmentID: strings.TrimSpace(departmentID),
|
||||
DoctorID: strings.TrimSpace(doctorID), PatientTypeID: patientType, RegistrationType: registrationType,
|
||||
TariffPeriodID: tariffPeriod, CompanyID: strings.TrimSpace(companyID), InsuranceID: strings.TrimSpace(insuranceID),
|
||||
GroupTariffID: groupTariff, ProviderID: strings.TrimSpace(providerID), PolicyNumber: strings.TrimSpace(policyNumber),
|
||||
GuaranteeNumber: strings.TrimSpace(guaranteeNumber), EntryCode: strings.TrimSpace(entryCode), ClassID: strings.TrimSpace(classID),
|
||||
Notes: strings.TrimSpace(notes), Referrer: strings.TrimSpace(referrer), EmployeeID: strings.TrimSpace(employeeID), EmployeeName: strings.TrimSpace(employeeName), Channel: normalizeText(channel), PackageID: packageID,
|
||||
}, ""
|
||||
}
|
||||
|
||||
func optionalInteger(payload map[string]any, key string, fallback int64) (int64, string) {
|
||||
value, present := payload[key]
|
||||
if !present || isLaravelEmpty(value) {
|
||||
return fallback, ""
|
||||
}
|
||||
number, ok := parseInteger(value)
|
||||
if !ok || number < 0 {
|
||||
return 0, key + " harus berupa angka"
|
||||
}
|
||||
return number, ""
|
||||
}
|
||||
|
||||
func writeRegistrationResponse(w http.ResponseWriter, statusCode int, success bool, status, message string, data any) {
|
||||
WriteJSON(w, statusCode, MedrecResponse{Success: success, Status: status, Message: message, Data: data})
|
||||
}
|
||||
84
internal/http/registration_test.go
Normal file
84
internal/http/registration_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
type fakeRegistrationRepository struct {
|
||||
outcome repository.RegistrationOutcome
|
||||
err error
|
||||
params repository.RegistrationParams
|
||||
}
|
||||
|
||||
func (f *fakeRegistrationRepository) RegisterPatient(_ context.Context, params repository.RegistrationParams) (repository.RegistrationOutcome, error) {
|
||||
f.params = params
|
||||
return f.outcome, f.err
|
||||
}
|
||||
|
||||
func TestRegisterPatientUsesConfiguredDepartmentAndNormalizesPayload(t *testing.T) {
|
||||
repo := &fakeRegistrationRepository{outcome: repository.RegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
Success: true,
|
||||
Status: "registered",
|
||||
Message: "Registrasi berhasil disimpan",
|
||||
}}
|
||||
handler := Handler{registrationRepo: repo, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{
|
||||
"nik":"3174-0115-0190-0001",
|
||||
"tgl_lahir":"1990-01-15",
|
||||
"tanggal":"2026-09-07",
|
||||
"dokter_id":"driqbal",
|
||||
"jenis_pasien_id":"1",
|
||||
"jenis_reg_id":2,
|
||||
"periode_tarif_id":"10",
|
||||
"perusahaan_id":"ADMEDIKAAA",
|
||||
"group_tarif_id":18,
|
||||
"kode_paket":"PN2600036",
|
||||
"kelas_id":"Default",
|
||||
"channel":"cpone"
|
||||
}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.params.DepartmentID != "08_01" || repo.params.NIK != "3174011501900001" || repo.params.RegistrationType != 2 {
|
||||
t.Fatalf("unexpected normalized params: %+v", repo.params)
|
||||
}
|
||||
if repo.params.PatientTypeID != 1 || repo.params.TariffPeriodID != 10 || repo.params.GroupTariffID != 18 || repo.params.Channel != "CPONE" {
|
||||
t.Fatalf("unexpected numeric/text params: %+v", repo.params)
|
||||
}
|
||||
if repo.params.PackageID != "PN2600036" {
|
||||
t.Fatalf("unexpected package: %+v", repo.params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterPatientRequiresIdentityWhenMedrecIsMissing(t *testing.T) {
|
||||
handler := Handler{registrationRepo: &fakeRegistrationRepository{}, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{"tanggal":"2026-09-07"}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusUnprocessableEntity || !strings.Contains(recorder.Body.String(), "nik dan tgl_lahir wajib") {
|
||||
t.Fatalf("unexpected validation response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterPatientForwardsRepositoryError(t *testing.T) {
|
||||
handler := Handler{registrationRepo: &fakeRegistrationRepository{err: errors.New("database unavailable")}, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{"medrec_id":"00001234"}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusInternalServerError || !strings.Contains(recorder.Body.String(), "database unavailable") {
|
||||
t.Fatalf("unexpected error response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
137
internal/repository/dokter.go
Normal file
137
internal/repository/dokter.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type DokterListParams struct {
|
||||
Search string
|
||||
DoctorIDs []string
|
||||
Page int
|
||||
PerPage int
|
||||
}
|
||||
|
||||
type DokterPage struct {
|
||||
Items []Dokter
|
||||
Total int
|
||||
}
|
||||
|
||||
type Dokter struct {
|
||||
KodeRS *string `json:"KodeRs"`
|
||||
NamaRS *string `json:"NamaRs"`
|
||||
DokterID string `json:"DokterID"`
|
||||
Nama *string `json:"Nama"`
|
||||
Sapaan *string `json:"Sapaan"`
|
||||
Inisial *string `json:"Inisial"`
|
||||
Gelar *string `json:"Gelar"`
|
||||
GelarBelakang *string `json:"Gelar_belakang"`
|
||||
JenisKaryawanID *string `json:"JenisKaryawanID"`
|
||||
Kelamin *string `json:"Kelamin"`
|
||||
GolonganDarah *string `json:"GolonganDarah"`
|
||||
TempatLahir *string `json:"TempatLahir"`
|
||||
TanggalLahir *string `json:"TanggalLahir"`
|
||||
NIK *string `json:"NIK"`
|
||||
Telepon *string `json:"Telepon"`
|
||||
Ponsel *string `json:"Ponsel"`
|
||||
Email *string `json:"Email"`
|
||||
Alamat *string `json:"Alamat"`
|
||||
Kota *string `json:"Kota"`
|
||||
KodePos *string `json:"KodePos"`
|
||||
Propinsi *string `json:"Propinsi"`
|
||||
Negara *string `json:"Negara"`
|
||||
TanggalMulai *string `json:"TanggalMulai"`
|
||||
Keilmuan *string `json:"Keilmuan"`
|
||||
SpesialisasiID *string `json:"SpesialisasiID"`
|
||||
KodeSpesialisasiID *string `json:"KodeSpesialisasiID"`
|
||||
KodeSubSpesialisasiID *string `json:"KodeSubSpesialisasiID"`
|
||||
Tipe *string `json:"Tipe"`
|
||||
NA *string `json:"NA"`
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) ListDokter(ctx context.Context, params DokterListParams) (DokterPage, error) {
|
||||
where, args := dokterListWhere(params)
|
||||
|
||||
var total int
|
||||
if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM dokter d "+where, args...).Scan(&total); err != nil {
|
||||
return DokterPage{}, err
|
||||
}
|
||||
|
||||
queryArgs := append(append([]any{}, args...), params.PerPage, (params.Page-1)*params.PerPage)
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT
|
||||
d.DokterID, d.Nama, d.Sapaan, d.Inisial, d.Gelar, d.Gelar_belakang,
|
||||
d.JenisKaryawanID, d.Kelamin, d.GolonganDarah, d.TempatLahir,
|
||||
d.TanggalLahir, d.NIK, d.Telepon, d.Ponsel, d.Email, d.Alamat,
|
||||
d.Kota, d.KodePos, d.Propinsi, d.Negara, d.TanggalMulai, d.Keilmuan,
|
||||
d.SpesialisasiID, d.KodeSpesialisasiID, d.KodeSubSpesialisasiID,
|
||||
d.Tipe, d.NA,
|
||||
(SELECT rs.KodeRS FROM rs ORDER BY rs.RSID LIMIT 1),
|
||||
(SELECT rs.Nama FROM rs ORDER BY rs.RSID LIMIT 1)
|
||||
FROM dokter d
|
||||
`+where+`
|
||||
ORDER BY d.Nama, d.DokterID
|
||||
LIMIT ? OFFSET ?`, queryArgs...)
|
||||
if err != nil {
|
||||
return DokterPage{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]Dokter, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanDokter(rows)
|
||||
if err != nil {
|
||||
return DokterPage{}, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return DokterPage{}, err
|
||||
}
|
||||
|
||||
return DokterPage{Items: items, Total: total}, nil
|
||||
}
|
||||
|
||||
func dokterListWhere(params DokterListParams) (string, []any) {
|
||||
conditions := []string{"d.NA = 'N'"}
|
||||
args := make([]any, 0, 2+len(params.DoctorIDs))
|
||||
|
||||
if params.Search != "" {
|
||||
conditions = append(conditions, "(d.DokterID LIKE ? OR d.Nama LIKE ?)")
|
||||
like := "%" + params.Search + "%"
|
||||
args = append(args, like, like)
|
||||
}
|
||||
if len(params.DoctorIDs) > 0 {
|
||||
placeholders := make([]string, len(params.DoctorIDs))
|
||||
for i, id := range params.DoctorIDs {
|
||||
placeholders[i] = "?"
|
||||
args = append(args, id)
|
||||
}
|
||||
conditions = append(conditions, "d.DokterID IN ("+strings.Join(placeholders, ",")+")")
|
||||
}
|
||||
|
||||
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func scanDokter(rows *sql.Rows) (Dokter, error) {
|
||||
var item Dokter
|
||||
var kodeRS, namaRS sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
&item.DokterID, &item.Nama, &item.Sapaan, &item.Inisial, &item.Gelar, &item.GelarBelakang,
|
||||
&item.JenisKaryawanID, &item.Kelamin, &item.GolonganDarah, &item.TempatLahir,
|
||||
&item.TanggalLahir, &item.NIK, &item.Telepon, &item.Ponsel, &item.Email, &item.Alamat,
|
||||
&item.Kota, &item.KodePos, &item.Propinsi, &item.Negara, &item.TanggalMulai, &item.Keilmuan,
|
||||
&item.SpesialisasiID, &item.KodeSpesialisasiID, &item.KodeSubSpesialisasiID,
|
||||
&item.Tipe, &item.NA, &kodeRS, &namaRS,
|
||||
)
|
||||
if err != nil {
|
||||
return Dokter{}, fmt.Errorf("scan dokter: %w", err)
|
||||
}
|
||||
|
||||
item.KodeRS = nullStringPtr(kodeRS)
|
||||
item.NamaRS = nullStringPtr(namaRS)
|
||||
return item, nil
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type PossibleDuplicateData struct {
|
||||
type MedrecPatient struct {
|
||||
MEDRECID string `json:"MEDRECID"`
|
||||
Nama *string `json:"Nama"`
|
||||
Kelamin *string `json:"-"`
|
||||
TanggalLahir *string `json:"TanggalLahir"`
|
||||
JenisNomerSosial *string `json:"JenisNomerSosial"`
|
||||
NomerSosial *string `json:"NomerSosial"`
|
||||
@@ -189,7 +190,7 @@ func findPossibleMedrecDuplicates(ctx context.Context, tx *sql.Tx, name string,
|
||||
}
|
||||
|
||||
const medrecPatientSelect = `
|
||||
SELECT MEDRECID, Nama, DATE_FORMAT(TanggalLahir, '%Y-%m-%d'),
|
||||
SELECT MEDRECID, Nama, DATE_FORMAT(TanggalLahir, '%Y-%m-%d'), Kelamin,
|
||||
JenisNomerSosial, NomerSosial, Telepon, Ponsel
|
||||
FROM medrec`
|
||||
|
||||
@@ -199,13 +200,14 @@ type rowScanner interface {
|
||||
|
||||
func scanMedrecPatient(row rowScanner) (MedrecPatient, error) {
|
||||
var patient MedrecPatient
|
||||
var name, birthDate, identityType, identityNumber, phone, mobile sql.NullString
|
||||
err := row.Scan(&patient.MEDRECID, &name, &birthDate, &identityType, &identityNumber, &phone, &mobile)
|
||||
var name, birthDate, gender, identityType, identityNumber, phone, mobile sql.NullString
|
||||
err := row.Scan(&patient.MEDRECID, &name, &birthDate, &gender, &identityType, &identityNumber, &phone, &mobile)
|
||||
if err != nil {
|
||||
return MedrecPatient{}, err
|
||||
}
|
||||
patient.Nama = nullStringPtr(name)
|
||||
patient.TanggalLahir = nullStringPtr(birthDate)
|
||||
patient.Kelamin = nullStringPtr(gender)
|
||||
patient.JenisNomerSosial = nullStringPtr(identityType)
|
||||
patient.NomerSosial = nullStringPtr(identityNumber)
|
||||
patient.Telepon = nullStringPtr(phone)
|
||||
|
||||
511
internal/repository/registration.go
Normal file
511
internal/repository/registration.go
Normal file
@@ -0,0 +1,511 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RegistrationParams struct {
|
||||
MedrecID string
|
||||
NIK string
|
||||
BirthDate time.Time
|
||||
RegistrationDate time.Time
|
||||
DepartmentID string
|
||||
DoctorID string
|
||||
PatientTypeID int64
|
||||
RegistrationType int64
|
||||
TariffPeriodID int64
|
||||
CompanyID string
|
||||
InsuranceID string
|
||||
GroupTariffID int64
|
||||
ProviderID string
|
||||
PolicyNumber string
|
||||
GuaranteeNumber string
|
||||
EntryCode string
|
||||
ClassID string
|
||||
Notes string
|
||||
Referrer string
|
||||
EmployeeID string
|
||||
EmployeeName string
|
||||
Channel string
|
||||
PackageID string
|
||||
}
|
||||
|
||||
type RegistrationOutcome struct {
|
||||
HTTPStatus int
|
||||
Success bool
|
||||
Status string
|
||||
Message string
|
||||
Data any
|
||||
}
|
||||
|
||||
type RegistrationData struct {
|
||||
Patient MedrecPatient `json:"patient"`
|
||||
Registration RegistrationRecord `json:"registration"`
|
||||
Package *RegistrationPackage `json:"package,omitempty"`
|
||||
}
|
||||
|
||||
type RegistrationPackage struct {
|
||||
PaketDispenserID int64 `json:"PaketDispenserID"`
|
||||
PaketID string `json:"PaketID"`
|
||||
Nama string `json:"Nama"`
|
||||
KunjunganKe int64 `json:"KunjunganKe"`
|
||||
}
|
||||
|
||||
type RegistrationRecord struct {
|
||||
RegID string `json:"RegID"`
|
||||
MEDRECID string `json:"MEDRECID"`
|
||||
Tanggal string `json:"Tanggal"`
|
||||
DepartmentID string `json:"DepartemenID"`
|
||||
DoctorID string `json:"DokterID"`
|
||||
PatientTypeID int64 `json:"JenisPasienID"`
|
||||
RegistrationID int64 `json:"JenisRegID"`
|
||||
TariffPeriodID int64 `json:"PeriodeTarifID"`
|
||||
CompanyID string `json:"PerusahaanID"`
|
||||
InsuranceID string `json:"AsuransiID"`
|
||||
ProviderID string `json:"ProviderID"`
|
||||
GroupTariffID int64 `json:"GroupTarifID"`
|
||||
PolicyNumber string `json:"NomerPolis"`
|
||||
Pulang string `json:"Pulang"`
|
||||
BatalDaftar string `json:"BatalDaftar"`
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) RegisterPatient(ctx context.Context, params RegistrationParams) (RegistrationOutcome, error) {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
outcome, err := r.registerPatientAttempt(ctx, params)
|
||||
if err == nil {
|
||||
return outcome, nil
|
||||
}
|
||||
if !isConcurrencyError(err) || ctx.Err() != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return RegistrationOutcome{}, fmt.Errorf("registrasi pasien gagal karena konflik transaksi")
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) registerPatientAttempt(ctx context.Context, params RegistrationParams) (outcome RegistrationOutcome, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
_ = tx.Rollback()
|
||||
panic(recovered)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
outcome, err = registerPatientInTransaction(ctx, tx, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params RegistrationParams) (RegistrationOutcome, error) {
|
||||
patient, found, err := findRegistrationPatient(ctx, tx, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return registrationOutcome(404, false, "patient_not_found", "Pasien tidak ditemukan", nil), nil
|
||||
}
|
||||
|
||||
existing, found, err := findExistingRegistration(ctx, tx, patient.MEDRECID, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if found {
|
||||
data := RegistrationData{Patient: patient, Registration: existing}
|
||||
if strings.TrimSpace(params.PackageID) != "" {
|
||||
data.Package, err = addRegistrationPackage(ctx, tx, existing, patient.MEDRECID, params.PackageID, params.Channel, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return registrationOutcome(200, true, "already_registered_visit", "Pasien sudah memiliki registrasi aktif pada tujuan yang sama", data), nil
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
now := time.Now().In(jakarta)
|
||||
nowText := now.Format("2006-01-02 15:04:05")
|
||||
regDate := params.RegistrationDate.Format("2006-01-02")
|
||||
prefix := registrationIDPrefix(params.RegistrationType, params.RegistrationDate)
|
||||
regID, err := nextRegistrationID(ctx, tx, prefix)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
shiftID, err := resolveDailyShiftID(ctx, tx, nil, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
var previousVisits int64
|
||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM regpas WHERE MEDRECID = ? AND NA = 'N'`, patient.MEDRECID).Scan(&previousVisits); err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO regpas SET
|
||||
RegID = ?, MEDRECID = ?, Nama = ?, ALAMATEXTPAS = ?, TELPEXTPAS = ?, TglLahir = ?, JnsKelamin = ?,
|
||||
KunjunganKe = ?, Tanggal = ?, JenisPasienID = ?, PeriodeTarifID = ?, PerusahaanID = ?,
|
||||
GroupTarifID = ?, AsuransiID = ?, ProviderID = ?, NomerPolis = ?, NIK = ?, NamaKaryawan = ?,
|
||||
NomerJaminan = ?, KodeMasukID = ?, DepartemenID = ?, DepartemenID2 = ?, JenisRegID = ?,
|
||||
Perujuk = ?, DokterID = ?, DokterID2 = '', KelasID = ?, MataUang = 'IDR', Rate = 1,
|
||||
Pulang = 'N', BolehPulang = 'N', Lunas = 'N', BatalDaftar = 'N', Catatan = '',
|
||||
CatatanMasuk = ?, ShiftHarianIDDaftar = ?, LoginBuat = ?, TanggalBuat = ?, NA = 'N'`,
|
||||
regID,
|
||||
patient.MEDRECID,
|
||||
patient.Nama,
|
||||
"",
|
||||
firstNonEmpty(pointerValue(patient.Ponsel), pointerValue(patient.Telepon)),
|
||||
pointerValue(patient.TanggalLahir),
|
||||
firstNonEmpty(pointerValue(patient.Kelamin), "L"),
|
||||
previousVisits+1,
|
||||
regDate,
|
||||
params.PatientTypeID,
|
||||
params.TariffPeriodID,
|
||||
params.CompanyID,
|
||||
params.GroupTariffID,
|
||||
params.InsuranceID,
|
||||
params.ProviderID,
|
||||
params.PolicyNumber,
|
||||
firstNonEmpty(params.EmployeeID, params.NIK, pointerValue(patient.NomerSosial)),
|
||||
params.EmployeeName,
|
||||
params.GuaranteeNumber,
|
||||
params.EntryCode,
|
||||
params.DepartmentID,
|
||||
params.DepartmentID,
|
||||
params.RegistrationType,
|
||||
params.Referrer,
|
||||
params.DoctorID,
|
||||
params.ClassID,
|
||||
params.Notes,
|
||||
shiftID,
|
||||
params.Channel,
|
||||
nowText,
|
||||
)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
registration := RegistrationRecord{
|
||||
RegID: regID, MEDRECID: patient.MEDRECID, Tanggal: regDate,
|
||||
DepartmentID: params.DepartmentID, DoctorID: params.DoctorID,
|
||||
PatientTypeID: params.PatientTypeID, RegistrationID: params.RegistrationType,
|
||||
TariffPeriodID: params.TariffPeriodID, CompanyID: params.CompanyID,
|
||||
InsuranceID: params.InsuranceID, ProviderID: params.ProviderID,
|
||||
GroupTariffID: params.GroupTariffID, PolicyNumber: params.PolicyNumber,
|
||||
Pulang: "N", BatalDaftar: "N",
|
||||
}
|
||||
data := RegistrationData{Patient: patient, Registration: registration}
|
||||
if strings.TrimSpace(params.PackageID) != "" {
|
||||
data.Package, err = addRegistrationPackage(ctx, tx, registration, patient.MEDRECID, params.PackageID, params.Channel, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return registrationOutcome(201, true, "registered", "Registrasi berhasil disimpan", data), nil
|
||||
}
|
||||
|
||||
func addRegistrationPackage(ctx context.Context, tx *sql.Tx, registration RegistrationRecord, medrecID, packageID, channel string, registrationDate time.Time) (*RegistrationPackage, error) {
|
||||
packageID = strings.ToUpper(strings.TrimSpace(packageID))
|
||||
header, found, err := findActiveOutpatientPackage(ctx, tx, packageID, registrationDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, ValidationError{Message: "Paket dispenser MCU tidak ditemukan, belum disetujui, atau sudah tidak berlaku"}
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now().In(jakarta).Format("2006-01-02 15:04:05")
|
||||
if registration.RegistrationID == 2 {
|
||||
if _, err := ensureMCUTransaction(ctx, tx, registration, channel, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
dbRegistration := dbLabRegistration{RegID: registration.RegID, MEDRECID: medrecID}
|
||||
dispenserID, visitNumber, err := ensurePackageDispenser(ctx, tx, dbRegistration, header, channel, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if registration.RegistrationID == 2 {
|
||||
if err := ensureMCUPackageDetails(ctx, tx, dispenserID, registration.RegID, medrecID, header.ID, channel, now); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &RegistrationPackage{
|
||||
PaketDispenserID: dispenserID,
|
||||
PaketID: header.ID,
|
||||
Nama: header.Name,
|
||||
KunjunganKe: visitNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureMCUTransaction(ctx context.Context, tx *sql.Tx, registration RegistrationRecord, channel, now string) (int64, error) {
|
||||
var transactionID int64
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TrxDepartemenID
|
||||
FROM trxdepartemen
|
||||
WHERE RegID = ? AND DepartemenID = ? AND DokterID = ? AND NA = 'N'
|
||||
ORDER BY TrxDepartemenID DESC LIMIT 1 FOR UPDATE`, registration.RegID, registration.DepartmentID, registration.DoctorID).Scan(&transactionID)
|
||||
if err == nil {
|
||||
return transactionID, nil
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO trxdepartemen (
|
||||
RegID, TrxID, DepartemenID, DokterID, Dokter2, AntrianID, Tanggal,
|
||||
KelasID, MataUang, StatusID, LogID, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, '', 0, ?, ?, 'IDR', 0, 0, ?, ?, 'N')`,
|
||||
registration.RegID, registration.RegID, registration.DepartmentID, registration.DoctorID,
|
||||
now, "Default", channel, now,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
type registrationPackageItem struct {
|
||||
PackageDepartmentID sql.NullInt64
|
||||
ItemDepartmentID sql.NullInt64
|
||||
ItemWarehouseID sql.NullInt64
|
||||
ItemID sql.NullString
|
||||
HeaderID sql.NullInt64
|
||||
DepartmentID sql.NullString
|
||||
UnitID sql.NullString
|
||||
BaseUnitID sql.NullString
|
||||
Quantity sql.NullString
|
||||
Price sql.NullString
|
||||
DiscountType sql.NullString
|
||||
Discount sql.NullString
|
||||
}
|
||||
|
||||
func ensureMCUPackageDetails(ctx context.Context, tx *sql.Tx, dispenserID int64, regID, medrecID, packageID, channel, now string) error {
|
||||
services, err := findActivePackageServices(ctx, tx, packageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, service := range services {
|
||||
var present int
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM paket_dispenser_layanan
|
||||
WHERE PaketDispenserID = ? AND LayananID = ? AND NA = 'N'
|
||||
)`, dispenserID, service.ServiceID).Scan(&present)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if present != 0 {
|
||||
continue
|
||||
}
|
||||
operationID, err := resolvePackageOperationIDForMCU(ctx, tx, service.HeaderService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO paket_dispenser_layanan (
|
||||
PaketDispenserID, PaketDispenserOperasiID, RegID, MEDRECID, PaketID,
|
||||
DepartemenID, LayananID, HeaderLayanan, JumlahAwal, Jumlah, JumlahHari,
|
||||
Harga, TipeDiskon, Diskon, ShareTipe, ShareRS, ShareDokter, ShareExternal,
|
||||
ShareLain, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`,
|
||||
dispenserID, operationID, regID, medrecID, packageID, service.DepartmentID, service.ServiceID,
|
||||
nullStringValue(service.HeaderService), nullStringValue(service.Quantity), nullStringValue(service.Quantity),
|
||||
nullInt64Value(service.Days), nullStringValue(service.Price), nullStringValue(service.DiscountType),
|
||||
nullStringValue(service.Discount), nullStringValue(service.ShareType), nullStringValue(service.ShareHospital),
|
||||
nullStringValue(service.ShareDoctor), nullStringValue(service.ShareExternal), nullStringValue(service.ShareOther),
|
||||
channel, now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT PaketDepartemenID, ItemDepartemenID, ItemGudangID, ItemID, HeaderID,
|
||||
DepartemenID, SatuanID, SatuanDasarID, Jumlah, Harga, TipeDiskon, Diskon
|
||||
FROM paket_item WHERE PaketID = ? AND NA = 'N' ORDER BY PaketItemID`, packageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]registrationPackageItem, 0)
|
||||
for rows.Next() {
|
||||
var item registrationPackageItem
|
||||
if err := rows.Scan(&item.PackageDepartmentID, &item.ItemDepartmentID, &item.ItemWarehouseID, &item.ItemID, &item.HeaderID, &item.DepartmentID, &item.UnitID, &item.BaseUnitID, &item.Quantity, &item.Price, &item.DiscountType, &item.Discount); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
rows.Close()
|
||||
for _, item := range items {
|
||||
var present int
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM paket_dispenser_item WHERE PaketDispenserID = ? AND ItemID = ? AND HeaderID = ? AND NA = 'N')`, dispenserID, nullStringValue(item.ItemID), nullInt64Value(item.HeaderID)).Scan(&present); err != nil {
|
||||
return err
|
||||
}
|
||||
if present != 0 {
|
||||
continue
|
||||
}
|
||||
operationID, err := resolvePackageOperationIDForMCU(ctx, tx, nullStringFromInt(item.HeaderID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO paket_dispenser_item (
|
||||
PaketDispenserID, PaketDispenserOperasiID, RegID, MEDRECID, PaketID,
|
||||
PaketDepartemenID, ItemDepartemenID, ItemGudangID, ItemID, DepartemenID,
|
||||
HeaderID, SatuanID, SatuanDasarID, JumlahAwal, Jumlah, Harga, TipeDiskon,
|
||||
Diskon, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`,
|
||||
dispenserID, operationID, regID, medrecID, packageID, nullInt64Value(item.PackageDepartmentID),
|
||||
nullInt64Value(item.ItemDepartmentID), nullInt64Value(item.ItemWarehouseID), nullStringValue(item.ItemID),
|
||||
nullStringValue(item.DepartmentID), nullInt64Value(item.HeaderID), nullStringValue(item.UnitID),
|
||||
nullStringValue(item.BaseUnitID), nullStringValue(item.Quantity), nullStringValue(item.Quantity),
|
||||
nullStringValue(item.Price), nullStringValue(item.DiscountType), nullStringValue(item.Discount), channel, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO paket_dispenser_adm (
|
||||
PaketAdmID, PaketDispenserID, RegID, PaketID, TipeRI, Hitung, AdmRI,
|
||||
AdmRIBaru, TipeDiskonRI, DiskonRI, LoginBuat, TanggalBuat, NA
|
||||
)
|
||||
SELECT pa.PaketAdmID, ?, ?, pa.PaketID, pa.TipeRI, 'Y', pa.AdmRI,
|
||||
pa.AdmRIBaru, pa.TipeDiskonRI, pa.DiskonRI, ?, ?, 'N'
|
||||
FROM paket_adm pa
|
||||
WHERE pa.PaketID = ? AND pa.NA = 'N'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM paket_dispenser_adm da
|
||||
WHERE da.PaketDispenserID = ? AND da.PaketAdmID = pa.PaketAdmID AND da.NA = 'N'
|
||||
)
|
||||
LIMIT 1`, dispenserID, regID, channel, now, packageID, dispenserID)
|
||||
return err
|
||||
}
|
||||
|
||||
func resolvePackageOperationIDForMCU(ctx context.Context, tx *sql.Tx, header sql.NullString) (int64, error) {
|
||||
if !header.Valid || strings.TrimSpace(header.String) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var operationID sql.NullInt64
|
||||
err := tx.QueryRowContext(ctx, `SELECT PaketDispenserOperasiID FROM paket_dispenser_operasi_header WHERE PaketOperasiHeaderID = ? LIMIT 1`, header.String).Scan(&operationID)
|
||||
if errors.Is(err, sql.ErrNoRows) || !operationID.Valid {
|
||||
return 0, nil
|
||||
}
|
||||
return operationID.Int64, err
|
||||
}
|
||||
|
||||
func nullStringFromInt(value sql.NullInt64) sql.NullString {
|
||||
if !value.Valid {
|
||||
return sql.NullString{}
|
||||
}
|
||||
return sql.NullString{String: strconv.FormatInt(value.Int64, 10), Valid: true}
|
||||
}
|
||||
|
||||
func findRegistrationPatient(ctx context.Context, tx *sql.Tx, params RegistrationParams) (MedrecPatient, bool, error) {
|
||||
query := medrecPatientSelect + ` WHERE NA = 'N' AND MEDRECID = ? LIMIT 1 FOR UPDATE`
|
||||
args := []any{params.MedrecID}
|
||||
if strings.TrimSpace(params.MedrecID) == "" {
|
||||
query = medrecPatientSelect + ` WHERE NA = 'N' AND JenisNomerSosial = 'KTP' AND NomerSosial = ? AND DATE(TanggalLahir) = ? LIMIT 1 FOR UPDATE`
|
||||
args = []any{params.NIK, params.BirthDate.Format("2006-01-02")}
|
||||
}
|
||||
patient, err := scanMedrecPatient(tx.QueryRowContext(ctx, query, args...))
|
||||
if err == sql.ErrNoRows {
|
||||
return MedrecPatient{}, false, nil
|
||||
}
|
||||
return patient, err == nil, err
|
||||
}
|
||||
|
||||
func findExistingRegistration(ctx context.Context, tx *sql.Tx, medrecID string, params RegistrationParams) (RegistrationRecord, bool, error) {
|
||||
query := `
|
||||
SELECT RegID, MEDRECID, DATE_FORMAT(Tanggal, '%Y-%m-%d'), DepartemenID, DokterID,
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID, PerusahaanID, AsuransiID,
|
||||
ProviderID, GroupTarifID, NomerPolis, Pulang, BatalDaftar
|
||||
FROM regpas
|
||||
WHERE MEDRECID = ? AND DATE(Tanggal) = ? AND DepartemenID = ?
|
||||
AND Pulang = 'N' AND (BatalDaftar IS NULL OR BatalDaftar IN ('N', '')) AND NA = 'N'`
|
||||
args := []any{medrecID, params.RegistrationDate.Format("2006-01-02"), params.DepartmentID}
|
||||
if params.DoctorID != "" {
|
||||
query += ` AND DokterID = ?`
|
||||
args = append(args, params.DoctorID)
|
||||
}
|
||||
query += ` ORDER BY RegID DESC LIMIT 1 FOR UPDATE`
|
||||
|
||||
var registration RegistrationRecord
|
||||
err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
®istration.RegID, ®istration.MEDRECID, ®istration.Tanggal, ®istration.DepartmentID,
|
||||
®istration.DoctorID, ®istration.PatientTypeID, ®istration.RegistrationID,
|
||||
®istration.TariffPeriodID, ®istration.CompanyID, ®istration.InsuranceID,
|
||||
®istration.ProviderID, ®istration.GroupTariffID, ®istration.PolicyNumber,
|
||||
®istration.Pulang, ®istration.BatalDaftar,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return RegistrationRecord{}, false, nil
|
||||
}
|
||||
return registration, err == nil, err
|
||||
}
|
||||
|
||||
func nextRegistrationID(ctx context.Context, tx *sql.Tx, prefix string) (string, error) {
|
||||
var lastID string
|
||||
err := tx.QueryRowContext(ctx, `SELECT RegID FROM regpas WHERE RegID LIKE ? ORDER BY RegID DESC LIMIT 1 FOR UPDATE`, prefix+"%").Scan(&lastID)
|
||||
if err == sql.ErrNoRows {
|
||||
return prefix + "00001", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
number, err := strconv.Atoi(strings.TrimPrefix(lastID, prefix))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s%05d", prefix, number+1), nil
|
||||
}
|
||||
|
||||
func registrationIDPrefix(registrationType int64, registrationDate time.Time) string {
|
||||
prefix := "P"
|
||||
if registrationType == 2 {
|
||||
prefix = "MCU"
|
||||
}
|
||||
return prefix + registrationDate.Format("0601")
|
||||
}
|
||||
|
||||
func registrationOutcome(statusCode int, success bool, status, message string, data any) RegistrationOutcome {
|
||||
return RegistrationOutcome{HTTPStatus: statusCode, Success: success, Status: status, Message: message, Data: data}
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
17
internal/repository/registration_prefix_test.go
Normal file
17
internal/repository/registration_prefix_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRegistrationIDPrefix(t *testing.T) {
|
||||
registrationDate := time.Date(2026, time.September, 7, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
if got, want := registrationIDPrefix(2, registrationDate), "MCU2609"; got != want {
|
||||
t.Fatalf("MCU prefix = %q; want %q", got, want)
|
||||
}
|
||||
if got, want := registrationIDPrefix(1, registrationDate), "P2609"; got != want {
|
||||
t.Fatalf("regular prefix = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user