Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user