148 lines
4.5 KiB
Go
148 lines
4.5 KiB
Go
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)
|
||
}
|