Files
boc/backend/main.go
T

325 lines
12 KiB
Go
Raw Normal View History

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
database, err := db.Connect(cfg.DBURL)
if err != nil {
logger.Fatal().Err(err).Msg("database connect failed")
}
defer database.Close()
// Run migrations from single source of truth
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")
}
logger.Info().Str("dir", migrationsDir).Msg("migrations completed")
// 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")
}
}