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 }