package handler import ( "encoding/json" "errors" "fmt" "io" "net/http" "github.com/flowy-live/llink/internal/utils/flog" "github.com/flowy-live/llink/internal/billing" "github.com/flowy-live/llink/internal/middleware" "github.com/flowy-live/llink/internal/network" ) type CreateCheckoutSessionRequest struct { Cadence string `json:"cadence"` } type CheckoutSessionResponse struct { URL string `json:"url"` } type PortalSessionResponse struct { URL string `json:"url"` } // GetNetworkUsage reports today's usage, daily limit (nil on pro), and reset // time. Open to any network member since the UI surfaces it to every sender. func (h *Handler) GetNetworkUsage(w http.ResponseWriter, r *http.Request) { humanId, ok := middleware.HumanIdFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } networkID := r.PathValue("id") if networkID == "" { http.Error(w, "network id is required", http.StatusBadRequest) return } isMember, err := h.networkSvc.IsMember(r.Context(), networkID, humanId) if err != nil { flog.Error("failed to check network membership", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } if !isMember { http.Error(w, "forbidden", http.StatusForbidden) return } usage, err := h.billingSvc.GetUsage(r.Context(), networkID) if err != nil { flog.Error("failed to get network usage", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return } writeJSON(w, usage) } func (h *Handler) GetNetworkBilling(w http.ResponseWriter, r *http.Request) { net, _, ok := h.loadNetworkForAdmin(w, r) if !ok { return } status, err := h.billingSvc.GetStatus(r.Context(), net.ID) if err != nil { flog.Error("failed to get billing status", "error", err, "network_id", net.ID) http.Error(w, "internal server error", http.StatusInternalServerError) return } writeJSON(w, status) } func (h *Handler) CreateCheckoutSession(w http.ResponseWriter, r *http.Request) { net, adminHumanId, ok := h.loadNetworkForAdmin(w, r) if !ok { return } var req CreateCheckoutSessionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid request body", http.StatusBadRequest) return } cadence := billing.Cadence(req.Cadence) if !cadence.IsValid() { http.Error(w, "invalid cadence", http.StatusBadRequest) return } adminHuman, err := h.humanSvc.GetByID(r.Context(), adminHumanId) if err != nil { flog.Error("failed to load admin human", "error", err, "human_id", adminHumanId) http.Error(w, "internal server error", http.StatusInternalServerError) return } seats, err := h.networkSvc.CountSeats(r.Context(), net.ID) if err != nil { flog.Error("failed to count seats", "error", err, "network_id", net.ID) http.Error(w, "internal server error", http.StatusInternalServerError) return } url, err := h.billingSvc.CreateCheckoutSession(r.Context(), billing.CheckoutParams{ NetworkID: net.ID, AdminHumanID: adminHumanId, AdminEmail: adminHuman.Email, Cadence: cadence, Seats: seats, }) if err != nil { flog.Error("failed to create checkout session", "error", err, "network_id", net.ID) http.Error(w, "internal server error", http.StatusInternalServerError) return } writeJSON(w, CheckoutSessionResponse{URL: url}) } func (h *Handler) CreatePortalSession(w http.ResponseWriter, r *http.Request) { net, _, ok := h.loadNetworkForAdmin(w, r) if !ok { return } url, err := h.billingSvc.CreatePortalSession(r.Context(), net.ID) if errors.Is(err, billing.ErrNoActiveSubscription) { http.Error(w, "no active subscription", http.StatusConflict) return } if err != nil { flog.Error("failed to create portal session", "error", err, "network_id", net.ID) http.Error(w, "internal server error", http.StatusInternalServerError) return } writeJSON(w, PortalSessionResponse{URL: url}) } const maxStripeWebhookBytes = 1 << 20 // 1 MiB func (h *Handler) HandleStripeWebhook(w http.ResponseWriter, r *http.Request) { payload, err := io.ReadAll(io.LimitReader(r.Body, maxStripeWebhookBytes)) if err != nil { flog.Warn("stripe webhook: failed to read body", "error", err) http.Error(w, "bad request", http.StatusBadRequest) return } signature := r.Header.Get("Stripe-Signature") if err := h.billingSvc.HandleWebhook(r.Context(), payload, signature); err != nil { flog.Error("stripe webhook failed", "error", err) formattedErr := fmt.Errorf("webhook processing failed: %w", err) http.Error(w, formattedErr.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) } // Resolves {id}, verifies the caller is admin. On failure writes the HTTP // error and returns ok=false. func (h *Handler) loadNetworkForAdmin(w http.ResponseWriter, r *http.Request) (*network.Network, string, bool) { humanId, ok := middleware.HumanIdFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return nil, "", false } networkID := r.PathValue("id") if networkID == "" { http.Error(w, "network id is required", http.StatusBadRequest) return nil, "", false } net, err := h.networkSvc.GetByID(r.Context(), networkID) if errors.Is(err, network.ErrNotFound) { http.Error(w, "network not found", http.StatusNotFound) return nil, "", false } if err != nil { flog.Error("failed to load network", "error", err, "network_id", networkID) http.Error(w, "internal server error", http.StatusInternalServerError) return nil, "", false } if net.AdminHumanId != humanId { http.Error(w, "forbidden", http.StatusForbidden) return nil, "", false } return net, humanId, true } func writeJSON(w http.ResponseWriter, body any) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(body); err != nil { flog.Error("failed to write json", "error", err) } }