324 lines
11 KiB
Go
324 lines
11 KiB
Go
|
|
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")
|
||
|
|
}
|
||
|
|
}
|