mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
338 lines
9.9 KiB
Go
338 lines
9.9 KiB
Go
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, force bool) error {
|
||
logger.Log.Info("Запуск синхронизации", zap.String("user_id", userID.String()), zap.Bool("force", force))
|
||
|
||
// 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, force); 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
|
||
}
|
||
|
||
// SyncInvoicesOnly запускает синхронизацию только накладных для конкретного пользователя
|
||
func (s *Service) SyncInvoicesOnly(userID uuid.UUID) error {
|
||
logger.Log.Info("Запуск синхронизации накладных", zap.String("user_id", userID.String()))
|
||
|
||
// Получаем клиент и инфо о сервере
|
||
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
|
||
|
||
// Синхронизация накладных
|
||
if err := s.syncInvoices(client, serverID, false); err != nil {
|
||
logger.Log.Error("Sync Invoices failed", zap.Error(err))
|
||
return 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, force bool) error {
|
||
var from time.Time
|
||
to := time.Now()
|
||
|
||
if force {
|
||
// Принудительная перезагрузка за последние 40 дней
|
||
from = time.Now().AddDate(0, 0, -40)
|
||
logger.Log.Info("Force sync invoices", zap.String("from", from.Format("2006-01-02")))
|
||
} else {
|
||
lastDate, err := s.invoiceRepo.GetLastInvoiceDate(serverID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if lastDate != nil {
|
||
from = lastDate.AddDate(0, 0, -7)
|
||
} else {
|
||
from = time.Now().AddDate(0, 0, -45)
|
||
}
|
||
}
|
||
|
||
invs, err := c.FetchInvoices(from, to)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
for i := range invs {
|
||
invs[i].RMSServerID = serverID
|
||
}
|
||
|
||
if len(invs) > 0 {
|
||
// Репозиторий использует OnConflict(UpdateAll), поэтому существующие записи обновятся
|
||
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
|
||
}
|