package api import ( "bytes" "mime/multipart" "net/http" "net/http/httptest" "reflect" "strings" "testing" "time" ) // Книга ПроДокторова как в кабинете: во второй строке месяц выгрузки («… - октября 2026»). func prodoctorovMonthBook(t *testing.T, monthLine string, records ...[]string) []byte { t.Helper() rows := [][]string{{"«Дана Медикал» тестовый филиал"}, {monthLine}, {}, {"№", "Дата", "Пациент", "Информация", "Специальность", "Запись на", "Цена приёма, р.", "Сумма, р.", "Оплата"}} return xlsxBook(t, map[string][][]string{"Sheet": append(rows, records...)}, []string{"Sheet"}) } // Телефонная выгрузка ПроДокторова: вместо «Информация … Запись на» — «Экспресс-запись». func prodoctorovPhoneBook(t *testing.T, clinic, monthLine string, records ...[]string) []byte { t.Helper() rows := [][]string{{clinic}, {monthLine}, {}, {"№", "Дата", "Пациент", "Экспресс-запись", "Сумма, р.", "Оплата"}} return xlsxBook(t, map[string][][]string{"Sheet": append(rows, records...)}, []string{"Sheet"}) } func TestProdoctorovPhoneExportAndBothKindsPerMonth(t *testing.T) { from := time.Date(2026, 9, 1, 0, 0, 0, 0, moscow) to := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) online := prodoctorovMonthBook(t, "запись на приём (в МИС) - сентября 2026 ", []string{"11", "03.09.2026 10:00", "79281230011", "", "", "05.09.2026 10:00"}, []string{"12", "04.09.2026 10:00", "79281230099", "", "", "06.09.2026 10:00"}, ) phone := prodoctorovPhoneBook(t, "«Дана Медикал» на Ляхова", "запись на приём (телефон) - сентября 2026 ", []string{"21", "10.09.2026 12:30", "79281230021", "нет", "500", "оплачено"}, []string{"22", "11.09.2026 12:30", "79281230099", "нет", "500", "оплачено"}, // тот же номер, что в онлайн-записи ) // одна телефонная выгрузка читается сама по себе opts, err := prodoctorovUploadOpts([][]byte{phone}, from, to) if err != nil { t.Fatalf("телефонная выгрузка не принята: %v", err) } if got := opts.pbxLists["продокторов"]; !reflect.DeepEqual(got, []string{"9281230021", "9281230099"}) { t.Fatalf("телефоны из телефонной выгрузки: %v", got) } // онлайн и телефон за один месяц — можно, номера объединяются без дублей opts, err = prodoctorovUploadOpts([][]byte{online, phone}, from, to) if err != nil { t.Fatalf("оба вида за месяц не приняты: %v", err) } want := []string{"9281230011", "9281230021", "9281230099"} if got := opts.pbxLists["продокторов"]; !reflect.DeepEqual(got, want) { t.Fatalf("телефоны %v, ожидали %v", got, want) } if i := opts.prodoctorovInfo; i.Months != 1 || i.Records != 4 || i.Phones != 3 { t.Fatalf("сводка: %+v", i) } // две телефонные выгрузки за один месяц — по-прежнему дубль if _, err := prodoctorovUploadOpts([][]byte{phone, phone}, from, to); err == nil || !strings.Contains(err.Error(), "телефонная запись") { t.Fatalf("дубль телефонной выгрузки принят: %v", err) } // выгрузка другой клиники (у «Очевидно» такие же файлы) не должна попасть в отчёт DANA other := prodoctorovPhoneBook(t, "Офтальмологический центр «Очевидно»", "запись на приём (телефон) - сентября 2026 ", []string{"31", "10.09.2026 12:30", "79281230031", "нет", "500", "оплачено"}) if _, err := prodoctorovUploadOpts([][]byte{other}, from, to); err == nil || !strings.Contains(err.Error(), "другой клиники") { t.Fatalf("файл чужой клиники принят: %v", err) } } func TestProdoctorovUploadCoversEveryMonthOfPeriod(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) // неделя на стыке сентября и октября sep := prodoctorovMonthBook(t, "запись на приём (в МИС) - сентября 2026 ", []string{"1", "27.09.2026 10:00", "79281230001", "", "", "30.09.2026 10:00"}, // до периода []string{"2", "29.09.2026 10:00", "79281230002", "", "", "01.10.2026 10:00"}, ) oct := prodoctorovMonthBook(t, "запись на приём (в МИС) - октября 2026 ", []string{"3", "02.10.2026 10:00", "+7 (928) 123-00-03", "", "", "03.10.2026 10:00"}, []string{"4", "06.10.2026 00:00", "79281230004", "", "", "07.10.2026 10:00"}, // правая граница исключается ) // Только октябрь: сентябрь нужен периоду, молча терять его записи нельзя. if _, err := prodoctorovUploadOpts([][]byte{oct}, from, to); err == nil || !strings.Contains(err.Error(), "сентябрь 2026") { t.Fatalf("пропущенный месяц не замечен: %v", err) } opts, err := prodoctorovUploadOpts([][]byte{oct, sep}, from, to) if err != nil { t.Fatal(err) } want := []string{"9281230002", "9281230003"} if got := opts.pbxLists["продокторов"]; !reflect.DeepEqual(got, want) { t.Fatalf("телефоны %v, ожидали %v", got, want) } if i := opts.prodoctorovInfo; i == nil || i.Months != 2 || i.Records != 2 || i.Phones != 2 { t.Fatalf("сводка по источнику неверна: %+v", i) } nov := prodoctorovMonthBook(t, "запись на приём (в МИС) - ноября 2026 ") if _, err := prodoctorovUploadOpts([][]byte{oct, sep, nov}, from, to); err == nil || !strings.Contains(err.Error(), "не относится") { t.Fatalf("лишний месяц принят: %v", err) } if _, err := prodoctorovUploadOpts([][]byte{oct, oct, sep}, from, to); err == nil || !strings.Contains(err.Error(), "два файла") { t.Fatalf("дубль месяца принят: %v", err) } noMonth := xlsxBook(t, map[string][][]string{"Sheet": {{"«Дана Медикал»"}, {"без месяца"}}}, []string{"Sheet"}) if _, err := prodoctorovUploadOpts([][]byte{noMonth}, from, to); err == nil { t.Fatal("файл без месяца принят") } } func TestProdoctorovUploadValidEmptyMonthStaysExplicitChannel(t *testing.T) { from := time.Date(2026, 10, 1, 0, 0, 0, 0, moscow) to := time.Date(2026, 11, 1, 0, 0, 0, 0, moscow) opts, err := prodoctorovUploadOpts([][]byte{prodoctorovMonthBook(t, "запись на приём (в МИС) - октября 2026 ")}, from, to) if err != nil { t.Fatal(err) } if got, ok := opts.pbxLists["продокторов"]; !ok || len(got) != 0 || opts.prodoctorovInfo.Records != 0 { t.Fatal("пустой месяц должен давать явный пустой канал, а не «не подключено»") } } func multipartRequest(t *testing.T, parts map[string][]struct{ name, body string }) *http.Request { t.Helper() var buf bytes.Buffer w := multipart.NewWriter(&buf) for field, files := range parts { for _, f := range files { fw, err := w.CreateFormFile(field, f.name) if err != nil { t.Fatal(err) } _, _ = fw.Write([]byte(f.body)) } } _ = w.Close() r := httptest.NewRequest(http.MethodPost, "/api/v1/admin/reports", &buf) r.Header.Set("Content-Type", w.FormDataContentType()) if err := r.ParseMultipartForm(1 << 20); err != nil { t.Fatal(err) } return r } type part = struct{ name, body string } func TestReadPartnerUploadsValidation(t *testing.T) { ole := string(oleSignature) + strings.Repeat("x", 600) zip := string(zipSignature) + strings.Repeat("x", 600) up, err := readPartnerUploads(multipartRequest(t, map[string][]part{ "sber_file": {{"Report_20261005_171611.xls", ole}}, "prodoctorov_file": {{"Appointment-2026-9.xlsx", zip}, {"Appointment-2026-10.xlsx", zip}}, })) if err != nil || len(up.sber) == 0 || len(up.prodoctorov) != 2 || !up.any() { t.Fatalf("корректные файлы отклонены: %v", err) } if up, err := readPartnerUploads(multipartRequest(t, nil)); err != nil || up.any() { t.Fatalf("без файлов должно быть пусто: %v", err) } for name, parts := range map[string]map[string][]part{ "Сбер не xls": {"sber_file": {{"report.xlsx", ole}}}, "Сбер вместо Excel HTML": {"sber_file": {{"report.xls", "login" + strings.Repeat(" ", 600)}}}, "Сбер два файла": {"sber_file": {{"a.xls", ole}, {"b.xls", ole}}}, "ПД не xlsx": {"prodoctorov_file": {{"a.xls", zip}}}, "ПД вместо Excel HTML": {"prodoctorov_file": {{"a.xlsx", "403" + strings.Repeat(" ", 600)}}}, "ПД семь файлов": {"prodoctorov_file": {{"1.xlsx", zip}, {"2.xlsx", zip}, {"3.xlsx", zip}, {"4.xlsx", zip}, {"5.xlsx", zip}, {"6.xlsx", zip}, {"7.xlsx", zip}}}, } { if _, err := readPartnerUploads(multipartRequest(t, parts)); err == nil { t.Errorf("%s: файл принят", name) } } }