package httpapi import ( "errors" "net/http" "github.com/descrybe/descrybe-v2/apps/api/internal/billing" ) // writePlanGate writes a 402 plan_gate payload when err is a known billing gate. // Returns true when the response was written. func writePlanGate(w http.ResponseWriter, err error) bool { if err == nil { return false } if !(errors.Is(err, billing.ErrInsufficientCredits) || errors.Is(err, billing.ErrProductLimitExceeded) || errors.Is(err, billing.ErrAIRequiresUpgrade) || errors.Is(err, billing.ErrEPRELRequiresUpgrade) || errors.Is(err, billing.ErrFeatureDisabled)) { return false } body := map[string]any{ "error": err.Error(), "code": planGateCode(err), "upgrade_url": "/pricing", } if errors.Is(err, billing.ErrFeatureDisabled) { body["error"] = "feature_disabled" if key := billing.FeatureKeyFromError(err); key != "" { body["feature"] = key } } JSON(w, http.StatusPaymentRequired, body) return true } // requireFeatures rejects with 402 when any key is not effective for the company. // Billing nil: pass-through only outside production; production fails closed with 503. // Returns false when the response was already written. func (s *Server) requireFeatures(w http.ResponseWriter, r *http.Request, keys ...string) bool { if len(keys) == 0 { return true } if s.testAssertFeatures != nil { if err := s.testAssertFeatures(r.Context(), keys...); err != nil { if writePlanGate(w, err) { return false } Error(w, http.StatusInternalServerError, "feature check failed") return false } return true } if s.Billing == nil { if s.Config.IsProduction() { Error(w, http.StatusServiceUnavailable, "billing unavailable") return false } return true } cid, ok := CompanyIDFromContext(r.Context()) if !ok { Error(w, http.StatusUnauthorized, "company required") return false } if err := s.Billing.AssertFeatures(r.Context(), cid, keys...); err != nil { if writePlanGate(w, err) { return false } Error(w, http.StatusInternalServerError, "feature check failed") return false } return true } // RequireFeature rejects the request with 402 when the company's effective // features do not include key. Billing nil: pass-through only outside production; // production fails closed with 503 (never silently allow all features). func (s *Server) RequireFeature(key string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !s.requireFeatures(w, r, key) { return } next.ServeHTTP(w, r) }) } }