implement core foundation
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"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"`
|
||||
}
|
||||
|
||||
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 {
|
||||
slog.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 {
|
||||
slog.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 {
|
||||
slog.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 {
|
||||
slog.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 {
|
||||
slog.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 {
|
||||
slog.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 {
|
||||
slog.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)
|
||||
}
|
||||
|
||||
// loadNetworkForAdmin resolves the {id} path param and verifies the caller
|
||||
// is the network's admin. On failure it 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 {
|
||||
slog.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 {
|
||||
slog.Error("failed to write json", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"cloud.google.com/go/firestore"
|
||||
"github.com/flowy-live/llink/internal/auth"
|
||||
"github.com/flowy-live/llink/internal/billing"
|
||||
"github.com/flowy-live/llink/internal/depot"
|
||||
"github.com/flowy-live/llink/internal/human"
|
||||
"github.com/flowy-live/llink/internal/livekit"
|
||||
@@ -30,11 +31,22 @@ type Handler struct {
|
||||
particleSvc particle.Service
|
||||
depotSvc depot.Service
|
||||
waitlistSvc waitlist.Service
|
||||
billingSvc billing.Service
|
||||
livekitClient livekit.Client
|
||||
firestoreClient *firestore.Client
|
||||
}
|
||||
|
||||
func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc network.Service, particleSvc particle.Service, depotSvc depot.Service, waitlistSvc waitlist.Service, livekitClient livekit.Client, firestoreClient *firestore.Client) *Handler {
|
||||
func NewHandler(
|
||||
authSvc auth.AuthService,
|
||||
humanSvc human.Service,
|
||||
networkSvc network.Service,
|
||||
particleSvc particle.Service,
|
||||
depotSvc depot.Service,
|
||||
waitlistSvc waitlist.Service,
|
||||
billingSvc billing.Service,
|
||||
livekitClient livekit.Client,
|
||||
firestoreClient *firestore.Client,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
authSvc: authSvc,
|
||||
humanSvc: humanSvc,
|
||||
@@ -42,6 +54,7 @@ func NewHandler(authSvc auth.AuthService, humanSvc human.Service, networkSvc net
|
||||
particleSvc: particleSvc,
|
||||
depotSvc: depotSvc,
|
||||
waitlistSvc: waitlistSvc,
|
||||
billingSvc: billingSvc,
|
||||
livekitClient: livekitClient,
|
||||
firestoreClient: firestoreClient,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user