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, 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, }) }