package api import ( "encoding/json" "net/http" "strconv" "strings" "time" "dana/internal/onec" ) // onecAuth проверяет API-ключ входящих запросов от 1С. func (a *API) checkOnecKey(r *http.Request) bool { key := r.Header.Get("X-Api-Key") if key == "" { key = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") } key = strings.TrimSpace(key) if key == "" { return false } if a.onecHook != "" && sameKey(key, a.onecHook) { return true } if a.onec != nil && a.onec.Key != "" && sameKey(key, a.onec.Key) { return true } return false } type onecSyncPayload struct { Branches []onec.Branch `json:"branches"` Doctors []onec.Doctor `json:"doctors"` Services []onecDoctorServices `json:"services"` Slots []onec.Slot `json:"slots"` } type onecDoctorServices struct { DoctorID string `json:"doctor_id"` Services []onec.Service `json:"services"` } // onecSyncAll принимает полный слепок каталога и расписания от регламентного задания 1С. func (a *API) onecSyncAll(w http.ResponseWriter, r *http.Request) { if !a.checkOnecKey(r) { fail(w, http.StatusUnauthorized, "unauthorized", "Неверный или отсутствующий API-ключ") return } var payload onecSyncPayload if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<20)).Decode(&payload); err != nil { fail(w, http.StatusBadRequest, "bad_json", "Не удалось разобрать тело запроса: "+err.Error()) return } ctx := r.Context() linked := 0 var err error // 1. Сохраняем филиалы и врачей if len(payload.Branches) > 0 || len(payload.Doctors) > 0 { linked, err = a.store.SaveOnecDirectory(ctx, payload.Branches, payload.Doctors) if err != nil { fail(w, http.StatusInternalServerError, "db_error", "Ошибка сохранения справочников: "+err.Error()) return } } // 2. Сохраняем услуги врачей for _, ds := range payload.Services { if ds.DoctorID != "" && len(ds.Services) > 0 { if err := a.store.SaveOnecServices(ctx, ds.DoctorID, ds.Services); err != nil { fail(w, http.StatusInternalServerError, "db_error", "Ошибка сохранения услуг: "+err.Error()) return } } } // 3. Сохраняем слоты расписания if len(payload.Slots) > 0 { minStart, maxEnd := findSlotBounds(payload.Slots) if err := a.store.ReplaceOnecSlots(ctx, minStart, maxEnd, payload.Slots); err != nil { fail(w, http.StatusInternalServerError, "db_error", "Ошибка сохранения слотов: "+err.Error()) return } } ok(w, map[string]any{ "ok": true, "linked": linked, "branches": len(payload.Branches), "doctors": len(payload.Doctors), "services": len(payload.Services), "slots": len(payload.Slots), }, nil) } func findSlotBounds(slots []onec.Slot) (time.Time, time.Time) { if len(slots) == 0 { now := time.Now() return now, now } minT := slots[0].Start maxT := slots[0].End for _, s := range slots[1:] { if s.Start.Before(minT) { minT = s.Start } if s.End.After(maxT) { maxT = s.End } } return minT, maxT } // onecPendingAppointments отдает 1С пачку ожидающих подтверждения записей. func (a *API) onecPendingAppointments(w http.ResponseWriter, r *http.Request) { if !a.checkOnecKey(r) { fail(w, http.StatusUnauthorized, "unauthorized", "Неверный или отсутствующий API-ключ") return } list, err := a.store.PendingOnecAppointments(r.Context(), 50) if err != nil { fail(w, http.StatusInternalServerError, "db_error", err.Error()) return } ok(w, map[string]any{ "ok": true, "appointments": list, }, nil) } type confirmAppointmentReq struct { OnecAppointmentID string `json:"onec_appointment_id"` End *time.Time `json:"end"` } // onecConfirmAppointment подтверждает создание записи в 1С. func (a *API) onecConfirmAppointment(w http.ResponseWriter, r *http.Request) { if !a.checkOnecKey(r) { fail(w, http.StatusUnauthorized, "unauthorized", "Неверный или отсутствующий API-ключ") return } idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { fail(w, http.StatusBadRequest, "bad_id", "Некорректный идентификатор записи") return } var req confirmAppointmentReq if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil { fail(w, http.StatusBadRequest, "bad_json", "Не удалось разобрать тело запроса") return } if req.OnecAppointmentID == "" { fail(w, http.StatusBadRequest, "missing_param", "onec_appointment_id обязателен") return } if err := a.store.ConfirmPendingAppointment(r.Context(), id, req.OnecAppointmentID, req.End); err != nil { fail(w, http.StatusInternalServerError, "db_error", err.Error()) return } if a.bookingConfirmEnabled() { _ = a.store.StartBookingConfirm(r.Context(), id) a.kickConfirm() } ok(w, map[string]any{"ok": true, "id": id, "status": "booked"}, nil) } type rejectAppointmentReq struct { Reason string `json:"reason"` } // onecRejectAppointment отклоняет запись (например, слот уже занят). func (a *API) onecRejectAppointment(w http.ResponseWriter, r *http.Request) { if !a.checkOnecKey(r) { fail(w, http.StatusUnauthorized, "unauthorized", "Неверный или отсутствующий API-ключ") return } idStr := r.PathValue("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil || id <= 0 { fail(w, http.StatusBadRequest, "bad_id", "Некорректный идентификатор записи") return } var req rejectAppointmentReq if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil { req.Reason = "rejected by 1C" } if err := a.store.RejectPendingAppointment(r.Context(), id, req.Reason); err != nil { fail(w, http.StatusInternalServerError, "db_error", err.Error()) return } ok(w, map[string]any{"ok": true, "id": id, "status": "failed"}, nil) }