package main import ( "fmt" "log" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/shopspring/decimal" "go.uber.org/zap" "rmser/config" "rmser/internal/domain/catalog" "rmser/internal/domain/invoices" "rmser/internal/infrastructure/db" "rmser/internal/infrastructure/ocr_client" tgBot "rmser/internal/transport/telegram" // Репозитории (инфраструктура) catalogPkg "rmser/internal/infrastructure/repository/catalog" invoicesPkg "rmser/internal/infrastructure/repository/invoices" ocrRepoPkg "rmser/internal/infrastructure/repository/ocr" opsRepoPkg "rmser/internal/infrastructure/repository/operations" recipesPkg "rmser/internal/infrastructure/repository/recipes" recRepoPkg "rmser/internal/infrastructure/repository/recommendations" "rmser/internal/infrastructure/rms" invServicePkg "rmser/internal/services/invoices" // Сервис накладных ocrServicePkg "rmser/internal/services/ocr" recServicePkg "rmser/internal/services/recommend" "rmser/internal/services/sync" "rmser/internal/transport/http/handlers" // Хендлеры "rmser/pkg/logger" ) func main() { // 1. Загрузка конфигурации cfg, err := config.LoadConfig(".") if err != nil { log.Fatalf("Ошибка загрузки конфига: %v", err) } // OCR Client pyClient := ocr_client.NewClient(cfg.OCR.ServiceURL) // 2. Инициализация логгера logger.Init(cfg.App.Mode) defer logger.Log.Sync() logger.Log.Info("Запуск приложения rmser", zap.String("mode", cfg.App.Mode)) // 3a. Подключение Redis (Новое) // redisClient, err := redis.NewClient(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB) // if err != nil { // logger.Log.Fatal("Ошибка подключения к Redis", zap.Error(err)) // } // 3. Подключение к БД (PostgreSQL) database := db.NewPostgresDB(cfg.DB.DSN) // 4. Инициализация слоев rmsClient := rms.NewClient(cfg.RMS.BaseURL, cfg.RMS.Login, cfg.RMS.Password) catalogRepo := catalogPkg.NewRepository(database) recipesRepo := recipesPkg.NewRepository(database) invoicesRepo := invoicesPkg.NewRepository(database) opsRepo := opsRepoPkg.NewRepository(database) recRepo := recRepoPkg.NewRepository(database) ocrRepo := ocrRepoPkg.NewRepository(database) syncService := sync.NewService(rmsClient, catalogRepo, recipesRepo, invoicesRepo, opsRepo) recService := recServicePkg.NewService(recRepo) ocrService := ocrServicePkg.NewService(ocrRepo, catalogRepo, pyClient) invoiceService := invServicePkg.NewService(rmsClient) // --- Инициализация Handler'ов --- invoiceHandler := handlers.NewInvoiceHandler(invoiceService) ocrHandler := handlers.NewOCRHandler(ocrService) recommendHandler := handlers.NewRecommendationsHandler(recService) // --- БЛОК ПРОВЕРКИ СИНХРОНИЗАЦИИ (Run-once on start) --- go func() { logger.Log.Info(">>> Запуск тестовой синхронизации...") // 1. Каталог if err := syncService.SyncCatalog(); err != nil { logger.Log.Error("Ошибка синхронизации каталога", zap.Error(err)) } else { logger.Log.Info("<<< Каталог успешно синхронизирован") } // 2. Техкарты if err := syncService.SyncRecipes(); err != nil { logger.Log.Error("Ошибка синхронизации техкарт", zap.Error(err)) } else { logger.Log.Info("<<< Техкарты успешно синхронизированы") } // 3. Накладные if err := syncService.SyncInvoices(); err != nil { logger.Log.Error("Ошибка синхронизации накладных", zap.Error(err)) } else { logger.Log.Info("<<< Накладные успешно синхронизированы") } // 4. Складские операции if err := syncService.SyncStoreOperations(); err != nil { logger.Log.Error("Ошибка синхронизации операций", zap.Error(err)) } else { logger.Log.Info("<<< Операции успешно синхронизированы") } logger.Log.Info(">>> Запуск расчета рекомендаций...") if err := recService.RefreshRecommendations(); err != nil { logger.Log.Error("Ошибка расчета рекомендаций", zap.Error(err)) } else { // Для отладки можно вывести пару штук recs, _ := recService.GetRecommendations() logger.Log.Info("<<< Анализ завершен", zap.Int("found", len(recs))) } // === ТЕСТ ОТПРАВКИ НАКЛАДНОЙ === // Запускаем через небольшую паузу, чтобы логи не перемешались // time.Sleep(2 * time.Second) // runManualInvoiceTest(rmsClient, catalogRepo) // =============================== }() // ------------------------------------------------------- // Запуск бота (в отдельной горутине, т.к. Start() блокирует поток) if cfg.Telegram.Token != "" { bot, err := tgBot.NewBot(cfg.Telegram, ocrService) if err != nil { logger.Log.Fatal("Ошибка создания Telegram бота", zap.Error(err)) } go bot.Start() defer bot.Stop() // Graceful shutdown } else { logger.Log.Warn("Telegram token не задан, бот не запущен") } // 5. Запуск HTTP сервера (Gin) if cfg.App.Mode == "release" { gin.SetMode(gin.ReleaseMode) } r := gin.Default() // --- Настройка CORS --- // Разрешаем запросы с любых источников для разработки Frontend corsConfig := cors.DefaultConfig() corsConfig.AllowAllOrigins = true // В продакшене заменить на AllowOrigins: []string{"http://domain.com"} corsConfig.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"} corsConfig.AllowHeaders = []string{"Origin", "Content-Length", "Content-Type", "Authorization"} r.Use(cors.New(corsConfig)) api := r.Group("/api") { // Invoices api.POST("/invoices/send", invoiceHandler.SendInvoice) // Recommendations api.GET("/recommendations", recommendHandler.GetRecommendations) // OCR api.GET("/ocr/catalog", ocrHandler.GetCatalog) api.GET("/ocr/matches", ocrHandler.GetMatches) api.POST("/ocr/match", ocrHandler.SaveMatch) api.GET("/ocr/unmatched", ocrHandler.GetUnmatched) } // Простой хелсчек r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{ "status": "ok", "time": time.Now().Format(time.RFC3339), }) }) logger.Log.Info("Сервер запускается", zap.String("port", cfg.App.Port)) if err := r.Run(":" + cfg.App.Port); err != nil { logger.Log.Fatal("Ошибка запуска сервера", zap.Error(err)) } } // runManualInvoiceTest создает тестовую накладную и отправляет её в RMS func runManualInvoiceTest(client rms.ClientI, catRepo catalog.Repository) { logger.Log.Info(">>> [TEST] Начало теста создания накладной...") // === НАСТРОЙКИ ТЕСТА === const ( MyStoreGUID = "1239d270-1bbe-f64f-b7ea-5f00518ef508" // <-- ВАШ СКЛАД MySupplierGUID = "780aa87e-2688-4f99-915b-7924ca392ac1" // <-- ВАШ ПОСТАВЩИК MyProductGUID = "607a1e96-f539-45d2-8709-3919f94bdc3e" // <-- ВАШ ТОВАР (Опционально) ) // ======================= // 1. Поиск товара var targetProduct catalog.Product var found bool // Если задан конкретный ID товара в константе - ищем его if MyProductGUID != "00000000-0000-0000-0000-000000000000" { products, _ := catRepo.GetAll() // В реальном коде лучше GetByID, но у нас repo пока простой targetUUID := uuid.MustParse(MyProductGUID) for _, p := range products { if p.ID == targetUUID { targetProduct = p found = true break } } if !found { logger.Log.Error("[TEST] Товар с указанным MyProductGUID не найден в локальной БД") return } } else { // Иначе ищем первый попавшийся GOODS products, err := catRepo.GetAll() if err != nil || len(products) == 0 { logger.Log.Error("[TEST] БД пуста. Выполните SyncCatalog.") return } for _, p := range products { // Ищем именно товар (GOODS) или заготовку (PREPARED), но не группу и не блюдо, если нужно if p.Type == "GOODS" { targetProduct = p found = true break } } if !found { logger.Log.Warn("[TEST] Не найдено товаров с типом GOODS. Берем первый попавшийся.") targetProduct = products[0] } } logger.Log.Info("[TEST] Используем товар", zap.String("name", targetProduct.Name), zap.String("type", targetProduct.Type), zap.String("uuid", targetProduct.ID.String()), ) // 2. Парсинг ID Склада и Поставщика storeID := uuid.Nil if MyStoreGUID != "00000000-0000-0000-0000-000000000000" { storeID = uuid.MustParse(MyStoreGUID) } else { logger.Log.Warn("[TEST] ID склада не задан! iiko вернет ошибку валидации.") storeID = uuid.New() // Рандом, чтобы XML собрался } supplierID := uuid.Nil if MySupplierGUID != "00000000-0000-0000-0000-000000000000" { supplierID = uuid.MustParse(MySupplierGUID) } else { logger.Log.Warn("[TEST] ID поставщика не задан!") supplierID = uuid.New() } // 3. Формируем накладную testInv := invoices.Invoice{ ID: uuid.Nil, DocumentNumber: fmt.Sprintf("TEST-%d", time.Now().Unix()%10000), DateIncoming: time.Now(), SupplierID: supplierID, DefaultStoreID: storeID, Status: "NEW", Items: []invoices.InvoiceItem{ { ProductID: targetProduct.ID, Amount: decimal.NewFromFloat(5.0), Price: decimal.NewFromFloat(100.0), Sum: decimal.NewFromFloat(500.0), }, }, } // 4. Отправляем logger.Log.Info("[TEST] Отправка запроса в RMS...") docNum, err := client.CreateIncomingInvoice(testInv) if err != nil { logger.Log.Error("[TEST] Ошибка отправки накладной", zap.Error(err)) } else { logger.Log.Info("[TEST] УСПЕХ! Накладная создана.", zap.String("doc_number", docNum)) } }