feat: add lab package registration endpoint
This commit is contained in:
20
README.md
20
README.md
@@ -24,6 +24,10 @@ POST /api/cpone/patients/lab-registration/{regId}/services
|
||||
Authorization: Bearer <your-secure-token>
|
||||
Content-Type: application/json
|
||||
|
||||
POST /api/cpone/patients/lab-registration/{regId}/packages
|
||||
Authorization: Bearer <your-secure-token>
|
||||
Content-Type: application/json
|
||||
|
||||
POST /api/cpone/patients/medrec
|
||||
Authorization: Bearer <your-secure-token>
|
||||
Content-Type: application/json
|
||||
@@ -107,6 +111,22 @@ Harga, diskon publik, share, unit cost, layanan multiple, dan spesimen diambil
|
||||
langsung dari master HIS. Layanan yang sudah ada akan dilewati dan dilaporkan
|
||||
melalui `skipped_layanan_ids`.
|
||||
|
||||
Contoh payload penambahan Paket Dispenser HIS:
|
||||
|
||||
```json
|
||||
{
|
||||
"paket_id": "DISP-260800011",
|
||||
"cito": false,
|
||||
"markup_cito": 0,
|
||||
"catatan": "",
|
||||
"channel": "CPONE"
|
||||
}
|
||||
```
|
||||
|
||||
Paket harus aktif, disetujui, berlaku untuk rawat jalan, dan belum memiliki
|
||||
layanan yang sama pada registrasi. Request ulang untuk paket yang sudah masuk
|
||||
bersifat idempoten dan tidak membuat transaksi ganda.
|
||||
|
||||
## Config
|
||||
|
||||
Copy `.env.example` menjadi `.env`, lalu isi koneksi MySQL:
|
||||
|
||||
@@ -52,6 +52,7 @@ func main() {
|
||||
mux.Handle("POST /api/cpone/patients/medrec", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.StoreMedrec)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.RegisterLab)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/services", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.AddLabServices)))
|
||||
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/packages", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.AddLabPackage)))
|
||||
mux.Handle("GET /api/cpone/patients/lab-results/{labNumber}", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.GetLabResult)))
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -19,6 +19,7 @@ type LayananRepository interface {
|
||||
type LabRegistrationRepository interface {
|
||||
RegisterLab(ctx context.Context, params repository.LabRegistrationParams) (repository.LabRegistrationOutcome, error)
|
||||
AddLabServices(ctx context.Context, regID string, params repository.AddLabServicesParams) (repository.LabRegistrationOutcome, error)
|
||||
AddLabPackage(ctx context.Context, regID string, params repository.AddLabPackageParams) (repository.LabRegistrationOutcome, error)
|
||||
}
|
||||
|
||||
type PatientRepository interface {
|
||||
|
||||
84
internal/http/lab_package.go
Normal file
84
internal/http/lab_package.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
var labIdentifier = regexp.MustCompile(`^[A-Za-z0-9-]+$`)
|
||||
|
||||
func (h Handler) AddLabPackage(w http.ResponseWriter, r *http.Request) {
|
||||
regID := strings.TrimSpace(r.PathValue("regId"))
|
||||
if regID == "" || utf8.RuneCountInString(regID) > 50 || !labIdentifier.MatchString(regID) {
|
||||
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", "Nomor registrasi laboratorium hanya boleh berisi huruf, angka, dan tanda hubung", nil)
|
||||
return
|
||||
}
|
||||
payload, _ := decodeJSONObject(r)
|
||||
params, message := validateAddLabPackage(payload)
|
||||
if message != "" {
|
||||
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", message, nil)
|
||||
return
|
||||
}
|
||||
outcome, err := h.labRepo.AddLabPackage(r.Context(), strings.ToUpper(regID), params)
|
||||
if err != nil {
|
||||
if validationMessage, ok := asValidationError(err); ok {
|
||||
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationMessage, nil)
|
||||
return
|
||||
}
|
||||
writeLabResponse(w, http.StatusInternalServerError, false, "error", err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeLabResponse(w, outcome.HTTPStatus, outcome.Success, outcome.Status, outcome.Message, outcome.Data)
|
||||
}
|
||||
|
||||
func validateAddLabPackage(payload map[string]any) (repository.AddLabPackageParams, string) {
|
||||
packageID, message := requiredString(payload, "paket_id", 50, "Paket ID wajib diisi")
|
||||
if message != "" {
|
||||
return repository.AddLabPackageParams{}, message
|
||||
}
|
||||
packageID = strings.ToUpper(strings.TrimSpace(packageID))
|
||||
if !labIdentifier.MatchString(packageID) {
|
||||
return repository.AddLabPackageParams{}, "Paket ID hanya boleh berisi huruf, angka, dan tanda hubung"
|
||||
}
|
||||
cito := false
|
||||
if value, present := nullableValue(payload, "cito"); present {
|
||||
var valid bool
|
||||
cito, valid = parseLaravelBoolean(value)
|
||||
if !valid {
|
||||
return repository.AddLabPackageParams{}, "The cito field must be true or false."
|
||||
}
|
||||
}
|
||||
markup := 0.0
|
||||
if value, present := nullableValue(payload, "markup_cito"); present {
|
||||
var valid bool
|
||||
markup, valid = parseNumeric(value)
|
||||
if !valid {
|
||||
return repository.AddLabPackageParams{}, "The markup cito must be a number."
|
||||
}
|
||||
if markup < 0 || markup > 100 {
|
||||
return repository.AddLabPackageParams{}, "The markup cito must be between 0 and 100."
|
||||
}
|
||||
}
|
||||
if !cito {
|
||||
markup = 0
|
||||
}
|
||||
notes, _, message := optionalString(payload, "catatan", 500)
|
||||
if message != "" {
|
||||
return repository.AddLabPackageParams{}, message
|
||||
}
|
||||
channel, present, message := optionalString(payload, "channel", 50)
|
||||
if message != "" {
|
||||
return repository.AddLabPackageParams{}, message
|
||||
}
|
||||
if !present {
|
||||
channel = "CPONE"
|
||||
}
|
||||
return repository.AddLabPackageParams{
|
||||
PackageID: packageID, Cito: cito, MarkupCito: markup,
|
||||
Notes: strings.TrimSpace(notes), Channel: strings.ToUpper(strings.TrimSpace(channel)),
|
||||
}, ""
|
||||
}
|
||||
50
internal/http/lab_package_test.go
Normal file
50
internal/http/lab_package_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
func TestAddLabPackageValidPayload(t *testing.T) {
|
||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||
HTTPStatus: 201, Success: true, Status: "package_added",
|
||||
Message: "Paket dispenser berhasil ditambahkan ke registrasi laboratorium",
|
||||
}}
|
||||
handler := Handler{labRepo: repo}
|
||||
request := httptest.NewRequest(http.MethodPost, "/packages", strings.NewReader(`{"paket_id":"disp-260800011","cito":true,"markup_cito":"10","catatan":" test ","channel":"cpone"}`))
|
||||
request.SetPathValue("regId", "lab-2608210003")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.AddLabPackage(recorder, request)
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.regID != "LAB-2608210003" || repo.packageParams.PackageID != "DISP-260800011" || !repo.packageParams.Cito || repo.packageParams.MarkupCito != 10 || repo.packageParams.Notes != "test" || repo.packageParams.Channel != "CPONE" {
|
||||
t.Fatalf("unexpected repository call: reg=%q params=%+v", repo.regID, repo.packageParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddLabPackageValidation(t *testing.T) {
|
||||
tests := []struct{ name, regID, body, message string }{
|
||||
{"invalid registration", "LAB/1", `{"paket_id":"DISP-1"}`, "Nomor registrasi laboratorium hanya boleh berisi huruf, angka, dan tanda hubung"},
|
||||
{"missing package", "LAB-1", `{}`, "Paket ID wajib diisi"},
|
||||
{"invalid package", "LAB-1", `{"paket_id":"DISP/1"}`, "Paket ID hanya boleh berisi huruf, angka, dan tanda hubung"},
|
||||
{"invalid cito", "LAB-1", `{"paket_id":"DISP-1","cito":"true"}`, "The cito field must be true or false."},
|
||||
{"invalid markup", "LAB-1", `{"paket_id":"DISP-1","markup_cito":101}`, "The markup cito must be between 0 and 100."},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
handler := Handler{labRepo: &fakeLabRegistrationRepository{}}
|
||||
request := httptest.NewRequest(http.MethodPost, "/packages", strings.NewReader(test.body))
|
||||
request.SetPathValue("regId", test.regID)
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.AddLabPackage(recorder, request)
|
||||
if recorder.Code != 422 || !strings.Contains(recorder.Body.String(), test.message) {
|
||||
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
type fakeLabRegistrationRepository struct {
|
||||
outcome repository.LabRegistrationOutcome
|
||||
err error
|
||||
params repository.LabRegistrationParams
|
||||
addParams repository.AddLabServicesParams
|
||||
regID string
|
||||
outcome repository.LabRegistrationOutcome
|
||||
err error
|
||||
params repository.LabRegistrationParams
|
||||
addParams repository.AddLabServicesParams
|
||||
packageParams repository.AddLabPackageParams
|
||||
regID string
|
||||
}
|
||||
|
||||
func (f *fakeLabRegistrationRepository) RegisterLab(_ context.Context, params repository.LabRegistrationParams) (repository.LabRegistrationOutcome, error) {
|
||||
@@ -31,6 +32,12 @@ func (f *fakeLabRegistrationRepository) AddLabServices(_ context.Context, regID
|
||||
return f.outcome, f.err
|
||||
}
|
||||
|
||||
func (f *fakeLabRegistrationRepository) AddLabPackage(_ context.Context, regID string, params repository.AddLabPackageParams) (repository.LabRegistrationOutcome, error) {
|
||||
f.regID = regID
|
||||
f.packageParams = params
|
||||
return f.outcome, f.err
|
||||
}
|
||||
|
||||
func TestRegisterLabValidPayloadAndResponseParity(t *testing.T) {
|
||||
doctorID := "DR01"
|
||||
registrationType := int64(3)
|
||||
|
||||
374
internal/repository/lab_package.go
Normal file
374
internal/repository/lab_package.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AddLabPackageParams struct {
|
||||
PackageID string
|
||||
Cito bool
|
||||
MarkupCito float64
|
||||
Notes string
|
||||
Channel string
|
||||
}
|
||||
|
||||
type labPackageHeader struct {
|
||||
ID string
|
||||
Name string
|
||||
Days sql.NullInt64
|
||||
TotalPrice sql.NullString
|
||||
}
|
||||
|
||||
type labPackageService struct {
|
||||
ID int64
|
||||
DepartmentID string
|
||||
ServiceID string
|
||||
HeaderService sql.NullString
|
||||
Quantity sql.NullInt64
|
||||
Days sql.NullInt64
|
||||
Price sql.NullString
|
||||
DiscountType sql.NullString
|
||||
Discount sql.NullString
|
||||
ShareType sql.NullString
|
||||
ShareHospital sql.NullString
|
||||
ShareDoctor sql.NullString
|
||||
ShareExternal sql.NullString
|
||||
ShareOther sql.NullString
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) AddLabPackage(ctx context.Context, regID string, params AddLabPackageParams) (LabRegistrationOutcome, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
outcome, err := r.addLabPackageAttempt(ctx, regID, params)
|
||||
if err == nil {
|
||||
return outcome, nil
|
||||
}
|
||||
lastErr = err
|
||||
if !isConcurrencyError(err) || ctx.Err() != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return LabRegistrationOutcome{}, lastErr
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) addLabPackageAttempt(ctx context.Context, regID string, params AddLabPackageParams) (outcome LabRegistrationOutcome, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
_ = tx.Rollback()
|
||||
panic(recovered)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
outcome, err = addLabPackageInTransaction(ctx, tx, strings.ToUpper(strings.TrimSpace(regID)), params)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func addLabPackageInTransaction(ctx context.Context, tx *sql.Tx, regID string, params AddLabPackageParams) (LabRegistrationOutcome, error) {
|
||||
registration, found, err := findLabRegistrationStateForUpdate(ctx, tx, regID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "registration_not_found", "Registrasi laboratorium tidak ditemukan", nil), nil
|
||||
}
|
||||
if registration.BatalDaftar == "Y" {
|
||||
return labOutcome(409, false, "registration_cancelled", "Registrasi laboratorium sudah dibatalkan", nil), nil
|
||||
}
|
||||
if registration.Pulang == "Y" {
|
||||
return labOutcome(409, false, "registration_closed", "Registrasi laboratorium sudah ditutup", nil), nil
|
||||
}
|
||||
transaction, found, err := findLabTransactionStateForUpdate(ctx, tx, registration.Registration.RegID, registration.Registration.DepartemenID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "transaction_not_found", "Transaksi departemen laboratorium tidak ditemukan", nil), nil
|
||||
}
|
||||
|
||||
packageHeader, found, err := findActiveOutpatientPackage(ctx, tx, params.PackageID, registration.Registration.Tanggal)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "package_not_found", "Paket dispenser rawat jalan tidak ditemukan, belum disetujui, atau sudah tidak berlaku", nil), nil
|
||||
}
|
||||
packageServices, err := findActivePackageServices(ctx, tx, packageHeader.ID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if len(packageServices) == 0 {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: "Paket dispenser tidak memiliki layanan aktif"}
|
||||
}
|
||||
|
||||
alreadyAdded, err := packageAlreadyAdded(ctx, tx, transaction.Transaction.TrxDepartemenID, packageHeader.ID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if alreadyAdded {
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration.Registration, &transaction.Transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data.Package = &LabPackage{PaketID: packageHeader.ID, Nama: packageHeader.Name}
|
||||
return labOutcome(200, true, "package_already_added", "Paket dispenser sudah terdaftar pada kunjungan laboratorium ini", &data), nil
|
||||
}
|
||||
serviceIDs := make([]string, len(packageServices))
|
||||
for i, service := range packageServices {
|
||||
serviceIDs[i] = service.ServiceID
|
||||
}
|
||||
conflicts, err := findExistingLabServiceIDs(ctx, tx, transaction.Transaction.TrxDepartemenID, serviceIDs)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if len(conflicts) > 0 {
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration.Registration, &transaction.Transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data.SkippedLayananIDs = &conflicts
|
||||
return labOutcome(409, false, "package_service_conflict", "Paket memiliki layanan yang sudah ada pada registrasi laboratorium", &data), nil
|
||||
}
|
||||
if registration.Registration.GroupTarifID.Valid {
|
||||
active, err := exists(ctx, tx, `SELECT EXISTS(SELECT 1 FROM perusahaan_grouptarif WHERE GroupTarifID = ? AND NA = 'N')`, registration.Registration.GroupTarifID.Int64)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !active {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: "Group tarif registrasi tidak ditemukan atau sudah tidak aktif"}
|
||||
}
|
||||
}
|
||||
|
||||
jkt, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
now := time.Now().In(jkt).Format("2006-01-02 15:04:05")
|
||||
dispenserID, visitNumber, err := ensurePackageDispenser(ctx, tx, registration.Registration, packageHeader, params.Channel, now)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
classID := "Default"
|
||||
if transaction.KelasID.Valid && strings.TrimSpace(transaction.KelasID.String) != "" {
|
||||
classID = transaction.KelasID.String
|
||||
} else if registration.KelasID.Valid && strings.TrimSpace(registration.KelasID.String) != "" {
|
||||
classID = registration.KelasID.String
|
||||
}
|
||||
doctorID := ""
|
||||
if transaction.DokterID.Valid {
|
||||
doctorID = transaction.DokterID.String
|
||||
} else if registration.Registration.DokterID.Valid {
|
||||
doctorID = registration.Registration.DokterID.String
|
||||
}
|
||||
cito := "N"
|
||||
if params.Cito {
|
||||
cito = "Y"
|
||||
}
|
||||
addedIDs := make([]string, 0, len(packageServices))
|
||||
for _, packageService := range packageServices {
|
||||
master, found, err := findLabServiceMaster(ctx, tx, packageService.ServiceID, packageService.DepartmentID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: fmt.Sprintf("Layanan %s pada paket tidak aktif di departemen %s", packageService.ServiceID, packageService.DepartmentID)}
|
||||
}
|
||||
operationID, err := resolvePackageOperationID(ctx, tx, packageService.HeaderService)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
dispenserServiceID, err := insertPackageDispenserService(ctx, tx, dispenserID, registration.Registration, packageHeader.ID, packageService, operationID, params.Channel, now)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
price := math.Max(0, parseNullableDecimal(packageService.Price))
|
||||
discount := math.Max(0, parseNullableDecimal(packageService.Discount))
|
||||
discountType := "Percentage"
|
||||
if packageService.DiscountType.Valid && packageService.DiscountType.String != "" {
|
||||
discountType = packageService.DiscountType.String
|
||||
}
|
||||
net := applyPackageDiscount(price, discount, discountType)
|
||||
patient, guarantor := net, 0.0
|
||||
if registration.Registration.GroupTarifID.Valid && registration.Registration.GroupTarifID.Int64 > 0 {
|
||||
patient, guarantor = 0, net
|
||||
}
|
||||
resolved := resolvedLabService{
|
||||
LayananID: master.LayananID, LayananDepartemenID: master.LayananDepartemenID,
|
||||
Satuan: master.Satuan, UnitCost: nullDecimal(master.UnitCost), FlagTipe: master.FlagTipe,
|
||||
Jumlah: 1, Cito: cito, MarkupCito: params.MarkupCito, Harga: price,
|
||||
HargaDefault: price, Total: net, ShareTipe: packageString(packageService.ShareType, "Percentage"),
|
||||
ShareRS: parseNullableDecimal(packageService.ShareHospital), ShareDokter: parseNullableDecimal(packageService.ShareDoctor),
|
||||
ShareExternal: parseNullableDecimal(packageService.ShareExternal), ShareLain: parseNullableDecimal(packageService.ShareOther),
|
||||
Catatan: params.Notes,
|
||||
}
|
||||
trxServiceID, err := insertPackageTransactionService(ctx, tx, resolved, discountType, discount, patient, patient, guarantor, guarantor, packageHeader.ID, registration.Registration.RegID, transaction.Transaction.TrxDepartemenID, registration.Registration.MEDRECID, packageService.DepartmentID, doctorID, classID, params.Channel, now)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `INSERT INTO trxlayanan_detail (TrxLayananID, PaketDispenserID, PaketDispenserLayananID, LoginBuat, TanggalBuat, NA) VALUES (?, ?, ?, ?, ?, 'N')`, trxServiceID, dispenserID, dispenserServiceID, params.Channel, now); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
addedIDs = append(addedIDs, master.LayananID)
|
||||
}
|
||||
if err := insertPackageAdministration(ctx, tx, dispenserID, registration.Registration.RegID, packageHeader.ID, params.Channel, now); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err := syncLabSpecimens(ctx, tx, transaction.Transaction.TrxDepartemenID, transaction.Transaction.TrxID, params.Channel, now); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err := updateLabTransactionTotal(ctx, tx, transaction.Transaction.TrxDepartemenID, registration.Registration.RegID, registration.Registration.DepartemenID); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration.Registration, &transaction.Transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data.Package = &LabPackage{PaketDispenserID: dispenserID, PaketID: packageHeader.ID, Nama: packageHeader.Name, KunjunganKe: visitNumber, AddedLayananIDs: addedIDs}
|
||||
return labOutcome(201, true, "package_added", "Paket dispenser berhasil ditambahkan ke registrasi laboratorium", &data), nil
|
||||
}
|
||||
|
||||
func packageAlreadyAdded(ctx context.Context, tx *sql.Tx, trxDepartmentID int64, packageID string) (bool, error) {
|
||||
var marker int
|
||||
err := tx.QueryRowContext(ctx, `SELECT 1 FROM trxlayanan WHERE TrxDepartemenID = ? AND PaketID = ? AND NA = 'N' LIMIT 1 FOR UPDATE`, trxDepartmentID, packageID).Scan(&marker)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
func findActiveOutpatientPackage(ctx context.Context, tx *sql.Tx, packageID string, date time.Time) (labPackageHeader, bool, error) {
|
||||
var result labPackageHeader
|
||||
err := tx.QueryRowContext(ctx, `SELECT PaketID, Nama, JumlahHari, TotalHarga FROM paket_header WHERE PaketID = ? AND StatusID = 2 AND RJ = 1 AND NA = 'N' AND (TglDari IS NULL OR TglDari = '' OR DATE(TglDari) <= DATE(?)) AND (TglSampai IS NULL OR TglSampai = '' OR DATE(TglSampai) >= DATE(?)) LIMIT 1 FOR UPDATE`, packageID, date, date).Scan(&result.ID, &result.Name, &result.Days, &result.TotalPrice)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return labPackageHeader{}, false, nil
|
||||
}
|
||||
return result, err == nil, err
|
||||
}
|
||||
|
||||
func findActivePackageServices(ctx context.Context, tx *sql.Tx, packageID string) ([]labPackageService, error) {
|
||||
rows, err := tx.QueryContext(ctx, `SELECT PaketLayananID, DepartemenID, LayananID, HeaderLayanan, Jumlah, JumlahHari, Harga, TipeDiskon, Diskon, ShareTipe, ShareRS, ShareDokter, ShareExternal, ShareLain FROM paket_layanan WHERE PaketID = ? AND NA = 'N' ORDER BY PaketLayananID`, packageID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]labPackageService, 0)
|
||||
for rows.Next() {
|
||||
var item labPackageService
|
||||
if err := rows.Scan(&item.ID, &item.DepartmentID, &item.ServiceID, &item.HeaderService, &item.Quantity, &item.Days, &item.Price, &item.DiscountType, &item.Discount, &item.ShareType, &item.ShareHospital, &item.ShareDoctor, &item.ShareExternal, &item.ShareOther); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func ensurePackageDispenser(ctx context.Context, tx *sql.Tx, registration dbLabRegistration, header labPackageHeader, channel, now string) (int64, int64, error) {
|
||||
var dispenserID int64
|
||||
err := tx.QueryRowContext(ctx, `SELECT h.PaketDispenserID FROM paket_dispenser_header h WHERE h.PaketID = ? AND h.RegID = ? AND h.MEDRECID = ? AND h.NA = 'N' AND NOT EXISTS (SELECT 1 FROM paket_dispenser_layanan l WHERE l.PaketDispenserID = h.PaketDispenserID) AND NOT EXISTS (SELECT 1 FROM trxlayanan_detail d WHERE d.PaketDispenserID = h.PaketDispenserID) ORDER BY h.PaketDispenserID DESC LIMIT 1 FOR UPDATE`, header.ID, registration.RegID, registration.MEDRECID).Scan(&dispenserID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, 0, err
|
||||
}
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
result, err := tx.ExecContext(ctx, `INSERT INTO paket_dispenser_header (PaketID, RegID, MEDRECID, JumlahHari, TotalHarga, LoginBuat, TanggalBuat, NA) VALUES (?, ?, ?, ?, ?, ?, ?, 'N')`, header.ID, registration.RegID, registration.MEDRECID, nullInt64Value(header.Days), nullStringValue(header.TotalPrice), channel, now)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
dispenserID, err = result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
var visit int64
|
||||
err = tx.QueryRowContext(ctx, `SELECT KunjunganKe FROM paket_dispenser_episode_rj WHERE PaketDispenserID = ? AND NA = 'N' LIMIT 1 FOR UPDATE`, dispenserID).Scan(&visit)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) + 1 FROM paket_dispenser_episode_rj e INNER JOIN paket_dispenser_header h ON h.PaketDispenserID = e.PaketDispenserID WHERE e.MEDRECID = ? AND h.PaketID = ? AND e.NA = 'N' AND h.NA = 'N'`, registration.MEDRECID, header.ID).Scan(&visit); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO paket_dispenser_episode_rj (PaketDispenserID, RegID, MEDRECID, KunjunganKe, LoginBuat, TanggalBuat, NA) VALUES (?, ?, ?, ?, ?, ?, 'N')`, dispenserID, registration.RegID, registration.MEDRECID, visit, channel, now)
|
||||
}
|
||||
return dispenserID, visit, err
|
||||
}
|
||||
|
||||
func resolvePackageOperationID(ctx context.Context, tx *sql.Tx, header sql.NullString) (int64, error) {
|
||||
if !header.Valid || strings.TrimSpace(header.String) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var id sql.NullInt64
|
||||
err := tx.QueryRowContext(ctx, `SELECT PaketDispenserOperasiID FROM paket_dispenser_operasi_header WHERE PaketOperasiHeaderID = ? LIMIT 1`, header.String).Scan(&id)
|
||||
if errors.Is(err, sql.ErrNoRows) || !id.Valid || id.Int64 < 1 {
|
||||
return 0, ValidationError{Message: "Mapping operasi untuk HeaderLayanan " + header.String + " tidak ditemukan"}
|
||||
}
|
||||
return id.Int64, err
|
||||
}
|
||||
|
||||
func insertPackageDispenserService(ctx context.Context, tx *sql.Tx, dispenserID int64, registration dbLabRegistration, packageID string, service labPackageService, operationID int64, channel, now string) (int64, error) {
|
||||
result, err := tx.ExecContext(ctx, `INSERT INTO paket_dispenser_layanan (PaketDispenserID, PaketDispenserOperasiID, RegID, PaketID, DepartemenID, LayananID, HeaderLayanan, JumlahAwal, Jumlah, JumlahHari, Harga, TipeDiskon, Diskon, ShareTipe, ShareRS, ShareDokter, ShareExternal, ShareLain, LoginBuat, TanggalBuat, NA) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`, dispenserID, operationID, registration.RegID, packageID, service.DepartmentID, service.ServiceID, nullStringValue(service.HeaderService), nullInt64Value(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 0, err
|
||||
}
|
||||
return result.LastInsertId()
|
||||
}
|
||||
|
||||
func insertPackageTransactionService(ctx context.Context, tx *sql.Tx, service resolvedLabService, discountType string, discount, patient, patientDefault, guarantor, guarantorDefault float64, packageID, regID string, trxDepartmentID int64, medrecID, departmentID, doctorID, classID, channel, now string) (int64, error) {
|
||||
headerDetail := 1
|
||||
if service.FlagTipe.Valid && service.FlagTipe.String == "M" {
|
||||
headerDetail = 0
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `INSERT INTO trxlayanan (TrxDepartemenID, RegID, MEDRECID, DepartemenID, DokterID, HeaderDetail, Urutan, LevelLayanan, HeaderLayananID, LayananID, LayananDepartemenID, PaketID, JenisRegID, KelasID, Catatan, Cito, MarkUpCito, Jumlah, Satuan, MataUang, Rate, UnitCost, TipeDiskon, Diskon, HargaDefault, Harga, DitanggungPasien, DitanggungPasienDefault, DitanggungPenjamin, DitanggungPenjaminDefault, ShareTipe, ShareRS, ShareDokter, ShareExternal, ShareLain2, Tagihkan, Tampilkan, LoginBuat, TanggalBuat, NA) VALUES (?, ?, ?, ?, ?, ?, 0, 0, '', ?, ?, ?, '3', ?, ?, ?, ?, ?, ?, 'IDR', 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'Y', 'Y', ?, ?, 'N')`, trxDepartmentID, regID, medrecID, departmentID, doctorID, headerDetail, service.LayananID, service.LayananDepartemenID, packageID, classID, service.Catatan, service.Cito, service.MarkupCito, service.Jumlah, nullStringValue(service.Satuan), service.UnitCost, discountType, discount, service.HargaDefault, service.Harga, patient, patientDefault, guarantor, guarantorDefault, service.ShareTipe, service.ShareRS, service.ShareDokter, service.ShareExternal, service.ShareLain, channel, now)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil || headerDetail != 0 {
|
||||
return id, err
|
||||
}
|
||||
if err := insertLabServiceSupport(ctx, tx, id, service.LayananID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
details, err := findMultipleLabDetails(ctx, tx, service.LayananID, departmentID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for _, detail := range details {
|
||||
if err := insertMultipleLabDetail(ctx, tx, detail, service, regID, trxDepartmentID, medrecID, departmentID, doctorID, classID, channel, now); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func insertPackageAdministration(ctx context.Context, tx *sql.Tx, dispenserID int64, regID, packageID, channel, now string) error {
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO paket_dispenser_adm (PaketAdmID, PaketDispenserID, RegID, PaketID, TipeRI, Hitung, AdmRI, AdmRIBaru, TipeDiskonRI, DiskonRI, LoginBuat, TanggalBuat, NA) SELECT PaketAdmID, ?, ?, PaketID, TipeRI, 'Y', AdmRI, AdmRIBaru, TipeDiskonRI, DiskonRI, ?, ?, 'N' FROM paket_adm WHERE PaketID = ? AND NA = 'N' LIMIT 1`, dispenserID, regID, channel, now, packageID)
|
||||
return err
|
||||
}
|
||||
|
||||
func applyPackageDiscount(price, discount float64, discountType string) float64 {
|
||||
if strings.EqualFold(discountType, "Percentage") {
|
||||
return math.Max(0, price-price*discount/100)
|
||||
}
|
||||
return math.Max(0, price-discount)
|
||||
}
|
||||
|
||||
func packageString(value sql.NullString, fallback string) string {
|
||||
if value.Valid && strings.TrimSpace(value.String) != "" {
|
||||
return value.String
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -51,6 +51,15 @@ type LabRegistrationData struct {
|
||||
Layanan []LabService `json:"layanan"`
|
||||
AddedLayananIDs *[]string `json:"added_layanan_ids,omitempty"`
|
||||
SkippedLayananIDs *[]string `json:"skipped_layanan_ids,omitempty"`
|
||||
Package *LabPackage `json:"package,omitempty"`
|
||||
}
|
||||
|
||||
type LabPackage struct {
|
||||
PaketDispenserID int64 `json:"PaketDispenserID,omitempty"`
|
||||
PaketID string `json:"PaketID"`
|
||||
Nama string `json:"Nama"`
|
||||
KunjunganKe int64 `json:"KunjunganKe,omitempty"`
|
||||
AddedLayananIDs []string `json:"added_layanan_ids,omitempty"`
|
||||
}
|
||||
|
||||
type LabRegistration struct {
|
||||
|
||||
@@ -261,7 +261,8 @@ func findLabRegistrationStateForUpdate(ctx context.Context, tx *sql.Tx, regID st
|
||||
var state labRegistrationServiceState
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT RegID, MEDRECID, Tanggal, DepartemenID, DokterID,
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID, KelasID, BatalDaftar, Pulang
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID, PerusahaanID, AsuransiID,
|
||||
ProviderID, GroupTarifID, NomerPolis, KelasID, BatalDaftar, Pulang
|
||||
FROM regpas WHERE RegID = ? AND NA = 'N' LIMIT 1 FOR UPDATE`, regID).Scan(
|
||||
&state.Registration.RegID,
|
||||
&state.Registration.MEDRECID,
|
||||
@@ -271,6 +272,11 @@ func findLabRegistrationStateForUpdate(ctx context.Context, tx *sql.Tx, regID st
|
||||
&state.Registration.JenisPasienID,
|
||||
&state.Registration.JenisRegID,
|
||||
&state.Registration.PeriodeTarifID,
|
||||
&state.Registration.PerusahaanID,
|
||||
&state.Registration.AsuransiID,
|
||||
&state.Registration.ProviderID,
|
||||
&state.Registration.GroupTarifID,
|
||||
&state.Registration.NomerPolis,
|
||||
&state.KelasID,
|
||||
&state.BatalDaftar,
|
||||
&state.Pulang,
|
||||
|
||||
Reference in New Issue
Block a user