package api import ( "bytes" "context" "io" "net/http" "reflect" "strings" "testing" "time" "dana/internal/prodoctorov" ) func prodoctorovTestBook(t *testing.T, records ...[]string) []byte { t.Helper() rows := [][]string{{"Онлайн-записи"}, {"Синтетическая клиника"}, {}, {"№", "Дата", "Пациент", "Информация", "Специальность", "Запись на", "Цена приёма, р.", "Сумма, р.", "Оплата"}} return xlsxBook(t, map[string][][]string{"Sheet": append(rows, records...)}, []string{"Sheet"}) } func TestProdoctorovCreationDateAndDeduplication(t *testing.T) { from := time.Date(2026, 10, 5, 0, 0, 0, 0, moscow) to := from.AddDate(0, 0, 7) book := prodoctorovTestBook(t, []string{"1", "04.10.2026 23:59", "79281230001", "", "", "06.10.2026 12:30"}, // visit in period, creation outside []string{"2", "05.10.2026 00:00", "+7 (928) 123-00-02¶", "", "", "25.10.2026 12:30"}, // creation in period, visit outside []string{"3", "11.10.2026 23:59", "89281230002", "", "", "25.10.2026 12:30"}, // duplicate phone []string{"4", "12.10.2026 00:00", "79281230003", "", "", "12.10.2026 12:30"}, // exclusive end ) phones, records, err := parseProdoctorovExport(book, from, to) if err != nil || records != 2 || !reflect.DeepEqual(phones, []string{"9281230002"}) { t.Fatal("creation date, boundaries or deduplication incorrect", err, records) } } func TestProdoctorovRejectsChangedFormatAndBadRows(t *testing.T) { from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) to := from.AddDate(0, 1, 0) for _, book := range [][]byte{ []byte("login"), xlsxBook(t, map[string][][]string{"Sheet": {{"wrong format"}}}, []string{"Sheet"}), prodoctorovTestBook(t, []string{"1", "private-invalid-date", "79281230001"}), prodoctorovTestBook(t, []string{"1", "05.10.2026 12:30", "private-invalid-phone"}), } { _, _, err := parseProdoctorovExport(book, from, to) if err == nil || strings.Contains(err.Error(), "private-invalid") { t.Fatal("invalid export accepted or private row leaked") } } phones, records, err := parseProdoctorovExport(prodoctorovTestBook(t), from, to) if err != nil || len(phones) != 0 || records != 0 { t.Fatal("valid empty export rejected", err) } } // Route the production fixed-host client to a synthetic transport, never the // actual cabinet. This also verifies requests use the supplied LPU explicitly. type prodoctorovTestTransport func(*http.Request) (*http.Response, error) func (f prodoctorovTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestProdoctorovOverlappingMonths(t *testing.T) { previous := http.DefaultTransport defer func() { http.DefaultTransport = previous }() var requested []string // Use XLSX bodies below; no real request/cookie/data ever leaves this test. http.DefaultTransport = prodoctorovTestTransport(func(r *http.Request) (*http.Response, error) { if r.URL.Host != "prodoctorov.ru" || r.URL.Query().Get("lpu") != "82347" { t.Fatal("wrong host or clinic") } requested = append(requested, r.URL.Query().Get("year")+"-"+r.URL.Query().Get("month")) body := prodoctorovTestBook(t) return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(body))}, nil }) c, _ := prodoctorov.New("session=synthetic", "82347") a := &API{prodoctorov: c} from := time.Date(2026, 12, 28, 0, 0, 0, 0, moscow) to := time.Date(2027, 1, 4, 0, 0, 0, 0, moscow) o, err := a.prodoctorovMatchOpts(context.Background(), from, to) if err != nil || !reflect.DeepEqual(requested, []string{"2026-12", "2027-1"}) || o.prodoctorovInfo.Months != 2 { t.Fatal("cross-year month selection incorrect", err, requested) } requested = nil _, err = a.prodoctorovMatchOpts(context.Background(), time.Date(2026, 10, 1, 0, 0, 0, 0, moscow), time.Date(2026, 11, 1, 0, 0, 0, 0, moscow)) if err != nil || !reflect.DeepEqual(requested, []string{"2026-10"}) { t.Fatal("exclusive month end fetched an extra month", err, requested) } } func TestProdoctorovMergesWithExistingChannelAndUsesOnly1CRevenue(t *testing.T) { from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) phones, records, err := parseProdoctorovExport(prodoctorovTestBook(t, []string{"1", "05.10.2026 12:30", "79281230001", "", "", "06.10.2026 12:30", "999999", "999999"}, []string{"2", "05.10.2026 12:30", "79281230002"}, ), from, from.AddDate(0, 1, 0)) if err != nil { t.Fatal(err) } book := xlsxBook(t, map[string][][]string{ "Лист_1": {{"Пациент", "Мобильный телефон", "Количество", "СуммаИтого"}, {"Тестов Один", "79281230001", "2", "1500"}, {"Другой Два", "79281230002", "1", "2000"}, {"Итого", "", "3", "3500"}}, "Продокторов": {{"79281230001"}}, }, []string{"Лист_1", "Продокторов"}) lists := mergeChannelLists(map[string][]string{"продокторов": {"89281230001"}}, map[string][]string{"продокторов": phones}) info := &prodoctorovSourceInfo{Records: records, Phones: len(phones)} _, summary, err := (&API{}).phoneMatchOpts(context.Background(), &progressTracker{}, "book.xlsx", book, matchOpts{pbxLists: lists, prodoctorovInfo: info}) if err != nil { t.Fatal(err) } s := summary.(channelsSummary) c := channelByName(s, "Продокторов") if c.Leads != 2 || c.Patients != 2 || c.Services != 3 || c.Revenue != 3500 || s.Prodoctorov != info { t.Fatalf("merged channel or 1C accounting incorrect: %+v", c) } } func TestProdoctorovFailedSecondMonthDoesNotReturnPartialData(t *testing.T) { previous := http.DefaultTransport defer func() { http.DefaultTransport = previous }() http.DefaultTransport = prodoctorovTestTransport(func(r *http.Request) (*http.Response, error) { if r.URL.Query().Get("month") == "11" { return &http.Response{StatusCode: 403, Header: make(http.Header), Body: http.NoBody}, nil } body := prodoctorovTestBook(t, []string{"1", "31.10.2026 12:30", "79281230001"}) return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(body))}, nil }) c, _ := prodoctorov.New("session=synthetic", "82347") from := time.Date(2026, 10, 26, 0, 0, 0, 0, moscow) to := time.Date(2026, 11, 2, 0, 0, 0, 0, moscow) o, err := (&API{prodoctorov: c}).prodoctorovMatchOpts(context.Background(), from, to) if err == nil || o.prodoctorovInfo != nil || len(o.pbxLists) != 0 { t.Fatal("failed monthly export returned partial successful data") } }