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_HOST=0.0.0.0
|
||||||
APP_PORT=8080
|
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_HOST=127.0.0.1
|
||||||
CPONE_DB_PORT=3306
|
CPONE_DB_PORT=3306
|
||||||
CPONE_DB_DATABASE=cpone_middleware
|
CPONE_DB_DATABASE=nama_database_his
|
||||||
CPONE_DB_USERNAME=root
|
CPONE_DB_USERNAME=cpone
|
||||||
CPONE_DB_PASSWORD=
|
CPONE_DB_PASSWORD=
|
||||||
|
|
||||||
DB_MAX_OPEN_CONNS=25
|
DB_MAX_OPEN_CONNS=25
|
||||||
@@ -13,5 +19,3 @@ DB_MAX_IDLE_CONNS=25
|
|||||||
DB_CONN_MAX_LIFETIME_MINUTES=5
|
DB_CONN_MAX_LIFETIME_MINUTES=5
|
||||||
|
|
||||||
CPONE_BEARER_TOKEN=replace-with-a-secure-token
|
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>
|
Authorization: Bearer <your-secure-token>
|
||||||
X-RS-Code: <kode-rs>
|
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
|
GET /api/cpone/tarif-kerjasama?search=&perusahaan_id=&tarif_id=&page=1&per_page=15
|
||||||
Authorization: Bearer <your-secure-token>
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
@@ -64,7 +72,6 @@ Contoh payload pembuatan pasien HIS:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"rs": "PRIMAYA",
|
|
||||||
"nama": "BUDI SANTOS",
|
"nama": "BUDI SANTOS",
|
||||||
"jenis_kelamin": "L",
|
"jenis_kelamin": "L",
|
||||||
"tgl_lahir": "1990-01-15",
|
"tgl_lahir": "1990-01-15",
|
||||||
@@ -91,6 +98,40 @@ Contoh payload pembuatan pasien HIS:
|
|||||||
Endpoint mengembalikan `created`, `already_exists`, atau `possible_duplicate`
|
Endpoint mengembalikan `created`, `already_exists`, atau `possible_duplicate`
|
||||||
sesuai hasil pemeriksaan identitas dan kemiripan data pasien di HIS.
|
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:
|
Contoh payload registrasi laboratorium:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -154,89 +195,45 @@ bersifat idempoten dan tidak membuat transaksi ganda.
|
|||||||
|
|
||||||
## Config
|
## 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
|
```text
|
||||||
APP_HOST=0.0.0.0
|
APP_HOST=0.0.0.0
|
||||||
APP_PORT=8080
|
APP_PORT=8080
|
||||||
|
CPONE_RS_CODE=AWALBROS_DEV
|
||||||
|
CPONE_DEPARTEMEN_ID=08_01
|
||||||
CPONE_DB_HOST=127.0.0.1
|
CPONE_DB_HOST=127.0.0.1
|
||||||
CPONE_DB_PORT=3306
|
CPONE_DB_PORT=3306
|
||||||
CPONE_DB_DATABASE=cpone_middleware
|
CPONE_DB_DATABASE=nama_database_his
|
||||||
CPONE_DB_USERNAME=root
|
CPONE_DB_USERNAME=cpone
|
||||||
CPONE_DB_PASSWORD=
|
CPONE_DB_PASSWORD=
|
||||||
CPONE_BEARER_TOKEN=replace-with-a-secure-token
|
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
|
`CPONE_DEPARTEMEN_ID` adalah `DepartemenID` HIS yang digunakan untuk registrasi
|
||||||
sebelum disimpan; bila `Ping` gagal, setting tidak disimpan.
|
laboratorium. Nilai ini diambil dari konfigurasi, sehingga tidak perlu dikirim
|
||||||
|
di payload endpoint registrasi.
|
||||||
|
|
||||||
```http
|
Semua endpoint bisnis tetap membutuhkan header:
|
||||||
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:
|
|
||||||
|
|
||||||
```http
|
```http
|
||||||
X-RS-Code: PRIMAYA_BEKASI_BARAT
|
X-RS-Code: PRIMAYA_BEKASI_BARAT
|
||||||
```
|
```
|
||||||
|
|
||||||
Sebagai alternatif dapat memakai query `?kode_rs=PRIMAYA_BEKASI_BARAT`.
|
Sebagai alternatif dapat memakai query `?kode_rs=AWALBROS_DEV`. Header memiliki
|
||||||
Header memiliki prioritas lebih tinggi daripada query. Response juga memuat
|
prioritas lebih tinggi daripada query. Kode tersebut hanya divalidasi terhadap
|
||||||
header `X-RS-Code` agar koneksi yang terpilih mudah diaudit. Salah satu dari
|
`CPONE_RS_CODE`; kode tidak digunakan untuk memilih database.
|
||||||
header atau query tersebut wajib dikirim; jika tidak ada, API mengembalikan
|
|
||||||
status `422` dan tidak mengakses database HIS.
|
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
|
## Run
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer resolver.Close()
|
defer resolver.Close()
|
||||||
|
|
||||||
handler := apphttp.NewDynamicHandler(cfg.RSCode)
|
handler := apphttp.NewDynamicHandler(cfg.RSCode, cfg.DepartmentID)
|
||||||
protected := func(handler http.Handler) http.Handler {
|
protected := func(handler http.Handler) http.Handler {
|
||||||
return apphttp.BearerAuth(cfg.BearerToken, apphttp.SelectDatabase(resolver, 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", protected(http.HandlerFunc(handler.ListPerusahaan)))
|
||||||
mux.Handle("GET /api/cpone/perusahaan/{id}", protected(http.HandlerFunc(handler.GetPerusahaan)))
|
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/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", protected(http.HandlerFunc(handler.ListTarifKerjasama)))
|
||||||
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", protected(http.HandlerFunc(handler.GetTarifKerjasama)))
|
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", protected(http.HandlerFunc(handler.GetTarifKerjasama)))
|
||||||
mux.Handle("GET /api/cpone/periode-tarif", protected(http.HandlerFunc(handler.ListPeriodeTarif)))
|
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/periode-tarif/{periodeTarifID}", protected(http.HandlerFunc(handler.ListPeriodeTarifLayanan)))
|
||||||
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
|
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
|
||||||
mux.Handle("POST /api/cpone/patients/medrec", protected(http.HandlerFunc(handler.StoreMedrec)))
|
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", 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}/services", protected(http.HandlerFunc(handler.AddLabServices)))
|
||||||
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/packages", protected(http.HandlerFunc(handler.AddLabPackage)))
|
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/packages", protected(http.HandlerFunc(handler.AddLabPackage)))
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Config struct {
|
|||||||
AppHost string
|
AppHost string
|
||||||
AppPort string
|
AppPort string
|
||||||
RSCode string
|
RSCode string
|
||||||
|
DepartmentID string
|
||||||
BearerToken string
|
BearerToken string
|
||||||
DBMaxOpenConns int
|
DBMaxOpenConns int
|
||||||
DBMaxIdleConns int
|
DBMaxIdleConns int
|
||||||
@@ -33,6 +34,7 @@ func Load(path string) (Config, error) {
|
|||||||
AppHost: env("APP_HOST", "0.0.0.0"),
|
AppHost: env("APP_HOST", "0.0.0.0"),
|
||||||
AppPort: env("APP_PORT", "8080"),
|
AppPort: env("APP_PORT", "8080"),
|
||||||
RSCode: env("CPONE_RS_CODE", ""),
|
RSCode: env("CPONE_RS_CODE", ""),
|
||||||
|
DepartmentID: env("CPONE_DEPARTEMEN_ID", ""),
|
||||||
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
||||||
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
||||||
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
||||||
@@ -47,6 +49,9 @@ func Load(path string) (Config, error) {
|
|||||||
if cfg.RSCode == "" {
|
if cfg.RSCode == "" {
|
||||||
return Config{}, fmt.Errorf("CPONE_RS_CODE wajib diisi")
|
return Config{}, fmt.Errorf("CPONE_RS_CODE wajib diisi")
|
||||||
}
|
}
|
||||||
|
if cfg.DepartmentID == "" {
|
||||||
|
return Config{}, fmt.Errorf("CPONE_DEPARTEMEN_ID wajib diisi")
|
||||||
|
}
|
||||||
if cfg.BearerToken == "" {
|
if cfg.BearerToken == "" {
|
||||||
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
||||||
}
|
}
|
||||||
|
|||||||
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)
|
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 {
|
type LabResultRepository interface {
|
||||||
GetLabResult(ctx context.Context, labNumber string) (repository.LabResultOutcome, error)
|
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)
|
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 {
|
type Handler struct {
|
||||||
layananRepo LayananRepository
|
layananRepo LayananRepository
|
||||||
labRepo LabRegistrationRepository
|
labRepo LabRegistrationRepository
|
||||||
patientRepo PatientRepository
|
patientRepo PatientRepository
|
||||||
|
registrationRepo RegistrationRepository
|
||||||
labResultRepo LabResultRepository
|
labResultRepo LabResultRepository
|
||||||
paketRepo PaketRepository
|
paketRepo PaketRepository
|
||||||
tarifRepo TarifKerjasamaRepository
|
tarifRepo TarifKerjasamaRepository
|
||||||
periodeTarifRepo PeriodeTarifRepository
|
periodeTarifRepo PeriodeTarifRepository
|
||||||
periodeTarifDetailRepo PeriodeTarifDetailRepository
|
periodeTarifDetailRepo PeriodeTarifDetailRepository
|
||||||
perusahaanRepo PerusahaanRepository
|
perusahaanRepo PerusahaanRepository
|
||||||
|
dokterRepo DokterRepository
|
||||||
hospitalCode string
|
hospitalCode string
|
||||||
|
departmentID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type CPONERepository interface {
|
type CPONERepository interface {
|
||||||
LayananRepository
|
LayananRepository
|
||||||
LabRegistrationRepository
|
LabRegistrationRepository
|
||||||
PatientRepository
|
PatientRepository
|
||||||
|
RegistrationRepository
|
||||||
LabResultRepository
|
LabResultRepository
|
||||||
PaketRepository
|
PaketRepository
|
||||||
TarifKerjasamaRepository
|
TarifKerjasamaRepository
|
||||||
PeriodeTarifRepository
|
PeriodeTarifRepository
|
||||||
PeriodeTarifDetailRepository
|
PeriodeTarifDetailRepository
|
||||||
PerusahaanRepository
|
PerusahaanRepository
|
||||||
|
DokterRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(repo CPONERepository) Handler {
|
func NewHandler(repo CPONERepository) Handler {
|
||||||
return Handler{
|
return Handler{
|
||||||
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
layananRepo: repo, labRepo: repo, patientRepo: repo, registrationRepo: repo,
|
||||||
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
||||||
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
|
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
|
||||||
|
dokterRepo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDynamicHandler(hospitalCode string) Handler {
|
func NewDynamicHandler(hospitalCode string, departmentID string) Handler {
|
||||||
return Handler{hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode))}
|
return Handler{
|
||||||
|
hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode)),
|
||||||
|
departmentID: strings.TrimSpace(departmentID),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
||||||
@@ -113,6 +130,13 @@ func (h Handler) patientRepository(ctx context.Context) PatientRepository {
|
|||||||
return h.patientRepo
|
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 {
|
func (h Handler) labResultRepository(ctx context.Context) LabResultRepository {
|
||||||
if repo, ok := selectedRepository(ctx); ok {
|
if repo, ok := selectedRepository(ctx); ok {
|
||||||
return repo
|
return repo
|
||||||
@@ -155,6 +179,13 @@ func (h Handler) perusahaanRepository(ctx context.Context) PerusahaanRepository
|
|||||||
return h.perusahaanRepo
|
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) {
|
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
|
||||||
layananIDs := parseLayananIDs(r)
|
layananIDs := parseLayananIDs(r)
|
||||||
if len(layananIDs) == 0 {
|
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)
|
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", err.Error(), nil)
|
||||||
return
|
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)
|
validated, validationMessage := validateLabRegistration(payload)
|
||||||
if validationMessage != "" {
|
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) {
|
func TestRegisterLabCompanyPayload(t *testing.T) {
|
||||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||||
HTTPStatus: http.StatusCreated,
|
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 {
|
type MedrecPatient struct {
|
||||||
MEDRECID string `json:"MEDRECID"`
|
MEDRECID string `json:"MEDRECID"`
|
||||||
Nama *string `json:"Nama"`
|
Nama *string `json:"Nama"`
|
||||||
|
Kelamin *string `json:"-"`
|
||||||
TanggalLahir *string `json:"TanggalLahir"`
|
TanggalLahir *string `json:"TanggalLahir"`
|
||||||
JenisNomerSosial *string `json:"JenisNomerSosial"`
|
JenisNomerSosial *string `json:"JenisNomerSosial"`
|
||||||
NomerSosial *string `json:"NomerSosial"`
|
NomerSosial *string `json:"NomerSosial"`
|
||||||
@@ -189,7 +190,7 @@ func findPossibleMedrecDuplicates(ctx context.Context, tx *sql.Tx, name string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const medrecPatientSelect = `
|
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
|
JenisNomerSosial, NomerSosial, Telepon, Ponsel
|
||||||
FROM medrec`
|
FROM medrec`
|
||||||
|
|
||||||
@@ -199,13 +200,14 @@ type rowScanner interface {
|
|||||||
|
|
||||||
func scanMedrecPatient(row rowScanner) (MedrecPatient, error) {
|
func scanMedrecPatient(row rowScanner) (MedrecPatient, error) {
|
||||||
var patient MedrecPatient
|
var patient MedrecPatient
|
||||||
var name, birthDate, identityType, identityNumber, phone, mobile sql.NullString
|
var name, birthDate, gender, identityType, identityNumber, phone, mobile sql.NullString
|
||||||
err := row.Scan(&patient.MEDRECID, &name, &birthDate, &identityType, &identityNumber, &phone, &mobile)
|
err := row.Scan(&patient.MEDRECID, &name, &birthDate, &gender, &identityType, &identityNumber, &phone, &mobile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MedrecPatient{}, err
|
return MedrecPatient{}, err
|
||||||
}
|
}
|
||||||
patient.Nama = nullStringPtr(name)
|
patient.Nama = nullStringPtr(name)
|
||||||
patient.TanggalLahir = nullStringPtr(birthDate)
|
patient.TanggalLahir = nullStringPtr(birthDate)
|
||||||
|
patient.Kelamin = nullStringPtr(gender)
|
||||||
patient.JenisNomerSosial = nullStringPtr(identityType)
|
patient.JenisNomerSosial = nullStringPtr(identityType)
|
||||||
patient.NomerSosial = nullStringPtr(identityNumber)
|
patient.NomerSosial = nullStringPtr(identityNumber)
|
||||||
patient.Telepon = nullStringPtr(phone)
|
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