package http import ( "encoding/json" "fmt" "log" "math" "net/http" "strconv" "strings" "time" "primaya-api/cpone-middleware/internal/repository" ) type PaketCreateResponse struct { Success bool `json:"success"` Status string `json:"status"` Message string `json:"message"` Data *PaketCreateResponseData `json:"data"` } type PaketCreateResponseData struct { PaketID string `json:"paket_id"` Nama string `json:"nama"` TotalHarga string `json:"total_harga"` TotalDiskon string `json:"total_diskon"` JumlahLayanan int `json:"jumlah_layanan"` } func (h Handler) CreatePaket(w http.ResponseWriter, r *http.Request) { payload, err := decodeJSONObject(r) if err != nil { writePaketCreateError(w, http.StatusUnprocessableEntity, "Payload harus berupa object JSON") return } params, message := validateCreatePaket(payload) if message != "" { writePaketCreateError(w, http.StatusUnprocessableEntity, message) return } result, err := h.paketRepository(r.Context()).CreatePaket(r.Context(), params) if err != nil { log.Printf("create paket failed: %v", err) if validationMessage, ok := asValidationError(err); ok { writePaketCreateError(w, http.StatusUnprocessableEntity, validationMessage) return } WriteJSON(w, http.StatusInternalServerError, Response{Success: false, Message: "internal server error", Data: nil}) return } WriteJSON(w, http.StatusCreated, PaketCreateResponse{ Success: true, Status: "created", Message: "Paket berhasil dibuat", Data: &PaketCreateResponseData{ PaketID: result.PaketID, Nama: result.Nama, TotalHarga: result.TotalHarga, TotalDiskon: result.TotalDiskon, JumlahLayanan: result.JumlahLayanan, }, }) } func validateCreatePaket(payload map[string]any) (repository.CreatePaketParams, string) { name, message := requiredString(payload, "nama", 255, "Nama paket wajib diisi") if message != "" { return repository.CreatePaketParams{}, message } typeID, message := requiredString(payload, "jenis_paket", 50, "Jenis paket wajib diisi") if message != "" { return repository.CreatePaketParams{}, message } rawatInap, message := optionalPaketBoolean(payload, "rawat_inap") if message != "" { return repository.CreatePaketParams{}, message } rawatJalan, message := optionalPaketBoolean(payload, "rawat_jalan") if message != "" { return repository.CreatePaketParams{}, message } if !rawatInap && !rawatJalan { return repository.CreatePaketParams{}, "Rawat inap atau rawat jalan minimal salah satu harus aktif" } multiVisit, message := optionalPaketBoolean(payload, "multi_visit") if message != "" { return repository.CreatePaketParams{}, message } startDate, message := optionalPaketDate(payload, "tanggal_mulai") if message != "" { return repository.CreatePaketParams{}, message } endDate, message := optionalPaketDate(payload, "tanggal_selesai") if message != "" { return repository.CreatePaketParams{}, message } if startDate != "" && endDate != "" && endDate < startDate { return repository.CreatePaketParams{}, "Tanggal selesai harus setelah atau sama dengan tanggal mulai" } classID, _, message := optionalString(payload, "kelas_id", 50) if message != "" { return repository.CreatePaketParams{}, message } if strings.TrimSpace(classID) == "" { classID = "Default" } headerID, _, message := optionalString(payload, "header_paket_id", 50) if message != "" { return repository.CreatePaketParams{}, message } notes, _, message := optionalString(payload, "keterangan", 500) if message != "" { return repository.CreatePaketParams{}, message } channel, present, message := optionalString(payload, "channel", 50) if message != "" { return repository.CreatePaketParams{}, message } if !present || strings.TrimSpace(channel) == "" { channel = "CPONE" } days := int64(1) if value, present := nullableValue(payload, "jumlah_hari"); present { days, message = parsePaketInteger(value, "jumlah_hari", "Jumlah hari paket harus berupa angka") if message != "" { return repository.CreatePaketParams{}, message } if days < 1 { return repository.CreatePaketParams{}, "Jumlah hari paket minimal 1" } } rawServices, exists := payload["layanan"] if !exists || isLaravelEmpty(rawServices) { return repository.CreatePaketParams{}, "Layanan wajib diisi" } services, ok := rawServices.([]any) if !ok || len(services) == 0 { return repository.CreatePaketParams{}, "Layanan harus berupa array dan minimal memiliki satu data" } result := make([]repository.CreatePaketLayananParams, 0, len(services)) seen := make(map[string]struct{}, len(services)) for index, rawService := range services { item, ok := rawService.(map[string]any) if !ok { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "layanan harus berupa object") } serviceID, message := requiredNestedString(item, "layanan_id", index, 50, "Layanan wajib diisi") if message != "" { return repository.CreatePaketParams{}, message } departmentID, message := requiredNestedString(item, "departemen_id", index, 50, "Departemen wajib diisi") if message != "" { return repository.CreatePaketParams{}, message } serviceID = strings.TrimSpace(serviceID) departmentID = strings.TrimSpace(departmentID) if _, exists := seen[serviceID]; exists { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "layanan_id tidak boleh duplikat dalam satu paket") } seen[serviceID] = struct{}{} quantity, message := optionalPaketNestedInteger(item, "jumlah", index, 1) if message != "" { return repository.CreatePaketParams{}, message } if quantity < 1 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "jumlah minimal 1") } serviceDays, message := optionalPaketNestedInteger(item, "jumlah_hari", index, 1) if message != "" { return repository.CreatePaketParams{}, message } if serviceDays < 1 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "jumlah_hari minimal 1") } harga, message := requiredPaketMoney(item, "harga", index, "Harga wajib diisi") if message != "" { return repository.CreatePaketParams{}, message } discountType, _, message := optionalNestedString(item, "tipe_diskon", index, 30) if message != "" { return repository.CreatePaketParams{}, message } if strings.TrimSpace(discountType) == "" { discountType = "Percentage" } discountType, message = normalizePaketType(discountType, "tipe_diskon", index) if message != "" { return repository.CreatePaketParams{}, message } discount, message := optionalPaketMoney(item, "diskon", index, 0) if message != "" { return repository.CreatePaketParams{}, message } if discountType == "Percentage" && discount > 10000 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "diskon percentage tidak boleh lebih dari 100") } if discountType == "Absolute" && discount > harga { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "diskon absolute tidak boleh lebih besar dari harga") } shareType, _, message := optionalNestedString(item, "tipe_share", index, 30) if message != "" { return repository.CreatePaketParams{}, message } if strings.TrimSpace(shareType) == "" { shareType = "Percentage" } shareType, message = normalizePaketType(shareType, "tipe_share", index) if message != "" { return repository.CreatePaketParams{}, message } shares := make([]int64, 4) shareKeys := []string{"share_rs", "share_dokter", "share_external", "share_lain"} for shareIndex, key := range shareKeys { shares[shareIndex], message = optionalPaketMoney(item, key, index, 0) if message != "" { return repository.CreatePaketParams{}, message } } if shareType == "Percentage" { shareTotal := shares[0] + shares[1] + shares[2] + shares[3] for _, share := range shares { if share < 0 || share > 10000 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "share percentage harus antara 0 dan 100") } } if shareTotal != 10000 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "total share percentage harus 100") } } else if math.Abs(float64(shares[0]+shares[1]+shares[2]+shares[3]-harga)) > 1 { return repository.CreatePaketParams{}, nestedCreatePaketMessage(index, "total share absolute harus sama dengan harga") } result = append(result, repository.CreatePaketLayananParams{ LayananID: serviceID, DepartemenID: departmentID, Jumlah: quantity, JumlahHari: serviceDays, HargaCents: harga, TipeDiskon: discountType, DiskonCents: discount, TipeShare: shareType, ShareRSCents: shares[0], ShareDokterCents: shares[1], ShareExternalCents: shares[2], ShareLainCents: shares[3], }) } return repository.CreatePaketParams{ Nama: strings.TrimSpace(name), JenisPaket: strings.ToUpper(strings.TrimSpace(typeID)), RawatInap: rawatInap, RawatJalan: rawatJalan, MultiVisit: multiVisit, TanggalMulai: startDate, TanggalSelesai: endDate, KelasID: strings.TrimSpace(classID), HeaderPaketID: strings.TrimSpace(headerID), Keterangan: strings.TrimSpace(notes), JumlahHari: days, Channel: strings.ToUpper(strings.TrimSpace(channel)), Layanan: result, }, "" } func optionalPaketBoolean(payload map[string]any, key string) (bool, string) { value, present := nullableValue(payload, key) if !present { return false, "" } parsed, ok := parseLaravelBoolean(value) if !ok { return false, fmt.Sprintf("The %s field must be true or false.", strings.ReplaceAll(key, "_", " ")) } return parsed, "" } func optionalPaketDate(payload map[string]any, key string) (string, string) { value, present := nullableValue(payload, key) if !present { return "", "" } date, ok := value.(string) if !ok || !validDateFormat(date, time.Local) { return "", fmt.Sprintf("The %s does not match the format Y-m-d.", strings.ReplaceAll(key, "_", " ")) } return date, "" } func parsePaketInteger(value any, key, message string) (int64, string) { parsed, ok := parseInteger(value) if !ok { return 0, message } return parsed, "" } func optionalPaketNestedInteger(item map[string]any, key string, index int, fallback int64) (int64, string) { value, present := nullableValue(item, key) if !present { return fallback, "" } parsed, ok := parseInteger(value) if !ok { return 0, nestedCreatePaketMessage(index, key+" harus berupa angka") } return parsed, "" } func requiredPaketMoney(item map[string]any, key string, index int, requiredMessage string) (int64, string) { value, present := nullableValue(item, key) if !present { return 0, nestedCreatePaketMessage(index, requiredMessage) } return parsePaketMoney(value, nestedCreatePaketMessage(index, key+" harus berupa nominal desimal")) } func optionalPaketMoney(item map[string]any, key string, index int, fallback int64) (int64, string) { value, present := nullableValue(item, key) if !present { return fallback, "" } return parsePaketMoney(value, nestedCreatePaketMessage(index, key+" harus berupa nominal desimal")) } func parsePaketMoney(value any, message string) (int64, string) { var text string switch typed := value.(type) { case json.Number: text = typed.String() case string: text = strings.TrimSpace(typed) default: return 0, message } if text == "" || strings.HasPrefix(text, "-") || strings.HasPrefix(text, "+") { return 0, message } parts := strings.Split(text, ".") if len(parts) > 2 || parts[0] == "" || len(parts[0]) > 16 || (len(parts) == 2 && len(parts[1]) > 2) { return 0, message } if _, err := strconv.ParseUint(parts[0], 10, 64); err != nil { return 0, message } frac := "" if len(parts) == 2 { frac = parts[1] } for len(frac) < 2 { frac += "0" } fracValue := int64(0) if frac != "" { parsed, err := strconv.ParseInt(frac, 10, 64) if err != nil { return 0, message } fracValue = parsed } whole, err := strconv.ParseInt(parts[0], 10, 64) if err != nil || whole > (math.MaxInt64-fracValue)/100 { return 0, message } return whole*100 + fracValue, "" } func normalizePaketType(value, key string, index int) (string, string) { value = strings.TrimSpace(value) switch strings.ToLower(value) { case "percentage": return "Percentage", "" case "absolute": return "Absolute", "" default: return "", nestedCreatePaketMessage(index, key+" harus Percentage atau Absolute") } } func nestedCreatePaketMessage(index int, message string) string { return fmt.Sprintf("The layanan.%d %s.", index, message) } func writePaketCreateError(w http.ResponseWriter, statusCode int, message string) { WriteJSON(w, statusCode, PaketCreateResponse{Success: false, Status: "validation_error", Message: message, Data: nil}) }