Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
158 lines
5.2 KiB
Go
158 lines
5.2 KiB
Go
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)
|
|
}
|