package main import ( "context" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" "boc/automation" "boc/cache" "boc/config" "boc/db" "boc/email" "boc/events" "boc/handlers" "boc/middleware" "boc/websocket" ) func main() { logger := zerolog.New(os.Stdout).With().Timestamp().Logger() cfg := config.Load() port := os.Getenv("PORT") if port == "" { port = "9092" } // Database connection with migrations database, err := db.Connect(cfg.DBURL) if err != nil { logger.Fatal().Err(err).Msg("database connect failed") } defer database.Close() // Run migrations migrationsDir := os.Getenv("MIGRATIONS_DIR") if migrationsDir == "" { migrationsDir = "./db/migrations" } if err := db.RunMigrations(database, migrationsDir); err != nil { logger.Fatal().Err(err).Msg("migrations failed") } // Redis cache redisClient, err := cache.NewRedisClient(cfg.RedisURL) if err != nil { logger.Warn().Err(err).Msg("redis connection failed, continuing without cache") redisClient = nil } else { defer redisClient.Close() logger.Info().Msg("redis connected") } // Kafka event streaming var kafkaClient *events.KafkaClient if len(cfg.KafkaBrokers) > 0 && cfg.KafkaBrokers[0] != "" { kafkaClient, err = events.NewKafkaClient(cfg.KafkaBrokers) if err != nil { logger.Warn().Err(err).Msg("kafka connection failed, continuing without event streaming") } else { defer kafkaClient.Close() if err := kafkaClient.EnsureTopics(); err != nil { logger.Warn().Err(err).Msg("failed to ensure kafka topics") } logger.Info().Strs("brokers", cfg.KafkaBrokers).Msg("kafka connected") } } // Automation engine autoEngine := automation.NewEngine(database, logger) autoEngine.Start(context.Background()) defer autoEngine.Stop() // WebSocket hub wsHub := websocket.NewHub(logger) go wsHub.Run() // Email client (Resend) var emailClient *email.Client if cfg.ResendAPIKey != "" { emailClient = email.NewClient(cfg.ResendAPIKey, cfg.FromEmail, cfg.FromName) logger.Info().Str("from", cfg.FromEmail).Msg("email client configured") } else { logger.Warn().Msg("RESEND_API_KEY not set, email features disabled") } // Handlers authH := &handlers.AuthHandler{ DB: database, JWTSecret: []byte(cfg.JWTSecret), } crmH := handlers.NewCRMHandler(database) salesH := handlers.NewSalesHandler(database) financeH := handlers.NewFinanceHandler(database) financeH.SetEmailClient(emailClient) marketingH := handlers.NewMarketingHandler(database) supportH := handlers.NewSupportHandler(database) analyticsH := handlers.NewAnalyticsHandler(database) hrH := handlers.NewHRHandler(database) legalH := handlers.NewLegalHandler(database) autoH := handlers.NewAutomationHandler(database, autoEngine) // Ledger integration quoteH := handlers.NewQuoteHandler(database) quoteH.SetEmailClient(emailClient) orderH := handlers.NewOrderHandler(database) supplierH := handlers.NewSupplierHandler(database) inventoryH := handlers.NewInventoryHandler(database) subscriptionH := handlers.NewSubscriptionHandler(database) receiptH := handlers.NewReceiptHandler(database) payrollH := handlers.NewPayrollHandler(database) bankH := handlers.NewBankHandler(database) projectH := handlers.NewProjectHandler(database) ledgerFinanceH := handlers.NewLedgerFinanceHandler() r := chi.NewRouter() r.Use(middleware.CORS) r.Use(hlog.NewHandler(logger)) r.Use(hlog.RequestIDHandler("req_id", "X-Request-ID")) r.Use(middleware.Logger(logger)) r.Use(chimw.Recoverer) // Public r.Handle("/health", handlers.NewHealthHandler()) r.Post("/api/v1/auth/login", authH.Login) // WebSocket r.Get("/ws", wsHub.HandleWebSocket) // Protected r.Group(func(r chi.Router) { r.Use(middleware.Auth(cfg)) r.Get("/api/v1/auth/me", authH.Me) // CRM r.Get("/api/v1/crm/customers", crmH.ListCustomers) r.Post("/api/v1/crm/customers", crmH.CreateCustomer) r.Get("/api/v1/crm/customers/{id}", crmH.GetCustomer) r.Put("/api/v1/crm/customers/{id}", crmH.UpdateCustomer) r.Delete("/api/v1/crm/customers/{id}", crmH.DeleteCustomer) r.Get("/api/v1/crm/leads", crmH.ListLeads) r.Get("/api/v1/crm/pipeline", crmH.GetPipeline) r.Post("/api/v1/crm/interactions", crmH.CreateInteraction) r.Get("/api/v1/crm/customers/{id}/interactions", crmH.GetCustomerInteractions) // Sales r.Get("/api/v1/sales/deals", salesH.ListDeals) r.Post("/api/v1/sales/deals", salesH.CreateDeal) r.Get("/api/v1/sales/deals/{id}", salesH.GetDeal) r.Put("/api/v1/sales/deals/{id}", salesH.UpdateDeal) r.Get("/api/v1/sales/mrr", salesH.GetMRR) r.Get("/api/v1/sales/arr", salesH.GetARR) r.Get("/api/v1/sales/products", salesH.ListProducts) r.Post("/api/v1/sales/products", salesH.CreateProduct) // Quotes r.Get("/api/v1/sales/quotes", quoteH.ListQuotes) r.Post("/api/v1/sales/quotes", quoteH.CreateQuote) r.Get("/api/v1/sales/quotes/{id}", quoteH.GetQuote) r.Post("/api/v1/sales/quotes/{id}/accept", quoteH.AcceptQuote) r.Post("/api/v1/sales/quotes/{id}/convert", quoteH.ConvertToOrder) r.Get("/api/v1/sales/quotes/{id}/pdf", quoteH.GenerateQuotePDF) r.Post("/api/v1/sales/quotes/{id}/send", quoteH.SendQuoteEmail) // Orders r.Get("/api/v1/sales/orders", orderH.ListOrders) r.Post("/api/v1/sales/orders", orderH.CreateOrder) r.Get("/api/v1/sales/orders/{id}", orderH.GetOrder) r.Put("/api/v1/sales/orders/{id}", orderH.UpdateOrder) r.Post("/api/v1/sales/orders/{id}/ship", orderH.ShipOrder) r.Post("/api/v1/sales/orders/{id}/deliver", orderH.DeliverOrder) // Finance (from aamos-ledger) r.Get("/api/v1/finance/balance", ledgerFinanceH.GetBalanceSheet) r.Get("/api/v1/finance/income", ledgerFinanceH.GetIncomeStatement) r.Get("/api/v1/finance/moms", ledgerFinanceH.GetMomsReport) r.Get("/api/v1/finance/accounts", ledgerFinanceH.GetAccounts) r.Get("/api/v1/finance/invoices", ledgerFinanceH.GetInvoices) r.Get("/api/v1/finance/cashflow", financeH.GetCashFlow) r.Get("/api/v1/finance/budget", financeH.GetBudget) r.Post("/api/v1/finance/expenses", financeH.CreateExpense) r.Get("/api/v1/finance/expenses", financeH.ListExpenses) r.Get("/api/v1/finance/invoices/{id}/pdf", financeH.GenerateInvoicePDF) r.Post("/api/v1/finance/invoices/{id}/send", financeH.SendInvoiceEmail) // Suppliers & Purchase r.Get("/api/v1/purchase/suppliers", supplierH.ListSuppliers) r.Post("/api/v1/purchase/suppliers", supplierH.CreateSupplier) r.Get("/api/v1/purchase/orders", supplierH.ListPurchaseOrders) r.Post("/api/v1/purchase/orders", supplierH.CreatePurchaseOrder) r.Get("/api/v1/purchase/invoices", supplierH.ListSupplierInvoices) r.Post("/api/v1/purchase/invoices", supplierH.CreateSupplierInvoice) // Inventory r.Get("/api/v1/inventory/warehouses", inventoryH.ListWarehouses) r.Post("/api/v1/inventory/warehouses", inventoryH.CreateWarehouse) r.Get("/api/v1/inventory", inventoryH.ListInventory) r.Post("/api/v1/inventory/adjust", inventoryH.AdjustStock) r.Get("/api/v1/inventory/movements", inventoryH.ListMovements) r.Get("/api/v1/inventory/low-stock", inventoryH.GetLowStock) // Subscriptions r.Get("/api/v1/subscriptions/plans", subscriptionH.ListPlans) r.Post("/api/v1/subscriptions/plans", subscriptionH.CreatePlan) r.Get("/api/v1/subscriptions", subscriptionH.ListSubscriptions) r.Post("/api/v1/subscriptions", subscriptionH.CreateSubscription) r.Post("/api/v1/subscriptions/generate-invoices", subscriptionH.GenerateRecurringInvoices) r.Get("/api/v1/subscriptions/invoices", subscriptionH.ListRecurringInvoices) // Receipts r.Get("/api/v1/receipts", receiptH.ListReceipts) r.Post("/api/v1/receipts", receiptH.UploadReceipt) r.Post("/api/v1/receipts/approve", receiptH.ApproveReceipt) // Payroll r.Get("/api/v1/payroll/runs", payrollH.ListPayrollRuns) r.Post("/api/v1/payroll/runs", payrollH.CreatePayrollRun) r.Get("/api/v1/payroll/runs/{id}", payrollH.GetPayrollRun) r.Post("/api/v1/payroll/runs/{id}/process", payrollH.ProcessPayroll) r.Post("/api/v1/payroll/runs/{id}/approve", payrollH.ApprovePayroll) // Bank r.Get("/api/v1/bank/accounts", bankH.ListAccounts) r.Post("/api/v1/bank/accounts", bankH.CreateAccount) r.Get("/api/v1/bank/transactions", bankH.ListTransactions) r.Post("/api/v1/bank/transactions/sync", bankH.SyncTransactions) r.Post("/api/v1/bank/transactions/{id}/match", bankH.MatchTransaction) // Projects r.Get("/api/v1/projects", projectH.ListProjects) r.Post("/api/v1/projects", projectH.CreateProject) r.Get("/api/v1/projects/{id}", projectH.GetProject) r.Post("/api/v1/projects/{id}/time", projectH.AddTime) r.Get("/api/v1/projects/summary", projectH.GetProjectSummary) // HR r.Get("/api/v1/hr/employees", hrH.ListEmployees) r.Post("/api/v1/hr/employees", hrH.CreateEmployee) r.Get("/api/v1/hr/employees/{id}", hrH.GetEmployee) r.Put("/api/v1/hr/employees/{id}", hrH.UpdateEmployee) r.Get("/api/v1/hr/leaves", hrH.ListLeaves) r.Post("/api/v1/hr/leaves", hrH.CreateLeave) r.Get("/api/v1/hr/timesheets", hrH.ListTimesheets) r.Post("/api/v1/hr/timesheets", hrH.CreateTimesheet) // Legal r.Get("/api/v1/legal/contracts", legalH.ListContracts) r.Post("/api/v1/legal/contracts", legalH.CreateContract) r.Get("/api/v1/legal/contracts/{id}", legalH.GetContract) r.Put("/api/v1/legal/contracts/{id}", legalH.UpdateContract) r.Get("/api/v1/legal/reminders", legalH.ListReminders) // Marketing r.Get("/api/v1/marketing/campaigns", marketingH.ListCampaigns) r.Post("/api/v1/marketing/campaigns", marketingH.CreateCampaign) r.Get("/api/v1/marketing/content", marketingH.ListContent) r.Post("/api/v1/marketing/content", marketingH.CreateContent) // Support r.Get("/api/v1/support/tickets", supportH.ListTickets) r.Post("/api/v1/support/tickets", supportH.CreateTicket) r.Get("/api/v1/support/tickets/{id}", supportH.GetTicket) r.Put("/api/v1/support/tickets/{id}", supportH.UpdateTicket) r.Post("/api/v1/support/tickets/{id}/comments", supportH.AddComment) r.Get("/api/v1/support/csat", supportH.GetCSAT) // Analytics r.Get("/api/v1/analytics/users", analyticsH.GetActiveUsers) r.Get("/api/v1/analytics/revenue", analyticsH.GetRevenue) r.Get("/api/v1/analytics/retention", analyticsH.GetRetention) r.Get("/api/v1/analytics/dashboard", analyticsH.GetDashboard) // Automation r.Get("/api/v1/automation/workflows", autoH.ListWorkflows) r.Post("/api/v1/automation/workflows", autoH.CreateWorkflow) r.Post("/api/v1/automation/workflows/{id}/trigger", autoH.TriggerWorkflow) r.Get("/api/v1/automation/jobs", autoH.ListScheduledJobs) r.Post("/api/v1/automation/jobs", autoH.CreateScheduledJob) r.Get("/api/v1/automation/runs", autoH.ListRuns) }) // Inject dependencies into context for handlers that need them _ = redisClient _ = kafkaClient _ = wsHub srv := &http.Server{ Addr: ":" + port, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } go func() { logger.Info().Str("addr", srv.Addr).Msg("BOC server starting") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("listen error") } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info().Msg("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { logger.Error().Err(err).Msg("shutdown error") } }