package storage import ( "context" "strings" "time" "github.com/jackc/pgx/v5" "dana/internal/onec" ) // ---------- Справочники ---------- // 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.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 }