Files
rmser/internal/infrastructure/rms/client.go
SERTY 4e4571b3db Настройки работают
Иерархия групп работает
Полностью завязано на пользователя и серверы
2025-12-18 07:21:31 +03:00

852 lines
25 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 rms
import (
"bytes"
"crypto/sha1"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/internal/domain/catalog"
"rmser/internal/domain/invoices"
"rmser/internal/domain/recipes"
"rmser/internal/domain/suppliers"
"rmser/pkg/logger"
)
const (
tokenTTL = 45 * time.Minute // Время жизни токена до принудительного обновления
)
// ClientI интерфейс
type ClientI interface {
Auth() error
Logout() error
FetchCatalog() ([]catalog.Product, error)
FetchStores() ([]catalog.Store, error)
FetchSuppliers() ([]suppliers.Supplier, error)
FetchMeasureUnits() ([]catalog.MeasureUnit, error)
FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error)
FetchInvoices(from, to time.Time) ([]invoices.Invoice, error)
FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error)
CreateIncomingInvoice(inv invoices.Invoice) (string, error)
GetProductByID(id uuid.UUID) (*ProductFullDTO, error)
UpdateProduct(product ProductFullDTO) (*ProductFullDTO, error)
}
type Client struct {
baseURL string
login string
passwordHash string
httpClient *http.Client
// Защита токена для конкурентного доступа
mu sync.RWMutex
token string
tokenCreatedAt time.Time
}
func NewClient(baseURL, login, password string) *Client {
h := sha1.New()
h.Write([]byte(password))
passHash := fmt.Sprintf("%x", h.Sum(nil))
return &Client{
baseURL: baseURL,
login: login,
passwordHash: passHash,
httpClient: &http.Client{Timeout: 60 * time.Second},
}
}
// Auth выполняет вход
func (c *Client) Auth() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.authUnsafe()
}
// authUnsafe - внутренняя логика авторизации без блокировок (вызывается внутри Lock)
func (c *Client) authUnsafe() error {
endpoint := c.baseURL + "/resto/api/auth"
data := url.Values{}
data.Set("login", c.login)
data.Set("pass", c.passwordHash)
req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
if err != nil {
return fmt.Errorf("ошибка создания запроса auth: %w", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("ошибка сети auth: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("ошибка авторизации (code %d): %s", resp.StatusCode, string(body))
}
c.token = string(body)
c.tokenCreatedAt = time.Now() // Запоминаем время получения
logger.Log.Info("RMS: Успешная авторизация", zap.String("token_preview", c.token[:5]+"..."))
return nil
}
// Logout освобождает лицензию
func (c *Client) Logout() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.logoutUnsafe()
}
// logoutUnsafe - внутренняя логика логаута
func (c *Client) logoutUnsafe() error {
if c.token == "" {
return nil
}
endpoint := c.baseURL + "/resto/api/logout"
data := url.Values{}
data.Set("key", c.token)
req, err := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
if err == nil {
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := c.httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
logger.Log.Info("RMS: Токен освобожден")
} else {
logger.Log.Warn("RMS: Ошибка освобождения токена", zap.Int("code", resp.StatusCode))
}
}
}
// Сбрасываем токен в любом случае, даже если запрос не прошел (он все равно протухнет)
c.token = ""
c.tokenCreatedAt = time.Time{}
return nil
}
// ensureToken проверяет срок жизни токена и обновляет его при необходимости
func (c *Client) ensureToken() error {
c.mu.RLock()
token := c.token
createdAt := c.tokenCreatedAt
c.mu.RUnlock()
// Если токена нет или он протух
if token == "" || time.Since(createdAt) > tokenTTL {
c.mu.Lock()
defer c.mu.Unlock()
// Double check locking (вдруг другая горутина уже обновила)
if c.token != "" && time.Since(c.tokenCreatedAt) <= tokenTTL {
return nil
}
if c.token != "" {
logger.Log.Info("RMS: Время жизни токена истекло (>45 мин), пересоздание...")
_ = c.logoutUnsafe() // Пытаемся освободить старый
}
return c.authUnsafe()
}
return nil
}
// doRequest выполняет запрос с автоматическим управлением токеном
func (c *Client) doRequest(method, path string, queryParams map[string]string) (*http.Response, error) {
// 1. Проверка времени жизни (45 минут)
if err := c.ensureToken(); err != nil {
return nil, err
}
// Читаем токен под RLock
c.mu.RLock()
currentToken := c.token
c.mu.RUnlock()
buildURL := func() string {
u, _ := url.Parse(c.baseURL + path)
q := u.Query()
q.Set("key", currentToken)
for k, v := range queryParams {
q.Set(k, v)
}
u.RawQuery = q.Encode()
return u.String()
}
req, _ := http.NewRequest(method, buildURL(), nil)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
// 2. Реактивная обработка 401 (если сервер перезагрузился или убил сессию раньше времени)
if resp.StatusCode == http.StatusUnauthorized {
resp.Body.Close()
logger.Log.Warn("RMS: Получен 401 Unauthorized, принудительная ре-авторизация...")
c.mu.Lock()
// Сбрасываем токен и логинимся заново
c.token = ""
authErr := c.authUnsafe()
c.mu.Unlock()
if authErr != nil {
return nil, authErr
}
// Повторяем запрос с новым токеном
c.mu.RLock()
currentToken = c.token
c.mu.RUnlock()
req, _ = http.NewRequest(method, buildURL(), nil)
return c.httpClient.Do(req)
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("api error: code=%d, body=%s", resp.StatusCode, string(body))
}
return resp, nil
}
// --- Методы получения данных (без изменений логики парсинга) ---
func (c *Client) FetchCatalog() ([]catalog.Product, error) {
var products []catalog.Product
// Группы
respGroups, err := c.doRequest("GET", "/resto/api/v2/entities/products/group/list", map[string]string{
"includeDeleted": "true",
})
if err != nil {
return nil, fmt.Errorf("get groups error: %w", err)
}
defer respGroups.Body.Close()
var groupDTOs []GroupDTO
if err := json.NewDecoder(respGroups.Body).Decode(&groupDTOs); err != nil {
return nil, fmt.Errorf("json decode groups error: %w", err)
}
// Товары
respProds, err := c.doRequest("GET", "/resto/api/v2/entities/products/list", map[string]string{
"includeDeleted": "true",
})
if err != nil {
return nil, fmt.Errorf("get products error: %w", err)
}
defer respProds.Body.Close()
var prodDTOs []ProductDTO
if err := json.NewDecoder(respProds.Body).Decode(&prodDTOs); err != nil {
return nil, fmt.Errorf("json decode products error: %w", err)
}
// Маппинг групп
for _, g := range groupDTOs {
id, _ := uuid.Parse(g.ID)
var parentID *uuid.UUID
if g.ParentID != nil {
if pid, err := uuid.Parse(*g.ParentID); err == nil {
parentID = &pid
}
}
products = append(products, catalog.Product{
ID: id,
ParentID: parentID,
Name: g.Name,
Num: g.Num,
Code: g.Code,
Type: "GROUP",
IsDeleted: g.Deleted,
})
}
// Маппинг товаров
for _, p := range prodDTOs {
id, _ := uuid.Parse(p.ID)
var parentID *uuid.UUID
if p.ParentID != nil {
if pid, err := uuid.Parse(*p.ParentID); err == nil {
parentID = &pid
}
}
// Обработка MainUnit
var mainUnitID *uuid.UUID
if p.MainUnit != nil {
if uid, err := uuid.Parse(*p.MainUnit); err == nil {
mainUnitID = &uid
}
}
// Маппинг фасовок
var containers []catalog.ProductContainer
for _, contDto := range p.Containers {
cID, err := uuid.Parse(contDto.ID)
if err == nil {
containers = append(containers, catalog.ProductContainer{
ID: cID,
ProductID: id,
Name: contDto.Name,
Count: decimal.NewFromFloat(contDto.Count),
})
}
}
products = append(products, catalog.Product{
ID: id,
ParentID: parentID,
Name: p.Name,
Num: p.Num,
Code: p.Code,
Type: p.Type,
UnitWeight: decimal.NewFromFloat(p.UnitWeight),
UnitCapacity: decimal.NewFromFloat(p.UnitCapacity),
MainUnitID: mainUnitID,
Containers: containers,
IsDeleted: p.Deleted,
})
}
return products, nil
}
// FetchStores загружает список складов (Account -> INVENTORY_ASSETS)
func (c *Client) FetchStores() ([]catalog.Store, error) {
resp, err := c.doRequest("GET", "/resto/api/v2/entities/list", map[string]string{
"rootType": "Account",
"includeDeleted": "false",
})
if err != nil {
return nil, fmt.Errorf("get stores error: %w", err)
}
defer resp.Body.Close()
var dtos []AccountDTO
if err := json.NewDecoder(resp.Body).Decode(&dtos); err != nil {
return nil, fmt.Errorf("json decode stores error: %w", err)
}
var stores []catalog.Store
for _, d := range dtos {
// Фильтруем только склады
if d.Type != "INVENTORY_ASSETS" {
continue
}
id, err := uuid.Parse(d.ID)
if err != nil {
continue
}
var parentCorpID uuid.UUID
if d.ParentCorporateID != nil {
if parsed, err := uuid.Parse(*d.ParentCorporateID); err == nil {
parentCorpID = parsed
}
}
stores = append(stores, catalog.Store{
ID: id,
Name: d.Name,
ParentCorporateID: parentCorpID,
IsDeleted: d.Deleted,
})
}
return stores, nil
}
// FetchMeasureUnits загружает справочник единиц измерения
func (c *Client) FetchMeasureUnits() ([]catalog.MeasureUnit, error) {
// rootType=MeasureUnit согласно документации iiko
resp, err := c.doRequest("GET", "/resto/api/v2/entities/list", map[string]string{
"rootType": "MeasureUnit",
"includeDeleted": "false",
})
if err != nil {
return nil, fmt.Errorf("get measure units error: %w", err)
}
defer resp.Body.Close()
var dtos []GenericEntityDTO
if err := json.NewDecoder(resp.Body).Decode(&dtos); err != nil {
return nil, fmt.Errorf("json decode error: %w", err)
}
var result []catalog.MeasureUnit
for _, d := range dtos {
id, err := uuid.Parse(d.ID)
if err != nil {
continue
}
result = append(result, catalog.MeasureUnit{
ID: id,
Name: d.Name,
Code: d.Code,
})
}
return result, nil
}
func (c *Client) FetchRecipes(dateFrom, dateTo time.Time) ([]recipes.Recipe, error) {
params := map[string]string{
"dateFrom": dateFrom.Format("2006-01-02"),
}
if !dateTo.IsZero() {
params["dateTo"] = dateTo.Format("2006-01-02")
}
resp, err := c.doRequest("GET", "/resto/api/v2/assemblyCharts/getAll", params)
if err != nil {
return nil, fmt.Errorf("get recipes error: %w", err)
}
defer resp.Body.Close()
var apiResp AssemblyChartsResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return nil, fmt.Errorf("json decode recipes error: %w", err)
}
var allrecipes []recipes.Recipe
for _, chart := range apiResp.AssemblyCharts {
rID, _ := uuid.Parse(chart.ID)
pID, _ := uuid.Parse(chart.AssembledProductID)
df, _ := time.Parse("2006-01-02", chart.DateFrom)
var dt *time.Time
if chart.DateTo != nil {
if t, err := time.Parse("2006-01-02", *chart.DateTo); err == nil {
dt = &t
}
}
var items []recipes.RecipeItem
for _, item := range chart.Items {
iPID, _ := uuid.Parse(item.ProductID)
// FIX: Генерируем уникальный ID для каждой строки в нашей БД,
// чтобы избежать конфликтов PK при переиспользовании строк в iiko.
items = append(items, recipes.RecipeItem{
ID: uuid.New(),
RecipeID: rID,
ProductID: iPID,
AmountIn: decimal.NewFromFloat(item.AmountIn),
AmountOut: decimal.NewFromFloat(item.AmountOut),
})
}
allrecipes = append(allrecipes, recipes.Recipe{
ID: rID,
ProductID: pID,
DateFrom: df,
DateTo: dt,
Items: items,
})
}
return allrecipes, nil
}
func (c *Client) FetchInvoices(from, to time.Time) ([]invoices.Invoice, error) {
params := map[string]string{
"from": from.Format("2006-01-02"),
"to": to.Format("2006-01-02"),
"currentYear": "false",
}
resp, err := c.doRequest("GET", "/resto/api/documents/export/incomingInvoice", params)
if err != nil {
return nil, fmt.Errorf("get invoices error: %w", err)
}
defer resp.Body.Close()
var xmlData IncomingInvoiceListXML
if err := xml.NewDecoder(resp.Body).Decode(&xmlData); err != nil {
return nil, fmt.Errorf("xml decode invoices error: %w", err)
}
var allinvoices []invoices.Invoice
for _, doc := range xmlData.Documents {
docID, _ := uuid.Parse(doc.ID)
supID, _ := uuid.Parse(doc.Supplier)
storeID, _ := uuid.Parse(doc.DefaultStore)
dateInc, _ := time.Parse("2006-01-02T15:04:05", doc.DateIncoming)
var items []invoices.InvoiceItem
for _, it := range doc.Items {
pID, _ := uuid.Parse(it.Product)
items = append(items, invoices.InvoiceItem{
InvoiceID: docID,
ProductID: pID,
Amount: decimal.NewFromFloat(it.Amount),
Price: decimal.NewFromFloat(it.Price),
Sum: decimal.NewFromFloat(it.Sum),
VatSum: decimal.NewFromFloat(it.VatSum),
})
}
allinvoices = append(allinvoices, invoices.Invoice{
ID: docID,
DocumentNumber: doc.DocumentNumber,
DateIncoming: dateInc,
SupplierID: supID,
DefaultStoreID: storeID,
Status: doc.Status,
Items: items,
})
}
return allinvoices, nil
}
// FetchStoreOperations загружает складской отчет по ID пресета
func (c *Client) FetchStoreOperations(presetID string, from, to time.Time) ([]StoreReportItemXML, error) {
params := map[string]string{
"presetId": presetID,
"dateFrom": from.Format("02.01.2006"), // В документации формат DD.MM.YYYY
"dateTo": to.Format("02.01.2006"),
}
resp, err := c.doRequest("GET", "/resto/api/reports/storeOperations", params)
if err != nil {
return nil, fmt.Errorf("fetch store operations error: %w", err)
}
defer resp.Body.Close()
var report StoreReportResponse
if err := xml.NewDecoder(resp.Body).Decode(&report); err != nil {
// Иногда RMS возвращает пустой ответ или ошибку текстом при отсутствии данных
return nil, fmt.Errorf("xml decode store operations error: %w", err)
}
return report.Items, nil
}
// CreateIncomingInvoice отправляет накладную в iiko
func (c *Client) CreateIncomingInvoice(inv invoices.Invoice) (string, error) {
// 1. Маппинг Domain -> XML DTO
// Статус по умолчанию NEW, если не передан
status := inv.Status
if status == "" {
status = "NEW"
}
// Комментарий по умолчанию, если пустой
comment := inv.Comment
if comment == "" {
comment = "Loaded via RMSER OCR"
}
reqDTO := IncomingInvoiceImportXML{
DocumentNumber: inv.DocumentNumber,
DateIncoming: inv.DateIncoming.Format("02.01.2006"),
DefaultStore: inv.DefaultStoreID.String(),
Supplier: inv.SupplierID.String(),
Status: status,
Comment: comment,
}
if inv.ID != uuid.Nil {
reqDTO.ID = inv.ID.String()
}
for i, item := range inv.Items {
amount, _ := item.Amount.Float64()
price, _ := item.Price.Float64()
sum, _ := item.Sum.Float64()
xmlItem := IncomingInvoiceImportItemXML{
ProductID: item.ProductID.String(),
Amount: amount,
Price: price,
Sum: sum,
Num: i + 1,
Store: inv.DefaultStoreID.String(),
}
if item.ContainerID != nil && *item.ContainerID != uuid.Nil {
xmlItem.ContainerId = item.ContainerID.String()
}
reqDTO.ItemsWrapper.Items = append(reqDTO.ItemsWrapper.Items, xmlItem)
}
// 2. Маршалинг в XML
xmlBytes, err := xml.Marshal(reqDTO)
if err != nil {
return "", fmt.Errorf("xml marshal error: %w", err)
}
// Добавляем XML header вручную
xmlPayload := []byte(xml.Header + string(xmlBytes))
// 3. Получение токена
if err := c.ensureToken(); err != nil {
return "", err
}
c.mu.RLock()
token := c.token
c.mu.RUnlock()
// 4. Формирование URL
endpoint, _ := url.Parse(c.baseURL + "/resto/api/documents/import/incomingInvoice")
q := endpoint.Query()
q.Set("key", token)
endpoint.RawQuery = q.Encode()
fullURL := endpoint.String()
// --- ЛОГИРОВАНИЕ ЗАПРОСА (URL + BODY) ---
// Логируем как Info, чтобы точно увидеть в консоли при отладке
logger.Log.Info("RMS POST Request Debug",
zap.String("method", "POST"),
zap.String("url", fullURL),
zap.String("body_payload", string(xmlPayload)),
)
// 5. Отправка
req, err := http.NewRequest("POST", fullURL, bytes.NewReader(xmlPayload))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/xml")
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("network error: %w", err)
}
defer resp.Body.Close()
// Читаем ответ
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// Логируем ответ для симметрии
logger.Log.Info("RMS POST Response Debug",
zap.Int("status_code", resp.StatusCode),
zap.String("response_body", string(respBody)),
)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("http error %d: %s", resp.StatusCode, string(respBody))
}
var result DocumentValidationResult
if err := xml.Unmarshal(respBody, &result); err != nil {
return "", fmt.Errorf("xml response unmarshal error: %w", err)
}
if !result.Valid {
logger.Log.Warn("RMS Invoice Import Failed",
zap.String("error", result.ErrorMessage),
zap.String("additional", result.AdditionalInfo),
)
return "", fmt.Errorf("iiko validation failed: %s (info: %s)", result.ErrorMessage, result.AdditionalInfo)
}
return result.DocumentNumber, nil
}
// GetProductByID получает полную структуру товара по ID (через /list?ids=...)
func (c *Client) GetProductByID(id uuid.UUID) (*ProductFullDTO, error) {
// Параметр ids должен быть списком. iiko ожидает ids=UUID
params := map[string]string{
"ids": id.String(),
"includeDeleted": "false",
}
resp, err := c.doRequest("GET", "/resto/api/v2/entities/products/list", params)
if err != nil {
return nil, fmt.Errorf("request error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("rms error code %d", resp.StatusCode)
}
// Ответ - это массив товаров
var products []ProductFullDTO
if err := json.NewDecoder(resp.Body).Decode(&products); err != nil {
return nil, fmt.Errorf("json decode error: %w", err)
}
if len(products) == 0 {
return nil, fmt.Errorf("product not found in rms")
}
return &products[0], nil
}
// UpdateProduct отправляет полную структуру товара на обновление (/update)
func (c *Client) UpdateProduct(product ProductFullDTO) (*ProductFullDTO, error) {
// Маршалим тело
bodyBytes, err := json.Marshal(product)
if err != nil {
return nil, fmt.Errorf("json marshal error: %w", err)
}
// Используем doRequestPost (надо реализовать или вручную, т.к. doRequest у нас GET-ориентирован в текущем коде был прост)
// Расширим логику doRequest или напишем тут, т.к. это POST с JSON body
if err := c.ensureToken(); err != nil {
return nil, err
}
c.mu.RLock()
token := c.token
c.mu.RUnlock()
endpoint := c.baseURL + "/resto/api/v2/entities/products/update?key=" + token
req, err := http.NewRequest("POST", endpoint, bytes.NewReader(bodyBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("update failed (code %d): %s", resp.StatusCode, string(respBody))
}
var result UpdateEntityResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("response unmarshal error: %w", err)
}
if result.Result != "SUCCESS" {
// Собираем ошибки
errMsg := "rms update error: "
for _, e := range result.Errors {
errMsg += fmt.Sprintf("[%s] %s; ", e.Code, e.Value)
}
return nil, fmt.Errorf(errMsg)
}
if result.Response == nil {
return nil, fmt.Errorf("empty response from rms after update")
}
return result.Response, nil
}
// GetServerInfo пытается получить информацию о сервере (имя, версия) без авторизации.
// Использует endpoint /resto/getServerMonitoringInfo.jsp
func GetServerInfo(baseURL string) (*ServerMonitoringInfoDTO, error) {
// Формируем URL. Убираем слэш в конце, если есть.
url := strings.TrimRight(baseURL, "/") + "/resto/getServerMonitoringInfo.jsp"
logger.Log.Info("RMS: Requesting server info", zap.String("url", url))
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
logger.Log.Error("RMS: Monitoring connection failed", zap.Error(err))
return nil, fmt.Errorf("connection error: %w", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body error: %w", err)
}
logger.Log.Info("RMS: Monitoring Response",
zap.Int("status", resp.StatusCode),
zap.String("body", string(bodyBytes)))
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code %d", resp.StatusCode)
}
var info ServerMonitoringInfoDTO
// Пробуем JSON (так как в логе пришел JSON)
if err := json.Unmarshal(bodyBytes, &info); err != nil {
// Если вдруг JSON не прошел, можно попробовать XML как фоллбек (для старых версий)
logger.Log.Warn("RMS: JSON decode failed, trying XML...", zap.Error(err))
if xmlErr := xml.Unmarshal(bodyBytes, &info); xmlErr != nil {
return nil, fmt.Errorf("decode error (json & xml failed): %w", err)
}
}
return &info, nil
}
// FetchSuppliers загружает список поставщиков через XML API
func (c *Client) FetchSuppliers() ([]suppliers.Supplier, error) {
// Endpoint /resto/api/suppliers
resp, err := c.doRequest("GET", "/resto/api/suppliers", nil)
if err != nil {
return nil, fmt.Errorf("get suppliers error: %w", err)
}
defer resp.Body.Close()
var xmlData SuppliersListXML
if err := xml.NewDecoder(resp.Body).Decode(&xmlData); err != nil {
return nil, fmt.Errorf("xml decode suppliers error: %w", err)
}
var result []suppliers.Supplier
for _, emp := range xmlData.Employees {
id, err := uuid.Parse(emp.ID)
if err != nil {
continue
}
isDeleted := emp.Deleted == "true"
result = append(result, suppliers.Supplier{
ID: id,
Name: emp.Name,
Code: emp.Code,
INN: emp.TaxpayerIdNumber,
IsDeleted: isDeleted,
// RMSServerID проставляется в сервисе перед сохранением
})
}
return result, nil
}