Files
rmser/internal/services/sync/service.go
SERTY 542beafe0e Перевел на multi-tenant
Добавил поставщиков
Накладные успешно создаются из фронта
2025-12-18 03:56:21 +03:00

308 lines
8.6 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 sync
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"go.uber.org/zap"
"rmser/internal/domain/account"
"rmser/internal/domain/catalog"
"rmser/internal/domain/invoices"
"rmser/internal/domain/operations"
"rmser/internal/domain/recipes"
"rmser/internal/domain/suppliers"
"rmser/internal/infrastructure/rms"
"rmser/pkg/logger"
)
const (
PresetPurchases = "1a3297e1-cb05-55dc-98a7-c13f13bc85a7" // Закупки
PresetUsage = "24d9402e-2d01-eca1-ebeb-7981f7d1cb86" // Расход
)
type Service struct {
rmsFactory *rms.Factory
accountRepo account.Repository
catalogRepo catalog.Repository
recipeRepo recipes.Repository
invoiceRepo invoices.Repository
opRepo operations.Repository
supplierRepo suppliers.Repository
}
func NewService(
rmsFactory *rms.Factory,
accountRepo account.Repository,
catalogRepo catalog.Repository,
recipeRepo recipes.Repository,
invoiceRepo invoices.Repository,
opRepo operations.Repository,
supplierRepo suppliers.Repository,
) *Service {
return &Service{
rmsFactory: rmsFactory,
accountRepo: accountRepo,
catalogRepo: catalogRepo,
recipeRepo: recipeRepo,
invoiceRepo: invoiceRepo,
opRepo: opRepo,
supplierRepo: supplierRepo,
}
}
// SyncAllData запускает полную синхронизацию для конкретного пользователя
func (s *Service) SyncAllData(userID uuid.UUID) error {
logger.Log.Info("Запуск полной синхронизации", zap.String("user_id", userID.String()))
// 1. Получаем клиент и инфо о сервере
client, err := s.rmsFactory.GetClientForUser(userID)
if err != nil {
return err
}
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return fmt.Errorf("active server not found for user %s", userID)
}
serverID := server.ID
// 2. Справочники
if err := s.syncStores(client, serverID); err != nil {
logger.Log.Error("Sync Stores failed", zap.Error(err))
}
if err := s.syncMeasureUnits(client, serverID); err != nil {
logger.Log.Error("Sync Units failed", zap.Error(err))
}
// 3. Поставщики
if err := s.syncSuppliers(client, serverID); err != nil {
logger.Log.Error("Sync Suppliers failed", zap.Error(err))
}
// 4. Товары
if err := s.syncProducts(client, serverID); err != nil {
logger.Log.Error("Sync Products failed", zap.Error(err))
}
// 5. Техкарты (тяжелый запрос)
if err := s.syncRecipes(client, serverID); err != nil {
logger.Log.Error("Sync Recipes failed", zap.Error(err))
}
// 6. Накладные (история)
if err := s.syncInvoices(client, serverID); err != nil {
logger.Log.Error("Sync Invoices failed", zap.Error(err))
}
// 7. Складские операции (тяжелый запрос)
// Для MVP можно отключить, если долго грузится
// if err := s.SyncStoreOperations(client, serverID); err != nil {
// logger.Log.Error("Sync Operations failed", zap.Error(err))
// }
logger.Log.Info("Синхронизация завершена", zap.String("user_id", userID.String()))
return nil
}
func (s *Service) syncSuppliers(c rms.ClientI, serverID uuid.UUID) error {
list, err := c.FetchSuppliers()
if err != nil {
return err
}
// Проставляем ServerID
for i := range list {
list[i].RMSServerID = serverID
}
return s.supplierRepo.SaveBatch(list)
}
func (s *Service) syncStores(c rms.ClientI, serverID uuid.UUID) error {
stores, err := c.FetchStores()
if err != nil {
return err
}
for i := range stores {
stores[i].RMSServerID = serverID
}
return s.catalogRepo.SaveStores(stores)
}
func (s *Service) syncMeasureUnits(c rms.ClientI, serverID uuid.UUID) error {
units, err := c.FetchMeasureUnits()
if err != nil {
return err
}
for i := range units {
units[i].RMSServerID = serverID
}
return s.catalogRepo.SaveMeasureUnits(units)
}
func (s *Service) syncProducts(c rms.ClientI, serverID uuid.UUID) error {
products, err := c.FetchCatalog()
if err != nil {
return err
}
// Важно: Проставляем ID рекурсивно и в фасовки
for i := range products {
products[i].RMSServerID = serverID
for j := range products[i].Containers {
products[i].Containers[j].RMSServerID = serverID
}
}
return s.catalogRepo.SaveProducts(products)
}
func (s *Service) syncRecipes(c rms.ClientI, serverID uuid.UUID) error {
dateFrom := time.Now().AddDate(0, -3, 0) // За 3 месяца
dateTo := time.Now()
recipesList, err := c.FetchRecipes(dateFrom, dateTo)
if err != nil {
return err
}
for i := range recipesList {
recipesList[i].RMSServerID = serverID
for j := range recipesList[i].Items {
recipesList[i].Items[j].RMSServerID = serverID
}
}
return s.recipeRepo.SaveRecipes(recipesList)
}
func (s *Service) syncInvoices(c rms.ClientI, serverID uuid.UUID) error {
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
if err != nil {
return err
}
var from time.Time
to := time.Now()
if lastDate != nil {
from = *lastDate
} else {
from = time.Now().AddDate(0, 0, -45) // 45 дней по дефолту
}
invs, err := c.FetchInvoices(from, to)
if err != nil {
return err
}
for i := range invs {
invs[i].RMSServerID = serverID
// В Items пока не добавляли ServerID
}
if len(invs) > 0 {
return s.invoiceRepo.SaveInvoices(invs)
}
return nil
}
// SyncStoreOperations публичный, если нужно вызывать отдельно
func (s *Service) SyncStoreOperations(c rms.ClientI, serverID uuid.UUID) error {
dateTo := time.Now()
dateFrom := dateTo.AddDate(0, 0, -30)
if err := s.syncReport(c, serverID, PresetPurchases, operations.OpTypePurchase, dateFrom, dateTo); err != nil {
return fmt.Errorf("purchases sync error: %w", err)
}
if err := s.syncReport(c, serverID, PresetUsage, operations.OpTypeUsage, dateFrom, dateTo); err != nil {
return fmt.Errorf("usage sync error: %w", err)
}
return nil
}
func (s *Service) syncReport(c rms.ClientI, serverID uuid.UUID, presetID string, targetOpType operations.OperationType, from, to time.Time) error {
items, err := c.FetchStoreOperations(presetID, from, to)
if err != nil {
return err
}
var ops []operations.StoreOperation
for _, item := range items {
pID, err := uuid.Parse(item.ProductID)
if err != nil {
continue
}
realOpType := classifyOperation(item.DocumentType)
if realOpType == operations.OpTypeUnknown || realOpType != targetOpType {
continue
}
ops = append(ops, operations.StoreOperation{
RMSServerID: serverID,
ProductID: pID,
OpType: realOpType,
DocumentType: item.DocumentType,
TransactionType: item.TransactionType,
DocumentNumber: item.DocumentNum,
Amount: decimal.NewFromFloat(item.Amount),
Sum: decimal.NewFromFloat(item.Sum),
Cost: decimal.NewFromFloat(item.Cost),
PeriodFrom: from,
PeriodTo: to,
})
}
return s.opRepo.SaveOperations(ops, serverID, targetOpType, from, to)
}
func classifyOperation(docType string) operations.OperationType {
switch docType {
case "INCOMING_INVOICE", "INCOMING_SERVICE":
return operations.OpTypePurchase
case "SALES_DOCUMENT", "WRITEOFF_DOCUMENT", "OUTGOING_INVOICE", "SESSION_ACCEPTANCE", "DISASSEMBLE_DOCUMENT":
return operations.OpTypeUsage
default:
return operations.OpTypeUnknown
}
}
// Добавляем структуру для возврата статистики
type SyncStats struct {
ServerName string
ProductsCount int64
StoresCount int64
SuppliersCount int64
InvoicesLast30 int64
LastInvoice *time.Time
}
// GetSyncStats собирает информацию о данных текущего сервера
func (s *Service) GetSyncStats(userID uuid.UUID) (*SyncStats, error) {
server, err := s.accountRepo.GetActiveServer(userID)
if err != nil || server == nil {
return nil, fmt.Errorf("нет активного сервера")
}
stats := &SyncStats{
ServerName: server.Name,
}
// Параллельный запуск не обязателен, запросы Count очень быстрые
if cnt, err := s.catalogRepo.CountGoods(server.ID); err == nil {
stats.ProductsCount = cnt
}
if cnt, err := s.catalogRepo.CountStores(server.ID); err == nil {
stats.StoresCount = cnt
}
if cnt, err := s.supplierRepo.Count(server.ID); err == nil {
stats.SuppliersCount = cnt
}
if cnt, err := s.invoiceRepo.CountRecent(server.ID, 30); err == nil {
stats.InvoicesLast30 = cnt
}
stats.LastInvoice, _ = s.invoiceRepo.GetLastInvoiceDate(server.ID)
return stats, nil
}