dana-medical-site/internal/storage/onec.go

594 lines
23 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 storage
import (
"context"
"strings"
"time"
"github.com/jackc/pgx/v5"
"dana/internal/onec"
)
var moscow = time.FixedZone("Europe/Moscow", 3*60*60)
// ---------- Справочники ----------
// normName приводит ФИО к виду для сравнения: без регистра, «ё» = «е», один пробел.
func normName(s string) string {
s = strings.ToLower(strings.ReplaceAll(s, "ё", "е"))
return strings.Join(strings.Fields(s), " ")
}
// SaveOnecDirectory сохраняет филиалы и врачей из 1С и сам привязывает врачей к карточкам сайта,
// когда ФИО совпало ровно с одной ещё не привязанной карточкой. Уже сделанную вручную привязку не трогает.
// Врачей, которых 1С больше не отдаёт, помечает неактивными.
func (s *Storage) SaveOnecDirectory(ctx context.Context, branches []onec.Branch, doctors []onec.Doctor) (linked int, err error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return 0, err
}
defer tx.Rollback(ctx)
for _, b := range branches {
if _, err := tx.Exec(ctx, `
INSERT INTO onec_branches (onec_id, name, address, seen_at) VALUES ($1,$2,$3, now())
ON CONFLICT (onec_id) DO UPDATE SET name=EXCLUDED.name, address=EXCLUDED.address, seen_at=now()`,
b.ID, b.Name, b.Address); err != nil {
return 0, err
}
}
seen := make([]string, 0, len(doctors))
for _, d := range doctors {
seen = append(seen, d.ID)
if _, err := tx.Exec(ctx, `
INSERT INTO onec_doctors (onec_id, full_name, specialties, branch_ids, kids_from, active, seen_at)
VALUES ($1,$2,$3,$4,$5,$6, now())
ON CONFLICT (onec_id) DO UPDATE SET full_name=EXCLUDED.full_name, specialties=EXCLUDED.specialties,
branch_ids=EXCLUDED.branch_ids, kids_from=EXCLUDED.kids_from, active=EXCLUDED.active, seen_at=now()`,
d.ID, d.Name, nonNil(d.Specialties), nonNil(d.BranchIDs), d.KidsFrom, d.IsActive()); err != nil {
return 0, err
}
}
if len(seen) > 0 {
if _, err := tx.Exec(ctx, `UPDATE onec_doctors SET active = false WHERE NOT (onec_id = ANY($1))`, seen); err != nil {
return 0, err
}
}
// автопривязка по ФИО
rows, err := tx.Query(ctx, `SELECT id, last_name, first_name, middle_name FROM doctors WHERE deleted_at IS NULL
AND id NOT IN (SELECT doctor_id FROM onec_doctors WHERE doctor_id IS NOT NULL)`)
if err != nil {
return 0, err
}
byName := map[string][]int64{}
for rows.Next() {
var id int64
var l, f, m string
if err := rows.Scan(&id, &l, &f, &m); err != nil {
rows.Close()
return 0, err
}
k := normName(l + " " + f + " " + m)
byName[k] = append(byName[k], id)
}
rows.Close()
if err := rows.Err(); err != nil {
return 0, err
}
un, err := tx.Query(ctx, `SELECT onec_id, full_name FROM onec_doctors WHERE doctor_id IS NULL`)
if err != nil {
return 0, err
}
type pair struct {
onecID string
id int64
}
var pairs []pair
used := map[int64]bool{}
for un.Next() {
var oid, name string
if err := un.Scan(&oid, &name); err != nil {
un.Close()
return 0, err
}
if ids := byName[normName(name)]; len(ids) == 1 && !used[ids[0]] {
used[ids[0]] = true
pairs = append(pairs, pair{oid, ids[0]})
}
}
un.Close()
if err := un.Err(); err != nil {
return 0, err
}
for _, p := range pairs {
if _, err := tx.Exec(ctx, `UPDATE onec_doctors SET doctor_id=$2 WHERE onec_id=$1`, p.onecID, p.id); err != nil {
return 0, err
}
linked++
}
return linked, tx.Commit(ctx)
}
func nonNil(a []string) []string {
if a == nil {
return []string{}
}
return a
}
// SaveOnecServices заменяет услуги приёма врача.
func (s *Storage) SaveOnecServices(ctx context.Context, doctorOnecID string, list []onec.Service) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `DELETE FROM onec_services WHERE doctor_onec_id=$1`, doctorOnecID); err != nil {
return err
}
for _, sv := range list {
if _, err := tx.Exec(ctx, `INSERT INTO onec_services (doctor_onec_id, onec_id, name, duration_min, price)
VALUES ($1,$2,$3,$4,$5) ON CONFLICT DO NOTHING`, doctorOnecID, sv.ID, sv.Name, sv.DurationMin, sv.Price); err != nil {
return err
}
}
return tx.Commit(ctx)
}
// LinkedOnecDoctorIDs — идентификаторы 1С у врачей, привязанных к карточкам сайта.
func (s *Storage) LinkedOnecDoctorIDs(ctx context.Context) ([]string, error) {
rows, err := s.pool.Query(ctx, `SELECT onec_id FROM onec_doctors WHERE doctor_id IS NOT NULL AND active ORDER BY onec_id`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
out = append(out, id)
}
return out, rows.Err()
}
// ---------- Окна ----------
// ReplaceOnecSlots заменяет окна периода [from, to) тем, что отдала 1С. Всё вместе, в одной транзакции:
// пациент не увидит расписание «наполовину обновлённым». Прошедшие окна старше суток удаляются.
func (s *Storage) ReplaceOnecSlots(ctx context.Context, from, to time.Time, slots []onec.Slot) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `DELETE FROM onec_slots WHERE (start_at >= $1 AND start_at < $2) OR start_at < now() - interval '1 day'`, from, to); err != nil {
return err
}
if err := insertSlots(ctx, tx, from, to, slots); err != nil {
return err
}
return tx.Commit(ctx)
}
// ReplaceOnecDoctorSlots обновляет окна одного врача на период (перед записью).
func (s *Storage) ReplaceOnecDoctorSlots(ctx context.Context, doctorOnecID string, from, to time.Time, slots []onec.Slot) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `DELETE FROM onec_slots WHERE doctor_onec_id=$1 AND start_at >= $2 AND start_at < $3`, doctorOnecID, from, to); err != nil {
return err
}
if err := insertSlots(ctx, tx, from, to, slots); err != nil {
return err
}
return tx.Commit(ctx)
}
func insertSlots(ctx context.Context, tx pgx.Tx, from, to time.Time, slots []onec.Slot) error {
b := &pgx.Batch{}
for _, sl := range slots {
if sl.DoctorID == "" || sl.Start.Before(from) || !sl.Start.Before(to) || (sl.Status != "free" && sl.Status != "busy") {
continue
}
b.Queue(`INSERT INTO onec_slots (doctor_onec_id, branch_onec_id, start_at, end_at, cabinet, status, slot_id)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT (doctor_onec_id, branch_onec_id, start_at) DO UPDATE SET end_at=EXCLUDED.end_at,
cabinet=EXCLUDED.cabinet, status=EXCLUDED.status, slot_id=EXCLUDED.slot_id`,
sl.DoctorID, sl.BranchID, sl.Start, sl.End, sl.Cabinet, sl.Status, sl.SlotID)
}
if b.Len() == 0 {
return nil
}
br := tx.SendBatch(ctx, b)
for i := 0; i < b.Len(); i++ {
if _, err := br.Exec(); err != nil {
br.Close()
return err
}
}
return br.Close()
}
type OnecSlot struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Branch string `json:"branch"`
BranchID string `json:"branch_id"`
Cabinet string `json:"cabinet,omitempty"`
Status string `json:"status,omitempty"`
SlotID string `json:"-"`
DoctorOnec string `json:"-"`
}
// OnecDoctorBySite возвращает идентификатор врача в 1С по карточке сайта; пусто — не привязан.
func (s *Storage) OnecDoctorBySite(ctx context.Context, doctorID int64) (string, error) {
var id string
err := s.pool.QueryRow(ctx, `SELECT onec_id FROM onec_doctors WHERE doctor_id=$1 AND active`, doctorID).Scan(&id)
if wrapNotFound(err) == ErrNotFound {
return "", nil
}
return id, err
}
// OnecDoctorBySlug — то же по адресу карточки; возвращает и id карточки.
func (s *Storage) OnecDoctorBySlug(ctx context.Context, slug string) (onecID string, doctorID int64, err error) {
err = s.pool.QueryRow(ctx, `
SELECT o.onec_id, d.id FROM onec_doctors o JOIN doctors d ON d.id = o.doctor_id
WHERE d.slug=$1 AND d.published AND d.deleted_at IS NULL AND d.booking_provider='native' AND o.active`, slug).Scan(&onecID, &doctorID)
if wrapNotFound(err) == ErrNotFound {
return "", 0, nil
}
return onecID, doctorID, err
}
// FreeOnecSlots — свободные окна врача в будущем; названия филиалов берутся из справочника 1С.
func (s *Storage) FreeOnecSlots(ctx context.Context, doctorOnecID string, from, to time.Time) ([]OnecSlot, error) {
rows, err := s.pool.Query(ctx, `
SELECT sl.start_at, sl.end_at, sl.branch_onec_id, COALESCE(NULLIF(b.address, ''), b.name, ''), sl.cabinet, sl.slot_id
FROM onec_slots sl LEFT JOIN onec_branches b ON b.onec_id = sl.branch_onec_id
WHERE sl.doctor_onec_id=$1 AND sl.status='free' AND sl.start_at >= greatest($2::timestamptz, now()) AND sl.start_at < $3
ORDER BY sl.start_at, sl.branch_onec_id`, doctorOnecID, from, to)
if err != nil {
return nil, err
}
defer rows.Close()
var out []OnecSlot
for rows.Next() {
var x OnecSlot
if err := rows.Scan(&x.Start, &x.End, &x.BranchID, &x.Branch, &x.Cabinet, &x.SlotID); err != nil {
return nil, err
}
x.Start = x.Start.In(moscow)
x.End = x.End.In(moscow)
x.Status = "free"
out = append(out, x)
}
return out, rows.Err()
}
// MarkOnecSlotBusy сразу гасит окно после записи, не дожидаясь следующей синхронизации.
func (s *Storage) MarkOnecSlotBusy(ctx context.Context, doctorOnecID, branchOnecID string, start time.Time) error {
_, err := s.pool.Exec(ctx, `UPDATE onec_slots SET status='busy' WHERE doctor_onec_id=$1 AND branch_onec_id=$2 AND start_at=$3`,
doctorOnecID, branchOnecID, start)
return err
}
// OnecBranchName — подпись филиала по идентификатору 1С.
func (s *Storage) OnecBranchName(ctx context.Context, branchOnecID string) string {
var n string
_ = s.pool.QueryRow(ctx, `SELECT COALESCE(NULLIF(address, ''), name) FROM onec_branches WHERE onec_id=$1`, branchOnecID).Scan(&n)
return n
}
// OnecServiceName — название услуги приёма.
func (s *Storage) OnecServiceName(ctx context.Context, doctorOnecID, serviceOnecID string) string {
var n string
_ = s.pool.QueryRow(ctx, `SELECT name FROM onec_services WHERE doctor_onec_id=$1 AND onec_id=$2`, doctorOnecID, serviceOnecID).Scan(&n)
return n
}
// OnecServices — услуги приёма врача.
func (s *Storage) OnecServices(ctx context.Context, doctorOnecID string) ([]onec.Service, error) {
rows, err := s.pool.Query(ctx, `SELECT onec_id, name, duration_min, price FROM onec_services WHERE doctor_onec_id=$1 ORDER BY name`, doctorOnecID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []onec.Service
for rows.Next() {
var x onec.Service
if err := rows.Scan(&x.ID, &x.Name, &x.DurationMin, &x.Price); err != nil {
return nil, err
}
out = append(out, x)
}
return out, rows.Err()
}
// ---------- Журнал синхронизации ----------
func (s *Storage) StartOnecSync(ctx context.Context) (int64, error) {
var id int64
err := s.pool.QueryRow(ctx, `INSERT INTO onec_sync_log DEFAULT VALUES RETURNING id`).Scan(&id)
return id, err
}
func (s *Storage) FinishOnecSync(ctx context.Context, id int64, doctors, slots int, errText string) error {
_, err := s.pool.Exec(ctx, `UPDATE onec_sync_log SET finished_at=now(), doctors=$2, slots=$3, error=$4 WHERE id=$1`, id, doctors, slots, errText)
if err != nil {
return err
}
_, err = s.pool.Exec(ctx, `DELETE FROM onec_sync_log WHERE started_at < now() - interval '7 days'`)
return err
}
type OnecSyncStatus struct {
LastRun *time.Time `json:"last_run"`
LastOK *time.Time `json:"last_ok"`
LastError string `json:"last_error"`
Doctors int `json:"doctors"`
Linked int `json:"linked"`
FreeSlots int `json:"free_slots"`
BusySlots int `json:"busy_slots"`
Appointment int `json:"appointments"`
}
func (s *Storage) OnecSyncStatus(ctx context.Context) (OnecSyncStatus, error) {
var st OnecSyncStatus
_ = s.pool.QueryRow(ctx, `SELECT started_at, error FROM onec_sync_log WHERE finished_at IS NOT NULL ORDER BY id DESC LIMIT 1`).Scan(&st.LastRun, &st.LastError)
_ = s.pool.QueryRow(ctx, `SELECT finished_at FROM onec_sync_log WHERE finished_at IS NOT NULL AND error = '' ORDER BY id DESC LIMIT 1`).Scan(&st.LastOK)
err := s.pool.QueryRow(ctx, `
SELECT (SELECT count(*) FROM onec_doctors WHERE active),
(SELECT count(*) FROM onec_doctors WHERE active AND doctor_id IS NOT NULL),
(SELECT count(*) FROM onec_slots WHERE status='free' AND start_at >= now()),
(SELECT count(*) FROM onec_slots WHERE status='busy' AND start_at >= now()),
(SELECT count(*) FROM onec_appointments WHERE status='booked')`).
Scan(&st.Doctors, &st.Linked, &st.FreeSlots, &st.BusySlots, &st.Appointment)
return st, err
}
// OnecFresh сообщает, была ли успешная синхронизация не раньше чем maxAge назад.
func (s *Storage) OnecFresh(ctx context.Context, maxAge time.Duration) bool {
var ok bool
_ = s.pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM onec_sync_log WHERE finished_at > now() - $1::interval AND error = '')`,
maxAge.String()).Scan(&ok)
return ok
}
// ---------- Админка ----------
type OnecDoctorRow struct {
OnecID string `json:"onec_id"`
FullName string `json:"full_name"`
Specialties []string `json:"specialties"`
Active bool `json:"active"`
DoctorID *int64 `json:"doctor_id"`
DoctorName string `json:"doctor_name"`
Booking string `json:"booking"`
FreeSlots int `json:"free_slots"`
}
func (s *Storage) OnecDoctorsAdmin(ctx context.Context) ([]OnecDoctorRow, error) {
rows, err := s.pool.Query(ctx, `
SELECT o.onec_id, o.full_name, o.specialties, o.active, o.doctor_id,
COALESCE(d.last_name || ' ' || d.first_name || ' ' || d.middle_name, ''), COALESCE(d.booking_provider, ''),
(SELECT count(*) FROM onec_slots sl WHERE sl.doctor_onec_id = o.onec_id AND sl.status='free' AND sl.start_at >= now())
FROM onec_doctors o LEFT JOIN doctors d ON d.id = o.doctor_id
ORDER BY o.active DESC, (o.doctor_id IS NULL) DESC, o.full_name`)
if err != nil {
return nil, err
}
defer rows.Close()
out := []OnecDoctorRow{}
for rows.Next() {
var r OnecDoctorRow
if err := rows.Scan(&r.OnecID, &r.FullName, &r.Specialties, &r.Active, &r.DoctorID, &r.DoctorName, &r.Booking, &r.FreeSlots); err != nil {
return nil, err
}
r.DoctorName = strings.TrimSpace(r.DoctorName)
out = append(out, r)
}
return out, rows.Err()
}
// SetOnecDoctorLink привязывает врача 1С к карточке сайта (doctorID = nil — отвязывает).
// Способ записи у карточки при этом не меняется: он включается отдельно (SetOnecBooking),
// а при отвязке запись «через 1С» возвращается к обычной заявке.
func (s *Storage) SetOnecDoctorLink(ctx context.Context, onecID string, doctorID *int64) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var prev *int64
if err := tx.QueryRow(ctx, `SELECT doctor_id FROM onec_doctors WHERE onec_id=$1 FOR UPDATE`, onecID).Scan(&prev); err != nil {
return wrapNotFound(err)
}
if doctorID != nil {
// карточка уже занята другим врачом 1С — освобождаем её
if _, err := tx.Exec(ctx, `UPDATE onec_doctors SET doctor_id=NULL WHERE doctor_id=$1 AND onec_id<>$2`, *doctorID, onecID); err != nil {
return err
}
}
if _, err := tx.Exec(ctx, `UPDATE onec_doctors SET doctor_id=$2 WHERE onec_id=$1`, onecID, doctorID); err != nil {
return err
}
if prev != nil && (doctorID == nil || *doctorID != *prev) {
if _, err := tx.Exec(ctx, `UPDATE doctors SET booking_provider='none', updated_at=now() WHERE id=$1 AND booking_provider='native'`, *prev); err != nil {
return err
}
}
return tx.Commit(ctx)
}
// SetOnecBooking включает (native) или выключает (none) запись через 1С у привязанного врача.
// Врач с Medflex при включении переходит на 1С; выключение возвращает к заявке.
func (s *Storage) SetOnecBooking(ctx context.Context, onecID string, enabled bool) error {
provider := "none"
if enabled {
provider = "native"
}
tag, err := s.pool.Exec(ctx, `
UPDATE doctors SET booking_provider=$2, updated_at=now()
WHERE id = (SELECT doctor_id FROM onec_doctors WHERE onec_id=$1) AND booking_provider <> $2`, onecID, provider)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
var linked bool
if err := s.pool.QueryRow(ctx, `SELECT doctor_id IS NOT NULL FROM onec_doctors WHERE onec_id=$1`, onecID).Scan(&linked); err != nil {
return wrapNotFound(err)
}
if !linked {
return ErrNotFound
}
}
return nil
}
type OnecGridRow struct {
DoctorOnecID string `json:"doctor_onec_id"`
DoctorName string `json:"doctor_name"`
Branch string `json:"branch"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
Cabinet string `json:"cabinet"`
Status string `json:"status"`
}
// OnecGrid — окна всех врачей за сутки [day, day+24ч) для сетки в админке.
func (s *Storage) OnecGrid(ctx context.Context, day time.Time) ([]OnecGridRow, error) {
rows, err := s.pool.Query(ctx, `
SELECT sl.doctor_onec_id, COALESCE(NULLIF(o.full_name, ''), sl.doctor_onec_id),
COALESCE(NULLIF(b.address, ''), b.name, ''), sl.start_at, sl.end_at, sl.cabinet, sl.status
FROM onec_slots sl
LEFT JOIN onec_doctors o ON o.onec_id = sl.doctor_onec_id
LEFT JOIN onec_branches b ON b.onec_id = sl.branch_onec_id
WHERE sl.start_at >= $1 AND sl.start_at < $1 + interval '1 day'
ORDER BY 2, sl.start_at`, day)
if err != nil {
return nil, err
}
defer rows.Close()
out := []OnecGridRow{}
for rows.Next() {
var r OnecGridRow
if err := rows.Scan(&r.DoctorOnecID, &r.DoctorName, &r.Branch, &r.Start, &r.End, &r.Cabinet, &r.Status); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// ---------- Записи с сайта ----------
type OnecAppointment struct {
ID int64 `json:"id"`
RequestID string `json:"request_id"`
LeadID *int64 `json:"lead_id"`
DoctorOnec string `json:"doctor_onec_id"`
DoctorName string `json:"doctor_name"`
Branch string `json:"branch"`
Start time.Time `json:"start"`
End *time.Time `json:"end"`
Patient string `json:"patient_name"`
Phone string `json:"patient_phone"`
OnecID string `json:"onec_appointment_id"`
Status string `json:"status"`
Error string `json:"error"`
CreatedAt time.Time `json:"created_at"`
ServiceName string `json:"service_name"`
ServiceID string `json:"-"`
BranchID string `json:"-"`
// подтверждение оператором в Битрикс24 (см. booking_confirm.go)
ConfirmState string `json:"confirm_state"`
BitrixLeadID string `json:"bitrix_lead_id"`
ConfirmError string `json:"confirm_error"`
}
// BeginOnecAppointment создаёт запись в статусе pending. Если такой request_id уже был, возвращает прежнюю запись
// и created=false — так повтор запроса не создаёт вторую запись в 1С.
func (s *Storage) BeginOnecAppointment(ctx context.Context, a OnecAppointment) (rec OnecAppointment, created bool, err error) {
err = s.pool.QueryRow(ctx, `
INSERT INTO onec_appointments (request_id, doctor_onec_id, branch_onec_id, service_onec_id, start_at, patient_name, patient_phone)
VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (request_id) DO NOTHING RETURNING id`,
a.RequestID, a.DoctorOnec, a.BranchID, a.ServiceID, a.Start, a.Patient, a.Phone).Scan(&a.ID)
if err == nil {
a.Status = "pending"
return a, true, nil
}
if wrapNotFound(err) != ErrNotFound {
return a, false, err
}
rec, err = s.onecAppointmentByRequest(ctx, a.RequestID)
return rec, false, err
}
func (s *Storage) onecAppointmentByRequest(ctx context.Context, requestID string) (OnecAppointment, error) {
var r OnecAppointment
err := s.pool.QueryRow(ctx, `
SELECT id, request_id, lead_id, doctor_onec_id, branch_onec_id, start_at, end_at, patient_name, patient_phone,
onec_appointment_id, status, error, created_at
FROM onec_appointments WHERE request_id=$1`, requestID).
Scan(&r.ID, &r.RequestID, &r.LeadID, &r.DoctorOnec, &r.BranchID, &r.Start, &r.End, &r.Patient, &r.Phone,
&r.OnecID, &r.Status, &r.Error, &r.CreatedAt)
return r, wrapNotFound(err)
}
func (s *Storage) FinishOnecAppointment(ctx context.Context, id int64, status, onecID, errText string, end *time.Time, leadID *int64) error {
_, err := s.pool.Exec(ctx, `UPDATE onec_appointments SET status=$2, onec_appointment_id=$3, error=$4, end_at=$5,
lead_id=COALESCE($6, lead_id), updated_at=now() WHERE id=$1`, id, status, onecID, errText, end, leadID)
return err
}
func (s *Storage) OnecAppointments(ctx context.Context, limit int) ([]OnecAppointment, error) {
rows, err := s.pool.Query(ctx, `
SELECT a.id, a.request_id, a.lead_id, a.doctor_onec_id, COALESCE(NULLIF(o.full_name, ''), a.doctor_onec_id),
COALESCE(NULLIF(b.address, ''), b.name, ''), a.start_at, a.end_at, a.patient_name, a.patient_phone,
a.onec_appointment_id, a.status, a.error, a.created_at, COALESCE(sv.name, ''),
a.confirm_state, a.bitrix_lead_id, a.confirm_error
FROM onec_appointments a
LEFT JOIN onec_doctors o ON o.onec_id = a.doctor_onec_id
LEFT JOIN onec_branches b ON b.onec_id = a.branch_onec_id
LEFT JOIN onec_services sv ON sv.doctor_onec_id = a.doctor_onec_id AND sv.onec_id = a.service_onec_id
ORDER BY a.created_at DESC LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
out := []OnecAppointment{}
for rows.Next() {
var r OnecAppointment
if err := rows.Scan(&r.ID, &r.RequestID, &r.LeadID, &r.DoctorOnec, &r.DoctorName, &r.Branch, &r.Start, &r.End,
&r.Patient, &r.Phone, &r.OnecID, &r.Status, &r.Error, &r.CreatedAt, &r.ServiceName,
&r.ConfirmState, &r.BitrixLeadID, &r.ConfirmError); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// SetOnecAppointmentStatus обновляет статус записи по номеру записи в 1С (вебхук 1С).
// Возвращает врача и время приёма, чтобы пересчитать его окна.
func (s *Storage) SetOnecAppointmentStatus(ctx context.Context, onecAppointmentID, status string) (doctorOnecID string, start time.Time, found bool, err error) {
err = s.pool.QueryRow(ctx, `
UPDATE onec_appointments SET status=$2, updated_at=now()
WHERE onec_appointment_id=$1 AND status IN ('booked','arrived','cancelled')
RETURNING doctor_onec_id, start_at`, onecAppointmentID, status).Scan(&doctorOnecID, &start)
if err != nil {
if wrapNotFound(err) == ErrNotFound {
return "", time.Time{}, false, nil
}
return "", time.Time{}, false, err
}
return doctorOnecID, start, true, nil
}