142 lines
5.1 KiB
Go
142 lines
5.1 KiB
Go
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
|
||
}
|