90 lines
2.6 KiB
Go
90 lines
2.6 KiB
Go
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)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|