package main import ( "context" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "Engine/internal/database" "Engine/internal/handler" "Engine/internal/service" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "fpa.db" } db, err := database.Connect(context.Background(), dbPath) if err != nil { logger.Error("failed to open database", "error", err) os.Exit(1) } defer db.Close() if err := database.Migrate(db); err != nil { logger.Error("migration failed", "error", err) os.Exit(1) } // Reference data (FK parents — must exist before budgets/actuals) referenceRepo := database.NewReferenceRepo(db) referenceH := handler.NewReferenceHandler(referenceRepo) // Core FP&A budgetRepo := database.NewBudgetRepo(db) actualsRepo := database.NewActualsRepo(db) budgetSvc := service.NewBudgetService(budgetRepo) varianceSvc := service.NewVarianceService(budgetRepo, actualsRepo) budgetH := handler.NewBudgetHandler(budgetSvc) actualsH := handler.NewActualsHandler(actualsRepo) varianceH := handler.NewVarianceHandler(varianceSvc) reportRepo := database.NewReportRepo(db) reportSvc := service.NewReportService(reportRepo) reportH := handler.NewReportHandler(reportSvc) mux := http.NewServeMux() // Reference endpoints mux.HandleFunc("POST /api/v1/department/create", referenceH.CreateDepartment) mux.HandleFunc("DELETE /api/v1/department/delete", referenceH.DeleteDepartment) mux.HandleFunc("GET /api/v1/department/list", referenceH.ListDepartments) mux.HandleFunc("GET /api/v1/department/bugdet", referenceH.ListDepartments) mux.HandleFunc("GET /api/v1/department/actual", referenceH.ListDepartments) mux.HandleFunc("POST /api/v1/gl-account/create", referenceH.CreateGLAccount) mux.HandleFunc("DELETE /api/v1/gl-account/delete", referenceH.DeleteGLAccount) mux.HandleFunc("GET /api/v1/gl-account/list", referenceH.ListGLAccounts) mux.HandleFunc("GET /api/v1/gl-account/bugdet", referenceH.ListDepartments) mux.HandleFunc("GET /api/v1/gl-account/actual", referenceH.ListDepartments) // Budget endpoints mux.HandleFunc("POST /api/v1/budget/create", budgetH.Create) mux.HandleFunc("PUT /api/v1/budget/update", budgetH.Update) mux.HandleFunc("DELETE /api/v1/budget/delete", budgetH.Delete) // Actuals + variance mux.HandleFunc("POST /api/v1/actuals/ingest", actualsH.Ingest) mux.HandleFunc("POST /api/v1/actuals/ingest/batch", actualsH.IngestBatch) mux.HandleFunc("GET /api/v1/variance", varianceH.Report) mux.HandleFunc("GET /api/v1/variance/alerts", varianceH.Alerts) mux.HandleFunc("GET /api/v1/variance/reforecast", varianceH.Alerts) //reports mux.HandleFunc("GET /api/v1/reports/pnl", reportH.PnL) mux.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) { if err := db.PingContext(r.Context()); err != nil { http.Error(w, `{"error":"db unhealthy"}`, http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) port := os.Getenv("PORT") if port == "" { port = "8080" } srv := &http.Server{ Addr: ":" + port, Handler: mux, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) go func() { logger.Info("server starting", "port", port, "db", dbPath) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Error("server error", "error", err) os.Exit(1) } }() <-quit logger.Info("shutting down gracefully") ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() srv.Shutdown(ctx) }