dana-medical-site/internal/api/booking_confirm_test.go
Abdulkadyr Karagishiev 121f286016 feat(crm): integrate website leads and online booking with new CRM and 1C
- Add internal/crm client with CanonicalJSON and HMAC-SHA256 signing for POST /api/public/leads
- Add database migration 0026_crm_deals.sql with crm_deal_id and partial indexes
- Add async lead intake dispatch to new CRM in handlers_leads.go
- Support dual-mode booking confirmation (CRM_MODE=both/crm_only/bitrix_only) in booking_confirm.go
- Add LinkCRM endpoint to bridge_1c.py and onec.Client
- Update config and env examples with CRM_API_URL, CRM_LEAD_INTAKE_SECRET, CRM_MODE
2026-10-16 18:01:49 +03:00

183 lines
7.8 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 (
"strings"
"testing"
"time"
"dana/internal/bitrix"
"dana/internal/storage"
)
func TestConfirmDeadline(t *testing.T) {
cfg := storage.DefaultBookingSettings // 24 ч после записи или 2 ч до приёма
created := time.Date(2026, 10, 9, 22, 0, 0, 0, moscow)
cases := []struct {
name string
start time.Time
want time.Time
}{
{"приём через три дня — 24 ч после записи", created.Add(72 * time.Hour), created.Add(24 * time.Hour)},
{"приём завтра утром — за 2 ч до приёма", created.Add(12 * time.Hour), created.Add(10 * time.Hour)},
{"приём через час — не раньше 30 минут после записи", created.Add(time.Hour), created.Add(30 * time.Minute)},
}
for _, c := range cases {
got, ok := confirmDeadline(created, c.start, cfg)
if !ok || !got.Equal(c.want) {
t.Errorf("%s: срок %v, ждали %v", c.name, got, c.want)
}
}
if _, ok := confirmDeadline(created, created.Add(72*time.Hour), storage.BookingSettings{}); ok {
t.Error("оба правила выключены — автоотмены быть не должно")
}
}
func TestDecideConfirm(t *testing.T) {
cfg := storage.DefaultBookingSettings
created := time.Date(2026, 10, 9, 10, 0, 0, 0, moscow)
start := created.Add(48 * time.Hour)
soon := created.Add(time.Hour)
late := created.Add(25 * time.Hour) // срок (24 ч) прошёл, приём ещё впереди
cases := []struct {
name string
semantic string
now time.Time
want confirmAction
}{
{"оператор подтвердил", bitrix.SemanticSuccess, soon, confirmOK},
{"оператор отменил", bitrix.SemanticFailure, soon, confirmCancel},
{"в работе, срок не вышел", bitrix.SemanticProcess, soon, confirmWait},
{"лид удалён, срок не вышел", "", soon, confirmWait},
{"в работе, срок вышел", bitrix.SemanticProcess, late, confirmExpire},
{"подтвердили даже после срока — подтверждаем", bitrix.SemanticSuccess, late, confirmOK},
{"провал после начала приёма — в 1С не трогаем", bitrix.SemanticFailure, start.Add(time.Minute), confirmMissed},
{"никто не ответил до приёма — в 1С не трогаем", bitrix.SemanticProcess, start, confirmMissed},
}
for _, c := range cases {
if got := decideConfirm(c.semantic, created, start, c.now, cfg); got != c.want {
t.Errorf("%s: %v, ждали %v", c.name, got, c.want)
}
}
if got := decideConfirm(bitrix.SemanticProcess, created, start, late, storage.BookingSettings{}); got != confirmWait {
t.Errorf("автоотмена выключена: %v, ждали ожидание", got)
}
}
func TestLeadOnlyDraftAsksToBookManually(t *testing.T) {
it := storage.ConfirmItem{
Start: time.Date(2026, 10, 12, 15, 30, 0, 0, moscow), Patient: "Анна", Phone: "+7 928 000 00 00",
DoctorName: "Петрова <script>", OnecID: "",
}
d := leadOnlyDraft(it)
if !strings.HasPrefix(d.Title, "Заявка на запись: ") || d.SourceID != bitrixSiteSourceID || d.Phone != "+79280000000" {
t.Errorf("заголовок/источник/телефон: %+v", d)
}
for _, want := range []string{"НЕ закреплено", "запишите его в 1С вручную", "12.10.2026 в 15:30", "&lt;script&gt;"} {
if !strings.Contains(d.Comments, want) {
t.Errorf("в комментарии нет %q: %q", want, d.Comments)
}
}
if strings.Contains(d.Comments, "<script>") || strings.Contains(d.Comments, "№ записи в 1С") {
t.Errorf("лишнее в комментарии: %q", d.Comments)
}
}
func TestLeadDraftEscapesPatientInput(t *testing.T) {
it := storage.ConfirmItem{
Start: time.Date(2026, 10, 12, 15, 30, 0, 0, moscow), CreatedAt: time.Date(2026, 10, 9, 10, 0, 0, 0, moscow),
Patient: "Анна", Phone: "8 (928) 000-00-00", DoctorName: "Иванов <b>И.</b>", Branch: "ул. Ленина, 1",
ServiceName: "Консультация", OnecID: "A-17",
}
d := leadDraft(it, storage.DefaultBookingSettings)
if d.Phone != "+79280000000" {
t.Errorf("телефон не нормализован: %q", d.Phone)
}
if strings.Contains(d.Comments, "<b>") || !strings.Contains(d.Comments, "&lt;b&gt;") {
t.Errorf("HTML пациента/врача не экранирован: %q", d.Comments)
}
for _, want := range []string{"12.10.2026 в 15:30", "A-17", "Консультация", "Клиент записался", "отменится автоматически 10.10 в 10:00"} {
if !strings.Contains(d.Comments, want) {
t.Errorf("в комментарии нет %q: %q", want, d.Comments)
}
}
if d.SourceID != bitrixSiteSourceID || !strings.HasPrefix(d.Title, "Онлайн-запись: ") {
t.Errorf("источник/заголовок: %+v", d)
}
}
func TestCRMDraftsFormatting(t *testing.T) {
it := storage.ConfirmItem{
ID: 42,
Start: time.Date(2026, 10, 12, 15, 30, 0, 0, moscow),
CreatedAt: time.Date(2026, 10, 9, 10, 0, 0, 0, moscow),
Patient: "Иван Тестов",
Phone: "8 (928) 123-45-67",
DoctorName: "Доктор Тестов",
Branch: "ул. Ленина, 10",
ServiceName: "УЗИ брюшной полости",
OnecID: "DOC-123",
}
crmDraft := bookingConfirmToCRMDraft(it, storage.DefaultBookingSettings)
if crmDraft.LocalLeadId != "booking_42" {
t.Errorf("expected LocalLeadId booking_42, got %s", crmDraft.LocalLeadId)
}
if crmDraft.Phone != "+79281234567" {
t.Errorf("expected normalized phone +79281234567, got %s", crmDraft.Phone)
}
if crmDraft.SourceKey != "site_booking" {
t.Errorf("expected SourceKey site_booking, got %s", crmDraft.SourceKey)
}
for _, expectedText := range []string{"Онлайн-запись с сайта", "Доктор Тестов", "УЗИ брюшной полости", "DOC-123"} {
if !strings.Contains(crmDraft.Comment, expectedText) {
t.Errorf("expected comment to contain %q, got:\n%s", expectedText, crmDraft.Comment)
}
}
leadOnly := leadOnlyToCRMDraft(it)
if leadOnly.LocalLeadId != "booking_failed_42" {
t.Errorf("expected LocalLeadId booking_failed_42, got %s", leadOnly.LocalLeadId)
}
if leadOnly.SourceKey != "site_booking_fallback" {
t.Errorf("expected SourceKey site_booking_fallback, got %s", leadOnly.SourceKey)
}
if !strings.Contains(leadOnly.Comment, "запишите его в 1С вручную") {
t.Errorf("expected manual booking note in fallback comment, got:\n%s", leadOnly.Comment)
}
leadItem := storage.LeadCRMItem{
ID: 101,
FormTitle: "Запись на консультацию",
Name: "Мария",
Phone: "89285554433",
Email: "maria@example.com",
Service: "Консультация кардиолога",
Doctor: "Кардиолог К.К.",
Branch: "Махачкала, ул. Ляхова 22",
Message: "Хотелось бы попасть в субботу",
PageURL: "https://danamedical.ru/doctors/kardiolog",
UTM: map[string]string{
"utm_source": "yandex",
"utm_medium": "cpc",
},
CreatedAt: time.Date(2026, 10, 9, 11, 0, 0, 0, moscow),
}
siteDraft := leadItemToCRMDraft(leadItem)
if siteDraft.LocalLeadId != "site_lead_101" {
t.Errorf("expected site_lead_101, got %s", siteDraft.LocalLeadId)
}
if siteDraft.Phone != "+79285554433" {
t.Errorf("expected +79285554433, got %s", siteDraft.Phone)
}
if siteDraft.PagePath != "/doctors/kardiolog" {
t.Errorf("expected /doctors/kardiolog, got %s", siteDraft.PagePath)
}
if siteDraft.UtmSource != "yandex" || siteDraft.UtmMedium != "cpc" {
t.Errorf("UTM not properly preserved: %+v", siteDraft)
}
if !strings.Contains(siteDraft.Comment, "Форма: Запись на консультацию") {
t.Errorf("Comment missing form title: %s", siteDraft.Comment)
}
}