package server import ( "context" "log" "net/http" "time" "github.com/gofrs/uuid/v5" _ "github.com/jackc/pgx/v5/stdlib" "github.com/jmoiron/sqlx" httpSwagger "github.com/swaggo/http-swagger" "github.com/saveinmed/backend-go/internal/config" "github.com/saveinmed/backend-go/internal/domain" "github.com/saveinmed/backend-go/internal/http/handler" "github.com/saveinmed/backend-go/internal/http/middleware" "github.com/saveinmed/backend-go/internal/infrastructure/mapbox" "github.com/saveinmed/backend-go/internal/notifications" "github.com/saveinmed/backend-go/internal/payments" "github.com/saveinmed/backend-go/internal/repository/postgres" "github.com/saveinmed/backend-go/internal/usecase" ) // Server wires the infrastructure and exposes HTTP handlers. type Server struct { cfg config.Config db *sqlx.DB mux *http.ServeMux svc *usecase.Service } func New(cfg config.Config) (*Server, error) { db, err := sqlx.Open("pgx", cfg.DatabaseURL) if err != nil { return nil, err } repoInstance := postgres.New(db) paymentGateway := payments.NewMercadoPagoGateway(cfg.MercadoPagoBaseURL, cfg.MercadoPagoAccessToken, cfg.BackendHost, cfg.MarketplaceCommission) mapboxClient := mapbox.New(cfg.MapboxAccessToken) // Services notifySvc := notifications.NewLoggerNotificationService() svc := usecase.NewService(repoInstance, paymentGateway, mapboxClient, notifySvc, cfg.MarketplaceCommission, cfg.BuyerFeeRate, cfg.JWTSecret, cfg.JWTExpiresIn, cfg.PasswordPepper) h := handler.New(svc, cfg.BuyerFeeRate) mux := http.NewServeMux() // Root endpoint mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) response := `{"message":"💊 SaveInMed API is running!","docs":"/docs","health":"/health","version":"1.0.0"}` _, _ = w.Write([]byte(response)) }) mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("ok")) }) auth := middleware.RequireAuth([]byte(cfg.JWTSecret)) adminOnly := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin") // Keep for strict admin routes if any // Allow Admin, Superadmin, Dono, Gerente to manage products productManagers := middleware.RequireAuth([]byte(cfg.JWTSecret), "Admin", "superadmin", "Dono", "Gerente") // Companies (Empresas) mux.Handle("POST /api/v1/companies", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/empresas", chain(http.HandlerFunc(h.CreateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("GET /api/v1/companies", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/empresas", chain(http.HandlerFunc(h.ListCompanies), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("GET /api/v1/companies/{id}", chain(http.HandlerFunc(h.GetCompany), middleware.Logger, middleware.Gzip)) mux.Handle("PATCH /api/v1/companies/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip)) mux.Handle("PATCH /api/v1/empresas/{id}", chain(http.HandlerFunc(h.UpdateCompany), middleware.Logger, middleware.Gzip)) // Alias for frontend mux.Handle("DELETE /api/v1/companies/{id}", chain(http.HandlerFunc(h.DeleteCompany), middleware.Logger, middleware.Gzip)) mux.Handle("PATCH /api/v1/companies/{id}/verify", chain(http.HandlerFunc(h.VerifyCompany), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("GET /api/v1/companies/me", chain(http.HandlerFunc(h.GetMyCompany), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/companies/{id}/rating", chain(http.HandlerFunc(h.GetCompanyRating), middleware.Logger, middleware.Gzip)) // KYC mux.Handle("POST /api/v1/companies/documents", chain(http.HandlerFunc(h.UploadDocument), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/companies/documents", chain(http.HandlerFunc(h.GetDocuments), middleware.Logger, middleware.Gzip, auth)) // Financials mux.Handle("GET /api/v1/finance/ledger", chain(http.HandlerFunc(h.GetLedger), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/finance/balance", chain(http.HandlerFunc(h.GetBalance), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/finance/withdrawals", chain(http.HandlerFunc(h.RequestWithdrawal), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/finance/withdrawals", chain(http.HandlerFunc(h.ListWithdrawals), middleware.Logger, middleware.Gzip, auth)) // Team mux.Handle("GET /api/v1/team", chain(http.HandlerFunc(h.ListTeam), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/team", chain(http.HandlerFunc(h.InviteMember), middleware.Logger, middleware.Gzip, auth)) // Product Management (Admin + Vendors) mux.Handle("POST /api/v1/products", chain(http.HandlerFunc(h.CreateProduct), middleware.Logger, middleware.Gzip, productManagers)) mux.Handle("PATCH /api/v1/products/{id}", chain(http.HandlerFunc(h.UpdateProduct), middleware.Logger, middleware.Gzip, productManagers)) mux.Handle("DELETE /api/v1/products/{id}", chain(http.HandlerFunc(h.DeleteProduct), middleware.Logger, middleware.Gzip, productManagers)) // Public/Shared Product Access mux.Handle("GET /api/v1/products", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip, auth)) // List might remain open or logged-in only mux.Handle("GET /api/v1/products/search", chain(http.HandlerFunc(h.SearchProducts), middleware.Logger, middleware.Gzip, middleware.OptionalAuth([]byte(cfg.JWTSecret)))) mux.Handle("GET /api/v1/products/{id}", chain(http.HandlerFunc(h.GetProduct), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/marketplace/records", chain(http.HandlerFunc(h.ListMarketplaceRecords), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/laboratorios", chain(http.HandlerFunc(h.ListManufacturers), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/categorias", chain(http.HandlerFunc(h.ListCategories), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/produtos-catalogo", chain(http.HandlerFunc(h.ListProducts), middleware.Logger, middleware.Gzip)) // Alias mux.Handle("GET /api/v1/produtos-catalogo/codigo-ean/{ean}", chain(http.HandlerFunc(h.GetProductByEAN), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/inventory", chain(http.HandlerFunc(h.ListInventory), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/inventory", chain(http.HandlerFunc(h.CreateInventoryItem), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/produtos-venda", chain(http.HandlerFunc(h.CreateInventoryItem), middleware.Logger, middleware.Gzip, auth)) // Alias mux.Handle("GET /api/v1/produtos-venda", chain(http.HandlerFunc(h.ListInventory), middleware.Logger, middleware.Gzip, auth)) // Alias for list mux.Handle("PUT /api/v1/produtos-venda/{id}", chain(http.HandlerFunc(h.UpdateInventoryItem), middleware.Logger, middleware.Gzip, auth)) // Update inventory mux.Handle("POST /api/v1/inventory/adjust", chain(http.HandlerFunc(h.AdjustInventory), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/orders", chain(http.HandlerFunc(h.CreateOrder), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/orders", chain(http.HandlerFunc(h.ListOrders), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/orders/{id}", chain(http.HandlerFunc(h.GetOrder), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/orders/{id}", chain(http.HandlerFunc(h.UpdateOrder), middleware.Logger, middleware.Gzip, auth)) // Add PUT support mux.Handle("PATCH /api/v1/orders/{id}/status", chain(http.HandlerFunc(h.UpdateOrderStatus), middleware.Logger, middleware.Gzip, auth)) mux.Handle("DELETE /api/v1/orders/{id}", chain(http.HandlerFunc(h.DeleteOrder), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/orders/{id}/payment", chain(http.HandlerFunc(h.CreatePaymentPreference), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/orders/{id}/pay", chain(http.HandlerFunc(h.ProcessOrderPayment), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/shipments", chain(http.HandlerFunc(h.CreateShipment), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/shipments", chain(http.HandlerFunc(h.ListShipments), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/shipments/", chain(http.HandlerFunc(h.GetShipmentByOrderID), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/payments/webhook", chain(http.HandlerFunc(h.HandlePaymentWebhook), middleware.Logger, middleware.Gzip)) // Generic/Mercado Pago mux.Handle("POST /api/v1/payments/webhook/stripe", chain(http.HandlerFunc(h.HandleStripeWebhook), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/payments/webhook/asaas", chain(http.HandlerFunc(h.HandleAsaasWebhook), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/reviews", chain(http.HandlerFunc(h.CreateReview), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/reviews", chain(http.HandlerFunc(h.ListReviews), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/dashboard", chain(http.HandlerFunc(h.GetDashboard), middleware.Logger, middleware.Gzip, auth)) // Payment Config (Admin) mux.Handle("GET /api/v1/admin/payment-gateways/{provider}", chain(http.HandlerFunc(h.GetPaymentGatewayConfig), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("PUT /api/v1/admin/payment-gateways/{provider}", chain(http.HandlerFunc(h.UpdatePaymentGatewayConfig), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("POST /api/v1/admin/payment-gateways/{provider}/test", chain(http.HandlerFunc(h.TestPaymentGateway), middleware.Logger, middleware.Gzip, adminOnly)) // Payment Config (Seller) mux.Handle("GET /api/v1/sellers/{id}/payment-config", chain(http.HandlerFunc(h.GetSellerPaymentConfig), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/sellers/{id}/onboarding", chain(http.HandlerFunc(h.OnboardSeller), middleware.Logger, middleware.Gzip, auth)) // Credit Lines (Boleto a Prazo) mux.Handle("POST /api/v1/companies/{company_id}/credit/check", chain(http.HandlerFunc(h.CheckCreditLine), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/companies/{company_id}/credit/limit", chain(http.HandlerFunc(h.SetCreditLimit), middleware.Logger, middleware.Gzip, adminOnly)) mux.Handle("POST /api/v1/auth/register", chain(http.HandlerFunc(h.Register), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/register/customer", chain(http.HandlerFunc(h.RegisterCustomer), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/register/tenant", chain(http.HandlerFunc(h.RegisterTenant), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/login", chain(http.HandlerFunc(h.Login), middleware.Logger, middleware.Gzip)) mux.Handle("GET /api/v1/auth/me", chain(http.HandlerFunc(h.GetMe), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/auth/logout", chain(http.HandlerFunc(h.Logout), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/password/forgot", chain(http.HandlerFunc(h.ForgotPassword), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/password/reset", chain(http.HandlerFunc(h.ResetPassword), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/refresh", chain(http.HandlerFunc(h.Refresh), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/refresh-token", chain(http.HandlerFunc(h.RefreshToken), middleware.Logger, middleware.Gzip)) mux.Handle("POST /api/v1/auth/verify-email", chain(http.HandlerFunc(h.VerifyEmail), middleware.Logger, middleware.Gzip)) // Address mux.Handle("POST /api/v1/enderecos", chain(http.HandlerFunc(h.CreateAddress), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/enderecos", chain(http.HandlerFunc(h.ListAddresses), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/enderecos/{id}", chain(http.HandlerFunc(h.UpdateAddress), middleware.Logger, middleware.Gzip, auth)) mux.Handle("DELETE /api/v1/enderecos/{id}", chain(http.HandlerFunc(h.DeleteAddress), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/push/register", chain(http.HandlerFunc(h.RegisterPushToken), middleware.Logger, middleware.Gzip, auth)) mux.Handle("DELETE /api/v1/push/unregister", chain(http.HandlerFunc(h.UnregisterPushToken), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/push/test", chain(http.HandlerFunc(h.TestPushNotification), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/users", chain(http.HandlerFunc(h.CreateUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/users", chain(http.HandlerFunc(h.ListUsers), middleware.Logger, middleware.Gzip, auth)) mux.Handle("GET /api/v1/users/", chain(http.HandlerFunc(h.GetUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PATCH /api/v1/users/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Add PATCH support mux.Handle("PATCH /api/v1/usuarios/", chain(http.HandlerFunc(h.UpdateUser), middleware.Logger, middleware.Gzip, auth)) // Alias for frontend mux.Handle("DELETE /api/v1/users/", chain(http.HandlerFunc(h.DeleteUser), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/cart", chain(http.HandlerFunc(h.AddToCart), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/cart", chain(http.HandlerFunc(h.UpdateCart), middleware.Logger, middleware.Gzip, auth)) // Add PUT support mux.Handle("GET /api/v1/cart", chain(http.HandlerFunc(h.GetCart), middleware.Logger, middleware.Gzip, auth)) mux.Handle("DELETE /api/v1/cart", chain(http.HandlerFunc(h.DeleteCartItem), middleware.Logger, middleware.Gzip, auth)) // Clear all mux.Handle("DELETE /api/v1/cart/", chain(http.HandlerFunc(h.DeleteCartItem), middleware.Logger, middleware.Gzip, auth)) // Clear item mux.Handle("GET /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.GetShippingSettings), middleware.Logger, middleware.Gzip, auth)) mux.Handle("PUT /api/v1/shipping/settings/{vendor_id}", chain(http.HandlerFunc(h.UpsertShippingSettings), middleware.Logger, middleware.Gzip, auth)) mux.Handle("POST /api/v1/shipping/calculate", chain(http.HandlerFunc(h.CalculateShipping), middleware.Logger, middleware.Gzip)) mux.Handle("GET /docs/", httpSwagger.Handler(httpSwagger.URL("/docs/doc.json"))) return &Server{cfg: cfg, db: db, mux: mux, svc: svc}, nil } // Start runs the HTTP server and ensures the database is reachable. func (s *Server) Start(ctx context.Context) error { if err := s.db.PingContext(ctx); err != nil { return err } repo := postgres.New(s.db) if err := repo.ApplyMigrations(ctx); err != nil { return err } // Seed Admin if s.cfg.AdminEmail != "" && s.cfg.AdminPassword != "" { // Checks if admin already exists _, err := repo.GetUserByEmail(ctx, s.cfg.AdminEmail) if err != nil { // If not found, create log.Printf("Seeding admin user: %s", s.cfg.AdminEmail) // 1. Create/Get Admin Company adminCNPJ := "00000000000000" company := &domain.Company{ ID: uuid.Nil, CNPJ: adminCNPJ, CorporateName: "SaveInMed Admin", Category: "admin", LicenseNumber: "ADMIN", IsVerified: true, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } // We need to check if company exists by CNPJ normally, but repo doesn't expose GetByCNPJ easily? // Let's rely on RegisterAccount handling it or check if we can query. // Actually RegisterAccount in Service handles creation if ID is Nil, but keys off ID. // We can try to create and ignore conflict, or use a known ID? // Let's use RegisterAccount logic. // Because RegisterAccount expects us to pass a company, and tries to Get by ID if ID is set, or Create if not. // But duplicate CNPJ will fail at DB level. // Let's assume on fresh boot it doesn't exist. // Or better: Use svc.RegisterAccount. But wait, svc.RegisterAccount logic: /* if company != nil { if company.ID == uuid.Nil { // create } else { // get } } */ // If we re-run, GetUserByEmail would have found the user, so we skip. // The only edge case is if User was deleted but Company remains. // In that case, CreateCompany will fail on CNPJ constraint. err := s.svc.RegisterAccount(ctx, company, &domain.User{ Role: "Admin", Name: s.cfg.AdminName, Username: s.cfg.AdminUsername, Email: s.cfg.AdminEmail, }, s.cfg.AdminPassword) if err != nil { // If error is duplicate key on company, maybe we should fetch the company and try creating user only? // For now, let's log error but not fail startup hard, or fail hard to signal issue. log.Printf("Failed to seed admin: %v", err) } else { // FORCE VERIFY the admin company if _, err := s.svc.VerifyCompany(ctx, company.ID); err != nil { log.Printf("Failed to verify admin company: %v", err) } log.Printf("Admin user created successfully") } } else { log.Printf("Admin user %s already exists", s.cfg.AdminEmail) } } corsConfig := middleware.CORSConfig{AllowedOrigins: s.cfg.CORSOrigins} srv := &http.Server{ Addr: s.cfg.Addr(), Handler: middleware.SecurityHeaders(middleware.CORSWithConfig(corsConfig)(s.mux)), ReadHeaderTimeout: 5 * time.Second, } log.Printf("starting %s on %s", s.cfg.AppName, s.cfg.Addr()) return srv.ListenAndServe() } func chain(h http.Handler, middlewareFns ...func(http.Handler) http.Handler) http.Handler { for i := len(middlewareFns) - 1; i >= 0; i-- { h = middlewareFns[i](h) } return h }