package httpapi import ( "errors" "log" "net/http" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" "github.com/google/uuid" ) func (s *Server) handleCreditsOverview(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) overview, err := s.Billing.CreditsOverview(r.Context(), cid, s.Config.LowCreditsThreshold) if err != nil { Error(w, http.StatusInternalServerError, "failed to load credits") return } JSON(w, http.StatusOK, overview) } func (s *Server) handleBillingUsage(w http.ResponseWriter, r *http.Request) { cid, _ := CompanyIDFromContext(r.Context()) usage, err := s.Billing.UsageSummary(r.Context(), cid, r.URL.Query().Get("range")) if err != nil { Error(w, http.StatusInternalServerError, "failed to load usage") return } JSON(w, http.StatusOK, usage) } // handleListPlans returns every plan (including client deals) for platform admin. func (s *Server) handleListPlans(w http.ResponseWriter, r *http.Request) { if s.Billing == nil { Error(w, http.StatusServiceUnavailable, "billing unavailable") return } _ = s.Billing.EnsureDefaultPlans(r.Context()) plans, err := s.Billing.ListPlans(r.Context()) if err != nil { Error(w, http.StatusInternalServerError, "failed to list plans") return } // Permission matrices are admin-sensitive; never let shared caches retain them. w.Header().Set("Cache-Control", "private, no-store") JSON(w, http.StatusOK, map[string]any{"plans": plans}) } // handleListPublicPlans returns Free/Starter/Plus/Growth/Business/Scale/Enterprise only. // Hides client-specific deals (A1, Merkur trial, legacy ladders) from company UI. func (s *Server) handleListPublicPlans(w http.ResponseWriter, r *http.Request) { if s.Billing == nil || s.Billing.Pool == nil { // Empty billing must not 500 on the public pricing surface. JSON(w, http.StatusOK, map[string]any{ "plans": []any{}, "credits_per_ai_product": billing.CreditsPerAIProduct, "assumed_content_langs": billing.AssumedPrimaryContentLanguages, }) return } _ = s.Billing.EnsureDefaultPlans(r.Context()) plans, err := s.Billing.ListPublicPlans(r.Context()) if err != nil { Error(w, http.StatusInternalServerError, "failed to list plans") return } JSON(w, http.StatusOK, map[string]any{ "plans": plans, "credits_per_ai_product": billing.CreditsPerAIProduct, "assumed_content_langs": billing.AssumedPrimaryContentLanguages, }) } // handleListCreditPacks returns one-time AI credit top-up packages for Checkout. func (s *Server) handleListCreditPacks(w http.ResponseWriter, r *http.Request) { JSON(w, http.StatusOK, map[string]any{"packs": billing.DefaultCreditPacks()}) } func (s *Server) handleUpsertPlan(w http.ResponseWriter, r *http.Request) { var body billing.Plan if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } plan, err := s.Billing.UpsertPlan(r.Context(), body) if err != nil { ClientOrLog(w, http.StatusBadRequest, "could not save plan", err, billing.ClientError) return } JSON(w, http.StatusOK, plan) } func (s *Server) handleAssignPlan(w http.ResponseWriter, r *http.Request) { var body struct { CompanyID string `json:"company_id"` PlanID int64 `json:"plan_id"` IsTrial bool `json:"is_trial"` TrialCredits int `json:"trial_credits"` } if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } cid, err := uuid.Parse(body.CompanyID) if err != nil { Error(w, http.StatusBadRequest, "invalid company_id") return } if body.PlanID <= 0 { Error(w, http.StatusBadRequest, "invalid plan_id") return } if err := s.Billing.AssignPlan(r.Context(), cid, body.PlanID, body.IsTrial, body.TrialCredits); err != nil { if errors.Is(err, billing.ErrPlanNotFound) { Error(w, http.StatusNotFound, billing.ErrPlanNotFound.Error()) return } ClientOrLog(w, http.StatusBadRequest, "could not assign plan", err, billing.ClientError) return } JSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleAddCredits(w http.ResponseWriter, r *http.Request) { var body struct { CompanyID string `json:"company_id"` Amount int `json:"amount"` } if err := DecodeJSON(r, &body); err != nil { Error(w, http.StatusBadRequest, "invalid json") return } cid, err := uuid.Parse(body.CompanyID) if err != nil { Error(w, http.StatusBadRequest, "invalid company_id") return } if err := s.Billing.AddCredits(r.Context(), cid, body.Amount); err != nil { ClientOrLog(w, http.StatusBadRequest, "could not add credits", err, billing.ClientError) return } JSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) handleRunBillingCycles(w http.ResponseWriter, r *http.Request) { res, err := s.Billing.RunDueBillingCycles(r.Context()) if err != nil && res.Processed == 0 && res.Failed == 0 { LogAndError(w, http.StatusInternalServerError, "billing cycle run failed", err) return } out := map[string]any{"processed": res.Processed, "failed": res.Failed} if err != nil { log.Printf("httpapi: billing cycle run completed with errors: %v", err) out["error"] = "billing cycle run completed with errors" } JSON(w, http.StatusOK, out) }