package api import ( "encoding/json" "net/http" "strconv" "strings" "unicode/utf8" "dana/internal/storage" ) const ( maxCheckupItems = 100 maxCheckupItemLen = 300 ) type checkupRequest struct { Slug string `json:"slug"` Audience string `json:"audience"` Name string `json:"name"` Lead string `json:"lead"` Price *float64 `json:"price"` OldPrice *float64 `json:"old_price"` ImageID *int64 `json:"image_id"` Items []string `json:"items"` Sort int `json:"sort"` Published bool `json:"published"` SEOTitle string `json:"seo_title"` SEODesc string `json:"seo_description"` SEOH1 string `json:"seo_h1"` } func decodeCheckup(w http.ResponseWriter, r *http.Request) (storage.CheckupInput, bool) { var req checkupRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный запрос") return storage.CheckupInput{}, false } in := storage.CheckupInput{ Audience: strings.TrimSpace(req.Audience), Name: strings.TrimSpace(req.Name), Lead: strings.TrimSpace(req.Lead), Price: req.Price, OldPrice: req.OldPrice, ImageID: req.ImageID, Sort: req.Sort, Published: req.Published, SEOTitle: strings.TrimSpace(req.SEOTitle), SEODesc: strings.TrimSpace(req.SEODesc), SEOH1: strings.TrimSpace(req.SEOH1), } if in.Name == "" { fail(w, http.StatusUnprocessableEntity, "validation", "Укажите название программы") return in, false } slug, valid := resolveSlug(w, req.Slug, in.Name) if !valid { return in, false } in.Slug = slug if in.Audience != "adults" && in.Audience != "kids" { fail(w, http.StatusUnprocessableEntity, "validation", "Выберите, для кого программа: взрослые или дети") return in, false } if in.Price != nil && *in.Price < 0 || in.OldPrice != nil && *in.OldPrice < 0 { fail(w, http.StatusUnprocessableEntity, "validation", "Цена не может быть отрицательной") return in, false } // Цена без скидки на сайте зачёркивается — она обязана быть больше действующей. if in.OldPrice != nil && (in.Price == nil || *in.OldPrice <= *in.Price) { fail(w, http.StatusUnprocessableEntity, "validation", "Цена без скидки должна быть больше цены программы") return in, false } if in.Sort == 0 { in.Sort = 100 } for _, item := range req.Items { item = strings.TrimSpace(item) if item == "" { continue } if utf8.RuneCountInString(item) > maxCheckupItemLen { fail(w, http.StatusUnprocessableEntity, "validation", "Пункт состава длиннее 300 символов: "+string([]rune(item)[:40])+"…") return in, false } in.Items = append(in.Items, item) } if len(in.Items) > maxCheckupItems { fail(w, http.StatusUnprocessableEntity, "validation", "В составе программы не больше 100 пунктов") return in, false } return in, true } func (a *API) adminCheckups(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { list, err := a.store.AdminCheckups(r.Context()) if err != nil { handleErr(w, err) return } ok(w, list, map[string]any{"total": len(list)}) } func (a *API) adminCreateCheckup(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { in, valid := decodeCheckup(w, r) if !valid { return } id, err := a.store.AdminCreateCheckup(r.Context(), in) if err != nil { handleErr(w, err) return } a.store.Audit(r.Context(), u.ID, "create", "checkup", id, clientIP(r)) writeJSON(w, http.StatusCreated, envelope{Data: map[string]any{"id": id, "slug": in.Slug}}) } func (a *API) adminUpdateCheckup(w http.ResponseWriter, r *http.Request, u *storage.AdminUser) { id, err := strconv.ParseInt(r.PathValue("id"), 10, 64) if err != nil { fail(w, http.StatusBadRequest, "bad_request", "Некорректный id") return } in, valid := decodeCheckup(w, r) if !valid { return } oldSlug, err := a.store.PublishedCheckupSlug(r.Context(), id) if err != nil { handleErr(w, err) return } if err := a.store.AdminUpdateCheckup(r.Context(), id, in); err != nil { handleErr(w, err) return } if oldSlug != "" && in.Published { a.keepOldPath(r.Context(), checkupWebPath(oldSlug), checkupWebPath(in.Slug)) } a.store.Audit(r.Context(), u.ID, "update", "checkup", id, clientIP(r)) ok(w, map[string]any{"id": id, "slug": in.Slug}, nil) }