package main import ( "context" "fmt" "io/fs" "log" "net/http" "os" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" "financeiro-carvalho/internal/db" "financeiro-carvalho/internal/handler" "financeiro-carvalho/internal/migration" "financeiro-carvalho/internal/repository" "financeiro-carvalho/internal/service" "financeiro-carvalho/static" ) func main() { _ = godotenv.Load() dsn := os.Getenv("DATABASE_URL") if dsn == "" { log.Fatal("DATABASE_URL is required") } ctx := context.Background() pool, err := db.Connect(ctx, dsn) if err != nil { log.Fatalf("db connect: %v", err) } defer pool.Close() if err := migration.Run(ctx, pool); err != nil { log.Fatalf("migration: %v", err) } port := os.Getenv("PORT") if port == "" { port = "8080" } r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer) categoryRepo := repository.NewCategoryRepository(pool) categorySvc := service.NewCategoryService(categoryRepo) categoryHandler := handler.NewCategoryHandler(categorySvc) transactionRepo := repository.NewTransactionRepository(pool) importSvc := service.NewImportService(transactionRepo) importHandler := handler.NewImportHandler(importSvc) manualTxRepo := repository.NewManualTransactionRepository(pool) txSvc := service.NewTransactionService(manualTxRepo) txHandler := handler.NewTransactionHandler(txSvc) recurringRepo := repository.NewRecurringRepository(pool) recurringSvc := service.NewRecurringService(recurringRepo, manualTxRepo) recurringHandler := handler.NewRecurringHandler(recurringSvc) r.Get("/health", handler.Health) r.Route("/api", func(r chi.Router) { r.Get("/categories", categoryHandler.List) r.Post("/categories", categoryHandler.Create) r.Put("/categories/{id}", categoryHandler.Update) r.Delete("/categories/{id}", categoryHandler.Delete) r.Post("/imports/preview", importHandler.Preview) r.Post("/imports/confirm", importHandler.Confirm) r.Get("/transactions", txHandler.List) r.Post("/transactions", txHandler.Create) r.Put("/transactions/{id}", txHandler.Update) r.Delete("/transactions/{id}", txHandler.Delete) r.Get("/recurring", recurringHandler.List) r.Post("/recurring", recurringHandler.Create) r.Put("/recurring/{id}", recurringHandler.Update) r.Delete("/recurring/{id}", recurringHandler.Delete) r.Get("/recurring/status", recurringHandler.MonthlyStatus) r.Post("/recurring/{id}/ignore", recurringHandler.Ignore) r.Delete("/recurring/{id}/ignore", recurringHandler.Unignore) }) // Serve Vue SPA — non-API routes fall through to index.html distFS, err := fs.Sub(static.FS, "dist") if err != nil { log.Fatalf("static embed sub: %v", err) } fileServer := http.FileServer(http.FS(distFS)) r.Get("/*", func(w http.ResponseWriter, req *http.Request) { path := req.URL.Path[1:] if _, statErr := fs.Stat(distFS, path); statErr != nil { index, readErr := fs.ReadFile(distFS, "index.html") if readErr != nil { http.NotFound(w, req) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write(index) return } fileServer.ServeHTTP(w, req) }) fmt.Printf("server listening on :%s\n", port) if err := http.ListenAndServe(":"+port, r); err != nil { log.Fatal(err) } }