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

142 lines
5.1 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 (
"context"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
"dana/internal/sberhealth"
)
type sberhealthSourceInfo struct {
From string `json:"from"`
To string `json:"to"`
Records int `json:"records"` // creation date inside 1C period
Phones int `json:"phones"` // unique keys before 1C stub exclusions
ExportRecords int `json:"export_records"` // reconciled with XLS footer
}
func (a *API) WithSberHealth(c *sberhealth.Client) { a.sberhealth = c }
func (a *API) sberhealthMatchOpts(ctx context.Context, from, to time.Time) (matchOpts, error) {
if !a.sberhealth.Configured() {
return matchOpts{}, fmt.Errorf("СберЗдоровье не подключено")
}
from, to = from.In(moscow), to.In(moscow)
if !to.After(from) {
return matchOpts{}, fmt.Errorf("СберЗдоровье: некорректный период 1С")
}
mode := "week"
if describeReportPeriod(from, to).Mode == "months" {
mode = "month"
}
// Confirmed sample includes requests created on crDateTill: provider's end
// date is inclusive. 1C integrations internally use an exclusive end.
export, err := a.sberhealth.Download(ctx, from, to.AddDate(0, 0, -1), mode)
if err != nil {
return matchOpts{}, err
}
if export.Format != "xls" {
return matchOpts{}, fmt.Errorf("СберЗдоровье: формат выгрузки изменился; ожидается проверенный бинарный XLS")
}
rows, err := sberhealth.ReadXLS(export.Data)
if err != nil {
return matchOpts{}, err
}
phones, records, total, err := parseSberHealthRows(rows, from, to)
if err != nil {
return matchOpts{}, err
}
info := &sberhealthSourceInfo{From: from.Format("2006-01-02"), To: to.AddDate(0, 0, -1).Format("2006-01-02"),
Records: records, Phones: len(phones), ExportRecords: total}
return matchOpts{pbxLists: map[string][]string{"сберздоровье": phones}, sberhealthInfo: info}, nil
}
var sberCountRE = regexp.MustCompile(`(?i)^количество\s+элементов\s*:\s*(\d+)\s*$`)
// The source sheet's schema is confirmed against Report_20261005_123725.xls:
// row 7 headers; B creation date, E phone; footer declares element count.
// All statuses/sources are retained, matching the user's -1 export filters.
// Appointment costs/prepayments never enter revenue: money comes only from 1C.
func parseSberHealthRows(rows [][]string, from, to time.Time) ([]string, int, int, error) {
header, idCol, dateCol, phoneCol := -1, -1, -1, -1
for i, row := range rows {
if i >= 15 {
break
}
id, date, phone := -1, -1, -1
for j, value := range row {
switch strings.ToLower(strings.TrimSpace(value)) {
case "#":
id = j
case "дата создания записи":
date = j
case "телефон":
phone = j
}
}
if id >= 0 && date >= 0 && phone >= 0 {
header, idCol, dateCol, phoneCol = i, id, date, phone
break
}
}
if header < 0 {
return nil, 0, 0, fmt.Errorf("СберЗдоровье: нет столбцов #, Дата создания записи и Телефон")
}
keys, ids := map[string]bool{}, map[string]bool{}
records, total, declared := 0, 0, -1
footer := false
for i := header + 1; i < len(rows); i++ {
row := rows[i]
id, date, phone := strings.TrimSpace(cell(row, idCol)), strings.TrimSpace(cell(row, dateCol)), strings.TrimSpace(cell(row, phoneCol))
if strings.TrimSpace(strings.Join(row, "")) == "" {
continue
}
if date == "" && phone == "" {
if m := sberCountRE.FindStringSubmatch(id); len(m) > 0 && declared < 0 {
declared, _ = strconv.Atoi(m[1])
footer = true
continue
}
if footer && strings.HasPrefix(strings.ToLower(id), "дата формирования отчета:") {
continue
}
return nil, 0, 0, fmt.Errorf("СберЗдоровье: неизвестная служебная строка %d", i+1)
}
if footer {
return nil, 0, 0, fmt.Errorf("СберЗдоровье: записи после итоговой строки")
}
created, err := time.ParseInLocation("02.01.2006 15:04", date, moscow)
idNumber, idErr := strconv.ParseUint(id, 10, 64)
if err != nil || idErr != nil || idNumber == 0 || ids[id] {
return nil, 0, 0, fmt.Errorf("СберЗдоровье: неверная дата, номер или дубль записи в строке %d", i+1)
}
ids[id] = true
total++
if created.Before(from) || !created.Before(to) {
continue
}
phones, bad := phoneKeys(phone)
if len(phones) == 0 || bad {
return nil, 0, 0, fmt.Errorf("СберЗдоровье: не распознан телефон в строке %d", i+1)
}
records++
for _, key := range phones {
keys[key] = true
}
}
if declared < 0 || declared != total {
return nil, 0, 0, fmt.Errorf("СберЗдоровье: число строк не совпало с «Количество элементов»; проверьте полноту выгрузки")
}
phones := make([]string, 0, len(keys))
for key := range keys {
phones = append(phones, key)
}
sort.Strings(phones)
return phones, records, total, nil
}