209 lines
6.2 KiB
Go
209 lines
6.2 KiB
Go
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)
|
||
}
|