package api import ( "crypto/sha256" "crypto/subtle" "encoding/json" "net/http" "strings" "time" ) const maxOnecEventBody = 8 << 10 func (a *API) WithOneCWebhook(key string) { a.onecHook = strings.TrimSpace(key) } func (a *API) onecAutonomousEnabled() bool { return a.onecHook != "" } type onecEvent struct { Event string `json:"event"` DoctorID string `json:"doctor_id"` AppointmentID string `json:"appointment_id"` Status string `json:"status"` BranchID string `json:"branch_id"` Start *time.Time `json:"start"` End *time.Time `json:"end"` } func sameKey(got, want string) bool { g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want)) return subtle.ConstantTimeCompare(g[:], w[:]) == 1 } func (a *API) onecEvents(w http.ResponseWriter, r *http.Request) { if !a.onecAutonomousEnabled() { fail(w, 404, "not_found", "Не найдено") return } ip := clientIP(r) if a.tooMany(w, "onec-ev:"+ip, 300, time.Minute) { return } if !a.checkOnecKey(r) { if a.tooMany(w, "onec-bad:"+ip, 20, 10*time.Minute) { return } fail(w, 401, "unauthorized", "Неверный ключ") return } var ev onecEvent if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxOnecEventBody)).Decode(&ev); err != nil { fail(w, 400, "bad_json", "Не удалось разобрать событие") return } switch ev.Event { case "schedule_changed", "directory_changed": if err := a.store.RequestOnecSnapshot(r.Context()); err != nil { handleErr(w, err) return } writeJSON(w, http.StatusAccepted, envelope{Data: map[string]any{"ok": true, "queued": "push"}}) case "appointment_status": if ev.AppointmentID == "" || (ev.Status != "booked" && ev.Status != "arrived" && ev.Status != "cancelled") { fail(w, 400, "validation", "Нужны appointment_id и status: booked, arrived или cancelled") return } if (ev.Start == nil) != (ev.End == nil) || (ev.Start != nil && !ev.Start.Before(*ev.End)) { fail(w, 400, "validation", "Некорректный интервал события") return } _, _, known, err := a.store.ApplyOnecAppointmentEvent(r.Context(), ev.AppointmentID, ev.Status, ev.DoctorID, ev.BranchID, ev.Start, ev.End) if err != nil { handleErr(w, err) return } if err = a.store.RequestOnecSnapshot(r.Context()); err != nil { handleErr(w, err) return } ok(w, map[string]any{"ok": true, "known": known}, nil) default: fail(w, 400, "validation", "Неизвестное событие: допустимы schedule_changed, directory_changed, appointment_status") } }