dana-medical-site/internal/api/admin_checkups.go
2026-10-14 13:33:13 +00:00

148 lines
4.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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