dana-medical-site/internal/api/report_partner_uploads_test.go
2026-10-14 13:33:13 +00:00

182 lines
9.3 KiB
Go
Raw Permalink 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 (
"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", "<html>login</html>" + strings.Repeat(" ", 600)}}},
"Сбер два файла": {"sber_file": {{"a.xls", ole}, {"b.xls", ole}}},
"ПД не xlsx": {"prodoctorov_file": {{"a.xls", zip}}},
"ПД вместо Excel HTML": {"prodoctorov_file": {{"a.xlsx", "<html>403</html>" + 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)
}
}
}