Files
aura-crm/production/backend/internal/booking/validate.go
T

74 lines
2.5 KiB
Go

package booking
import (
"strings"
"time"
"unicode/utf8"
)
// Same caps as internal/client/internal/order — problem_description ends up
// in an order's own field once confirmed (internal/pdfgen renders it via
// MultiCell), so the resource-exhaustion rationale there applies here too.
const (
maxShortFieldLen = 255
maxLongFieldLen = 5000
// preferredAtGrace tolerates client-side clock skew and the time
// between form-fill and submit — a request for "right now" shouldn't
// bounce just because a few minutes passed.
preferredAtGrace = 15 * time.Minute
)
// cleanField strips carriage returns/newlines and trims whitespace — same
// rationale as site's internal/lead.cleanField: unsanitized text ends up
// verbatim in the staff Telegram alert (notify.Send), and a raw newline
// could spoof extra "fields" in that message.
func cleanField(s string) string {
replacer := strings.NewReplacer("\r", " ", "\n", " ")
return strings.TrimSpace(replacer.Replace(s))
}
type createInput struct {
Name string `json:"name"`
Phone string `json:"phone"`
DeviceType string `json:"device_type"`
ProblemDescription string `json:"problem_description"`
PreferredAt string `json:"preferred_at"`
// Address/IsOnsite are only ever set via CreateByStaff (the "+Заявка"
// type-selector's "Выездной ремонт" branch) — the public Create handler
// never reads or sets them, so they default to zero values there.
Address string `json:"address"`
IsOnsite bool `json:"is_onsite"`
// Website is a honeypot field, same convention as site's lead form —
// real visitors never see or fill it (hidden off-screen). A non-empty
// value means a bot filled every field it could find.
Website string `json:"website"`
}
func (b createInput) validate() string {
if b.Name == "" {
return "name is required"
}
if b.Phone == "" {
return "phone is required"
}
if b.DeviceType == "" {
return "device_type is required"
}
for _, f := range []string{b.Name, b.Phone, b.DeviceType} {
if utf8.RuneCountInString(f) > maxShortFieldLen {
return "one of the fields is too long"
}
}
if utf8.RuneCountInString(b.ProblemDescription) > maxLongFieldLen {
return "problem_description is too long"
}
t, err := time.Parse(time.RFC3339, b.PreferredAt)
if err != nil {
return "preferred_at must be a valid date/time"
}
if t.Before(time.Now().Add(-preferredAtGrace)) {
return "preferred_at must not be in the past"
}
return ""
}