feat(cpone): migrate lab registration endpoints
This commit is contained in:
331
internal/http/lab_registration.go
Normal file
331
internal/http/lab_registration.go
Normal file
@@ -0,0 +1,331 @@
|
||||
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.labRepo.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, patientTypePresent, message := optionalString(payload, "jenis_pasien_id", 0)
|
||||
if message != "" {
|
||||
return validatedLabRegistration{}, message
|
||||
}
|
||||
if patientTypePresent && patientType != "0" {
|
||||
return validatedLabRegistration{}, "Registrasi laboratorium API saat ini hanya mendukung pasien umum"
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
|
||||
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,
|
||||
TariffPeriodID: tariffPeriodID,
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user