package api import ( "bytes" "context" "fmt" "io" "net/http" "os" "reflect" "strings" "testing" "time" "dana/internal/sberhealth" ) func sberTestRows(records ...[]string) [][]string { rows := [][]string{{"СберЗдоровье"}, {}, {}, {}, {}, {}, {"#", "Дата создания записи", "Дата приема", "Пациент", "Телефон", "Врач", "Специальность", "Предоплата", "Сумма предоплаты", "Статус", "Филиал", "Источник", "Причина отказа"}} rows = append(rows, records...) return append(rows, nil, []string{fmt.Sprintf("Количество элементов: %d", len(records))}, []string{"Дата формирования отчета: 05.10.2026 12:37"}) } func TestSberHealthCreationDateDedupAndFooter(t *testing.T) { from := time.Date(2026, 9, 28, 0, 0, 0, 0, moscow) to := time.Date(2026, 10, 6, 0, 0, 0, 0, moscow) phones, records, total, err := parseSberHealthRows(sberTestRows( []string{"1", "27.09.2026 23:59", "28.09.2026 12:00", "Тест", "79281230001"}, []string{"2", "28.09.2026 00:00", "25.10.2026 12:00", "Тест", "+7 (928) 123-00-02¶"}, []string{"3", "05.10.2026 23:59", "", "Тест", "89281230002"}, []string{"4", "06.10.2026 00:00", "", "Тест", "79281230003"}, ), from, to) if err != nil || records != 2 || total != 4 || !reflect.DeepEqual(phones, []string{"9281230002"}) { t.Fatal("creation filter, inclusive last day, dedup or footer incorrect", err, records, total) } empty, count, total, err := parseSberHealthRows(sberTestRows(), from, to) if err != nil || len(empty) != 0 || count != 0 || total != 0 { t.Fatal("valid empty report rejected", err) } } func TestSberHealthInvalidAndIncompleteExports(t *testing.T) { from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) to := from.AddDate(0, 1, 0) valid := []string{"1", "05.10.2026 12:30", "", "Тест", "79281230001"} truncated := sberTestRows(valid) truncated[len(truncated)-2][0] = "Количество элементов: 20" missingFooter := sberTestRows(valid)[:8] duplicate := sberTestRows(valid, valid) badPhone := sberTestRows([]string{"1", "05.10.2026 12:30", "", "Тест", "private-invalid-phone"}) badDate := sberTestRows([]string{"1", "private-invalid-date", "", "Тест", "79281230001"}) for _, rows := range [][][]string{truncated, missingFooter, duplicate, badPhone, badDate, {{"unknown format"}}} { phones, _, _, err := parseSberHealthRows(rows, from, to) if err == nil || len(phones) != 0 || strings.Contains(err.Error(), "private-invalid") { t.Fatal("bad/truncated export accepted or private data leaked") } } } func TestSberHealthMergeUsesOnly1CRevenue(t *testing.T) { from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) phones, records, total, err := parseSberHealthRows(sberTestRows( []string{"1", "05.10.2026 12:30", "", "Тест", "79281230001", "", "", "Да", "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 := &sberhealthSourceInfo{Records: records, Phones: len(phones), ExportRecords: total} _, result, err := (&API{}).phoneMatchOpts(context.Background(), &progressTracker{}, "book.xlsx", book, matchOpts{pbxLists: lists, sberhealthInfo: info}) if err != nil { t.Fatal(err) } summary := result.(channelsSummary) channel := channelByName(summary, "Сберздоровье") if channel.Leads != 2 || channel.Patients != 2 || channel.Revenue != 3500 || channel.Services != 3 || summary.SberHealth != info { t.Fatal("merge or 1C-only accounting incorrect") } } // Opt-in local regression against the user's original sample. Never copied to // testdata/repository, never printed, and no external requests are performed. func TestSberHealthSuppliedXLSSample(t *testing.T) { path := os.Getenv("DANA_SBER_SAMPLE_FILE") if path == "" { t.Skip("local source sample not supplied") } data, err := os.ReadFile(path) if err != nil { t.Fatal("cannot read local sample") } rows, err := sberhealth.ReadXLS(data) if err != nil { t.Fatal(err) } from := time.Date(2026, 9, 28, 0, 0, 0, 0, moscow) to := time.Date(2026, 10, 6, 0, 0, 0, 0, moscow) phones, records, total, err := parseSberHealthRows(rows, from, to) if err != nil { t.Fatal(err) } if len(phones) != 6 || records != 7 || total != 7 { t.Fatal("sample does not reconcile with independent XLS inspection") } // Exercise the real integration on local bytes through a fake transport, // verifying the exclusive 1C end becomes an inclusive provider date. previous := http.DefaultTransport defer func() { http.DefaultTransport = previous }() http.DefaultTransport = prodoctorovTestTransport(func(r *http.Request) (*http.Response, error) { q := r.URL.Query() if r.URL.Host != "docdoc.ru" || q.Get("clinic_id") != "294888" || q.Get("crDateFrom") != "28.09.2026" || q.Get("crDateTill") != "05.10.2026" || q.Get("periodInput") != "week" { t.Fatal("incorrect inclusive provider date mapping") } return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(bytes.NewReader(data))}, nil }) c, _ := sberhealth.New("session=synthetic", "294888") opts, err := (&API{sberhealth: c}).sberhealthMatchOpts(context.Background(), from, to) if err != nil || opts.sberhealthInfo == nil || opts.sberhealthInfo.Records != 7 || opts.sberhealthInfo.Phones != 6 || opts.sberhealthInfo.ExportRecords != 7 { t.Fatal("sample integration failed", err) } } func TestSberHealthMonthlyRequestAndNoPartialDataOnFailure(t *testing.T) { previous := http.DefaultTransport defer func() { http.DefaultTransport = previous }() http.DefaultTransport = prodoctorovTestTransport(func(r *http.Request) (*http.Response, error) { q := r.URL.Query() if q.Get("periodInput") != "month" || q.Get("crDateFrom") != "01.10.2026" || q.Get("crDateTill") != "31.10.2026" { t.Fatal("monthly date conversion failed") } return &http.Response{StatusCode: 403, Header: make(http.Header), Body: http.NoBody}, nil }) c, _ := sberhealth.New("session=synthetic", "294888") from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) opts, err := (&API{sberhealth: c}).sberhealthMatchOpts(context.Background(), from, from.AddDate(0, 1, 0)) if err == nil || opts.sberhealthInfo != nil || len(opts.pbxLists) != 0 { t.Fatal("failed download returned successful empty/partial options") } }