package handlers import ( "encoding/json" "log" "net/http" "strconv" "aggios-app/backend/internal/domain" "aggios-app/backend/internal/service" "github.com/google/uuid" "github.com/gorilla/mux" ) // PlanHandler handles plan-related endpoints type PlanHandler struct { planService *service.PlanService } // NewPlanHandler creates a new plan handler func NewPlanHandler(planService *service.PlanService) *PlanHandler { return &PlanHandler{ planService: planService, } } // RegisterRoutes registers plan routes func (h *PlanHandler) RegisterRoutes(r *mux.Router) { // Note: Route protection is done in main.go with authMiddleware wrapper r.HandleFunc("/api/admin/plans", h.CreatePlan).Methods(http.MethodPost) r.HandleFunc("/api/admin/plans", h.ListPlans).Methods(http.MethodGet) r.HandleFunc("/api/admin/plans/{id}", h.GetPlan).Methods(http.MethodGet) r.HandleFunc("/api/admin/plans/{id}", h.UpdatePlan).Methods(http.MethodPut) r.HandleFunc("/api/admin/plans/{id}", h.DeletePlan).Methods(http.MethodDelete) // Public routes (for signup flow) r.HandleFunc("/api/plans", h.ListActivePlans).Methods(http.MethodGet) r.HandleFunc("/api/plans/{id}", h.GetActivePlan).Methods(http.MethodGet) } // CreatePlan creates a new plan (admin only) func (h *PlanHandler) CreatePlan(w http.ResponseWriter, r *http.Request) { log.Printf("📋 CREATE PLAN - Method: %s", r.Method) var req domain.CreatePlanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("❌ Invalid request body: %v", err) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Invalid request body", "message": err.Error()}) return } plan, err := h.planService.CreatePlan(&req) if err != nil { log.Printf("❌ Error creating plan: %v", err) w.Header().Set("Content-Type", "application/json") switch err { case service.ErrPlanSlugTaken: w.WriteHeader(http.StatusConflict) json.NewEncoder(w).Encode(map[string]string{"error": "Slug already taken", "message": err.Error()}) case service.ErrInvalidUserRange: w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Invalid user range", "message": err.Error()}) default: w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Internal server error", "message": err.Error()}) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Plan created successfully", "plan": plan, }) log.Printf("✅ Plan created: %s", plan.ID) } // GetPlan retrieves a plan by ID (admin only) func (h *PlanHandler) GetPlan(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] id, err := uuid.Parse(idStr) if err != nil { http.Error(w, "Invalid plan ID", http.StatusBadRequest) return } plan, err := h.planService.GetPlan(id) if err != nil { if err == service.ErrPlanNotFound { http.Error(w, "Plan not found", http.StatusNotFound) } else { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "plan": plan, }) } // ListPlans retrieves all plans (admin only) func (h *PlanHandler) ListPlans(w http.ResponseWriter, r *http.Request) { plans, err := h.planService.ListPlans() if err != nil { log.Printf("❌ Error listing plans: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "plans": plans, }) log.Printf("✅ Listed %d plans", len(plans)) } // ListActivePlans retrieves all active plans (public) func (h *PlanHandler) ListActivePlans(w http.ResponseWriter, r *http.Request) { plans, err := h.planService.ListActivePlans() if err != nil { log.Printf("❌ Error listing active plans: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "plans": plans, }) } // GetActivePlan retrieves an active plan by ID (public) func (h *PlanHandler) GetActivePlan(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) idStr := vars["id"] id, err := uuid.Parse(idStr) if err != nil { http.Error(w, "Invalid plan ID", http.StatusBadRequest) return } plan, err := h.planService.GetPlan(id) if err != nil { if err == service.ErrPlanNotFound { http.Error(w, "Plan not found", http.StatusNotFound) } else { http.Error(w, "Internal server error", http.StatusInternalServerError) } return } // Check if plan is active if !plan.IsActive { http.Error(w, "Plan not available", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "plan": plan, }) } // UpdatePlan updates a plan (admin only) func (h *PlanHandler) UpdatePlan(w http.ResponseWriter, r *http.Request) { log.Printf("📋 UPDATE PLAN - Method: %s", r.Method) vars := mux.Vars(r) idStr := vars["id"] id, err := uuid.Parse(idStr) if err != nil { http.Error(w, "Invalid plan ID", http.StatusBadRequest) return } var req domain.UpdatePlanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { log.Printf("❌ Invalid request body: %v", err) http.Error(w, "Invalid request body", http.StatusBadRequest) return } plan, err := h.planService.UpdatePlan(id, &req) if err != nil { log.Printf("❌ Error updating plan: %v", err) switch err { case service.ErrPlanNotFound: http.Error(w, "Plan not found", http.StatusNotFound) case service.ErrPlanSlugTaken: http.Error(w, err.Error(), http.StatusConflict) case service.ErrInvalidUserRange: http.Error(w, err.Error(), http.StatusBadRequest) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Plan updated successfully", "plan": plan, }) log.Printf("✅ Plan updated: %s", plan.ID) } // DeletePlan deletes a plan (admin only) func (h *PlanHandler) DeletePlan(w http.ResponseWriter, r *http.Request) { log.Printf("📋 DELETE PLAN - Method: %s", r.Method) vars := mux.Vars(r) idStr := vars["id"] id, err := uuid.Parse(idStr) if err != nil { http.Error(w, "Invalid plan ID", http.StatusBadRequest) return } err = h.planService.DeletePlan(id) if err != nil { log.Printf("❌ Error deleting plan: %v", err) switch err { case service.ErrPlanNotFound: http.Error(w, "Plan not found", http.StatusNotFound) default: http.Error(w, "Internal server error", http.StatusInternalServerError) } return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Plan deleted successfully", }) log.Printf("✅ Plan deleted: %s", idStr) } // GetPlanByUserCount returns a plan for a given user count func (h *PlanHandler) GetPlanByUserCount(w http.ResponseWriter, r *http.Request) { userCountStr := r.URL.Query().Get("user_count") if userCountStr == "" { http.Error(w, "user_count parameter required", http.StatusBadRequest) return } userCount, err := strconv.Atoi(userCountStr) if err != nil { http.Error(w, "Invalid user_count", http.StatusBadRequest) return } plan, err := h.planService.GetPlanByUserCount(userCount) if err != nil { http.Error(w, "No plan available for this user count", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "plan": plan, }) }