Files
cpone_midleware/internal/http/paket_create.go

379 lines
13 KiB
Go

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