dana-medical-site/internal/api/onec_push.go
Abdulkadyr Karagishiev a02c4faad7
Some checks failed
Deploy Dana Medical Site / deploy (push) Failing after 2s
feat(onec): support outbound push/pull sync and async appointment queue
2026-10-19 18:30:23 +03:00

209 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}