Files
cpone_midleware/internal/http/lab_registration.go

392 lines
12 KiB
Go

package http
import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"primaya-api/cpone-middleware/internal/repository"
)
var integerValue = regexp.MustCompile(`^[+-]?\d+$`)
type LabRegistrationResponse struct {
Success bool `json:"success"`
Status string `json:"status"`
Message string `json:"message"`
Data *repository.LabRegistrationData `json:"data"`
}
type validatedLabRegistration struct {
Params repository.LabRegistrationParams
}
func (h Handler) RegisterLab(w http.ResponseWriter, r *http.Request) {
payload, err := decodeJSONObject(r)
if err != nil {
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", err.Error(), nil)
return
}
validated, validationMessage := validateLabRegistration(payload)
if validationMessage != "" {
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationMessage, nil)
return
}
outcome, err := h.labRepository(r.Context()).RegisterLab(r.Context(), validated.Params)
if err != nil {
var validationErr repository.ValidationError
if errors.As(err, &validationErr) {
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationErr.Message, 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 decodeJSONObject(r *http.Request) (map[string]any, error) {
decoder := json.NewDecoder(r.Body)
decoder.UseNumber()
payload := make(map[string]any)
if err := decoder.Decode(&payload); err != nil {
// Laravel treats an empty, malformed, or non-object JSON body as empty
// request input; validation then reports the first required field.
return payload, nil
}
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
return make(map[string]any), nil
}
return payload, nil
}
func validateLabRegistration(payload map[string]any) (validatedLabRegistration, string) {
medrecID, message := requiredString(payload, "medrec_id", 50, "Medrec ID wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
jakarta, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return validatedLabRegistration{}, err.Error()
}
registrationDate := time.Now().In(jakarta)
if value, present := nullableValue(payload, "tanggal"); present {
dateText, ok := value.(string)
if !ok || !validDateFormat(dateText, jakarta) {
return validatedLabRegistration{}, "Tanggal registrasi harus format Y-m-d"
}
registrationDate, _ = time.ParseInLocation("2006-01-02", dateText, jakarta)
}
departmentID, message := requiredString(payload, "departemen_id", 50, "Departemen wajib diisi untuk membuat registrasi")
if message != "" {
return validatedLabRegistration{}, message
}
doctorID, message := requiredString(payload, "dokter_id", 50, "Dokter penanggung jawab wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
senderDoctorID, senderPresent, message := optionalString(payload, "dokter_pengirim_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
referrer, referrerPresent, message := optionalString(payload, "perujuk", 255)
if message != "" {
return validatedLabRegistration{}, message
}
if !senderPresent && !referrerPresent {
return validatedLabRegistration{}, "Dokter pengirim atau perujuk wajib diisi"
}
patientType, message := requiredString(payload, "jenis_pasien_id", 20, "Jenis pasien wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
patientType = strings.TrimSpace(patientType)
tariffPeriodID, message := requiredInteger(payload, "periode_tarif_id", "Periode tarif wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
if tariffPeriodID < 1 {
return validatedLabRegistration{}, "The periode tarif id must be at least 1."
}
companyID, companyPresent, message := optionalString(payload, "perusahaan_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
insuranceID, insurancePresent, message := optionalString(payload, "asuransi_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
providerID, _, message := optionalString(payload, "provider_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
policyNumber, _, message := optionalString(payload, "nomer_polis", 100)
if message != "" {
return validatedLabRegistration{}, message
}
employeeID, _, message := optionalString(payload, "nik_karyawan", 100)
if message != "" {
return validatedLabRegistration{}, message
}
employeeName, _, message := optionalString(payload, "nama_karyawan", 255)
if message != "" {
return validatedLabRegistration{}, message
}
var groupTariffID int64
if value, present := nullableValue(payload, "group_tarif_id"); present {
parsed, ok := parseInteger(value)
if !ok {
return validatedLabRegistration{}, "The group tarif id must be an integer."
}
if parsed < 1 && patientType != "0" {
return validatedLabRegistration{}, "The group tarif id must be at least 1."
}
groupTariffID = parsed
}
if patientType != "0" {
if !companyPresent || strings.TrimSpace(companyID) == "" {
return validatedLabRegistration{}, "Perusahaan wajib diisi untuk pasien penjamin"
}
if !insurancePresent || strings.TrimSpace(insuranceID) == "" {
return validatedLabRegistration{}, "Asuransi/penanggung wajib diisi untuk pasien penjamin"
}
if groupTariffID < 1 {
return validatedLabRegistration{}, "Group tarif wajib diisi untuk pasien penjamin"
}
} else {
companyID = ""
insuranceID = ""
providerID = ""
groupTariffID = 0
}
classID, classPresent, message := optionalString(payload, "kelas_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
if !classPresent || strings.TrimSpace(classID) == "" {
classID = "Default"
} else {
classID = strings.TrimSpace(classID)
}
var dailyShiftID *int64
if value, present := nullableValue(payload, "shift_harian_id"); present {
parsed, ok := parseInteger(value)
if !ok {
return validatedLabRegistration{}, "The shift harian id must be an integer."
}
if parsed < 1 {
return validatedLabRegistration{}, "The shift harian id must be at least 1."
}
dailyShiftID = &parsed
}
workingDiagnosis, message := requiredString(payload, "diagnosa_kerja", 1000, "Diagnosa kerja wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
if utf8.RuneCountInString(workingDiagnosis) < 3 {
return validatedLabRegistration{}, "Diagnosa kerja minimal 3 karakter"
}
if consistsOfOneRepeatedRune(workingDiagnosis) {
return validatedLabRegistration{}, "Diagnosa kerja tidak valid"
}
notes, _, message := optionalString(payload, "catatan", 1000)
if message != "" {
return validatedLabRegistration{}, message
}
channel, channelPresent, message := optionalString(payload, "channel", 50)
if message != "" {
return validatedLabRegistration{}, message
}
if !channelPresent {
channel = "CPONE"
}
channel = strings.ToUpper(strings.TrimSpace(channel))
return validatedLabRegistration{Params: repository.LabRegistrationParams{
MedrecID: medrecID,
RegistrationDate: registrationDate,
DepartmentID: departmentID,
DoctorID: doctorID,
SenderDoctorID: senderDoctorID,
Referrer: referrer,
PatientType: patientType,
TariffPeriodID: tariffPeriodID,
CompanyID: strings.TrimSpace(companyID),
InsuranceID: strings.TrimSpace(insuranceID),
ProviderID: strings.TrimSpace(providerID),
GroupTariffID: groupTariffID,
PolicyNumber: strings.TrimSpace(policyNumber),
EmployeeID: strings.TrimSpace(employeeID),
EmployeeName: strings.TrimSpace(employeeName),
ClassID: classID,
DailyShiftID: dailyShiftID,
WorkingDiagnosis: workingDiagnosis,
Notes: notes,
Channel: channel,
}}, ""
}
func requiredString(payload map[string]any, key string, maxLength int, requiredMessage string) (string, string) {
value, exists := payload[key]
if !exists || isLaravelEmpty(value) {
return "", requiredMessage
}
text, ok := value.(string)
if !ok {
return "", fmt.Sprintf("The %s must be a string.", validationAttribute(key))
}
if maxLength > 0 && utf8.RuneCountInString(text) > maxLength {
return "", fmt.Sprintf("The %s must not be greater than %d characters.", validationAttribute(key), maxLength)
}
return text, ""
}
func optionalString(payload map[string]any, key string, maxLength int) (string, bool, string) {
value, present := nullableValue(payload, key)
if !present {
return "", false, ""
}
text, ok := value.(string)
if !ok {
return "", false, fmt.Sprintf("The %s must be a string.", validationAttribute(key))
}
if maxLength > 0 && utf8.RuneCountInString(text) > maxLength {
return "", false, fmt.Sprintf("The %s must not be greater than %d characters.", validationAttribute(key), maxLength)
}
return text, true, ""
}
func requiredInteger(payload map[string]any, key string, requiredMessage string) (int64, string) {
value, exists := payload[key]
if !exists || isLaravelEmpty(value) {
return 0, requiredMessage
}
parsed, ok := parseInteger(value)
if !ok {
return 0, fmt.Sprintf("The %s must be an integer.", validationAttribute(key))
}
return parsed, ""
}
func nullableValue(payload map[string]any, key string) (any, bool) {
value, exists := payload[key]
if !exists || value == nil || value == "" {
return nil, false
}
return value, true
}
func parseInteger(value any) (int64, bool) {
switch typed := value.(type) {
case json.Number:
if integerValue.MatchString(typed.String()) {
parsed, err := strconv.ParseInt(typed.String(), 10, 64)
return parsed, err == nil
}
parsedFloat, err := strconv.ParseFloat(typed.String(), 64)
if err != nil || math.Trunc(parsedFloat) != parsedFloat || parsedFloat > math.MaxInt64 || parsedFloat < math.MinInt64 {
return 0, false
}
return int64(parsedFloat), true
case string:
typed = strings.TrimSpace(typed)
if !integerValue.MatchString(typed) || hasInvalidIntegerLeadingZero(typed) {
return 0, false
}
parsed, err := strconv.ParseInt(typed, 10, 64)
return parsed, err == nil
case bool:
if typed {
return 1, true
}
return 0, false
default:
return 0, false
}
}
func hasInvalidIntegerLeadingZero(value string) bool {
digits := strings.TrimPrefix(strings.TrimPrefix(value, "+"), "-")
return len(digits) > 1 && strings.HasPrefix(digits, "0")
}
func isLaravelEmpty(value any) bool {
if value == nil || value == "" {
return true
}
switch typed := value.(type) {
case []any:
return len(typed) == 0
case map[string]any:
return len(typed) == 0
default:
return false
}
}
func validDateFormat(value string, location *time.Location) bool {
parsed, err := time.ParseInLocation("2006-01-02", value, location)
return err == nil && parsed.Format("2006-01-02") == value
}
func consistsOfOneRepeatedRune(value string) bool {
var first rune
count := 0
for _, current := range value {
if count == 0 {
first = current
} else if current != first {
return false
}
count++
}
return count > 0
}
func validationAttribute(key string) string {
return strings.ReplaceAll(key, "_", " ")
}
func writeLabResponse(
w http.ResponseWriter,
statusCode int,
success bool,
status string,
message string,
data *repository.LabRegistrationData,
) {
WriteJSON(w, statusCode, LabRegistrationResponse{
Success: success,
Status: status,
Message: message,
Data: data,
})
}