diff options
Diffstat (limited to '')
| -rw-r--r-- | internal/adminapi/api.go | 208 |
1 files changed, 200 insertions, 8 deletions
diff --git a/internal/adminapi/api.go b/internal/adminapi/api.go index 4e66d7f..9f460e3 100644 --- a/internal/adminapi/api.go +++ b/internal/adminapi/api.go @@ -144,6 +144,7 @@ func (a *API) Handler() http.Handler { mux.HandleFunc("GET "+apiPrefix+"/models", a.withAuth("platform.read", a.listModels)) mux.HandleFunc("POST "+apiPrefix+"/models", a.withAuth("platform.write", a.createModel)) mux.HandleFunc("POST "+apiPrefix+"/models/{id}/toggle", a.withAuth("platform.write", a.toggleModel)) + mux.HandleFunc("POST "+apiPrefix+"/models/{id}/prices", a.withAuth("platform.write", a.createModelPriceVersion)) mux.HandleFunc("POST "+apiPrefix+"/reload", a.withAuth("platform.write", a.reload)) if a.billing != nil { mux.HandleFunc("GET "+apiPrefix+"/billing/accounts", a.withAuth("billing.read", a.listBillingAccounts)) @@ -152,6 +153,16 @@ func (a *API) Handler() http.Handler { mux.HandleFunc("GET "+apiPrefix+"/billing/orders/{id}", a.withAuth("billing.read", a.getTopUpOrder)) mux.HandleFunc("POST "+apiPrefix+"/billing/adjustments", a.withAuth("billing.adjust", a.adjustBalance)) mux.HandleFunc("POST "+apiPrefix+"/billing/checkout-sessions", a.withAuth("billing.topup", a.createCheckoutSession)) + mux.HandleFunc("POST "+apiPrefix+"/billing/portal-sessions", a.withAuth("billing.topup", a.createPortalSession)) + mux.HandleFunc("POST "+apiPrefix+"/billing/orders/{id}/retry", a.withAuth("billing.topup", a.retryCheckoutSession)) + mux.HandleFunc("POST "+apiPrefix+"/billing/orders/{id}/resolve-missing", a.withAuth("billing.adjust", a.resolveMissingTopUp)) + mux.HandleFunc("POST "+apiPrefix+"/billing/orders/{id}/reverse-missing-credit", a.withAuth("billing.adjust", a.reverseMissingTopUpCredit)) + mux.HandleFunc("POST "+apiPrefix+"/billing/orders/{id}/refund", a.withAuth("billing.adjust", a.createRefund)) + mux.HandleFunc("GET "+apiPrefix+"/billing/refunds", a.withAuth("billing.read", a.listRefunds)) + mux.HandleFunc("GET "+apiPrefix+"/billing/disputes", a.withAuth("billing.read", a.listDisputes)) + mux.HandleFunc("GET "+apiPrefix+"/billing/invoices", a.withAuth("billing.read", a.listInvoices)) + mux.HandleFunc("POST "+apiPrefix+"/billing/reconcile", a.withAuth("billing.adjust", a.reconcileBilling)) + mux.HandleFunc("GET "+apiPrefix+"/billing/export.csv", a.withAuth("billing.read", a.exportBillingCSV)) } mux.HandleFunc("GET "+apiPrefix+"/usage", a.withAuth("usage.read", a.listUsage)) mux.HandleFunc("GET "+apiPrefix+"/usage/summary", a.withAuth("usage.read", a.usageSummary)) @@ -945,11 +956,11 @@ func (a *API) listBillingLedger(w http.ResponseWriter, r *http.Request) { func (a *API) listTopUpOrders(w http.ResponseWriter, r *http.Request) { actor := a.actor(r) - if actor.TenantID == "" { - apierror.Write(w, apierror.Error{Status: http.StatusBadRequest, Type: "tenant_required", Message: "A tenant is required"}, requestID(r)) - return + tenantID := actor.TenantID + if tenantID == "" { + tenantID = r.URL.Query().Get("tenant_id") } - result, err := a.billing.ListTopUpOrders(r.Context(), actor.TenantID, 50) + result, err := a.billing.ListTopUpOrders(r.Context(), tenantID, 100) if err != nil { a.billingError(w, r, err) return @@ -959,10 +970,6 @@ func (a *API) listTopUpOrders(w http.ResponseWriter, r *http.Request) { func (a *API) getTopUpOrder(w http.ResponseWriter, r *http.Request) { actor := a.actor(r) - if actor.TenantID == "" { - apierror.Write(w, apierror.Error{Status: http.StatusBadRequest, Type: "tenant_required", Message: "A tenant is required"}, requestID(r)) - return - } result, err := a.billing.GetTopUpOrder(r.Context(), actor.TenantID, r.PathValue("id")) if err != nil { a.billingError(w, r, err) @@ -992,6 +999,7 @@ func (a *API) createCheckoutSession(w http.ResponseWriter, r *http.Request) { if tenantID := a.actor(r).TenantID; tenantID != "" { input.TenantID = tenantID } + input.CustomerEmail = a.actor(r).Email result, err := a.billing.CreateCheckout(r.Context(), input) if err != nil { a.billingError(w, r, err) @@ -1000,6 +1008,169 @@ func (a *API) createCheckoutSession(w http.ResponseWriter, r *http.Request) { writeStatusJSON(w, http.StatusCreated, result) } +func (a *API) createPortalSession(w http.ResponseWriter, r *http.Request) { + tenantID := a.actor(r).TenantID + if tenantID == "" { + apierror.Write(w, apierror.Error{Status: http.StatusBadRequest, Type: "tenant_required", Message: "A tenant is required"}, requestID(r)) + return + } + result, err := a.billing.CreatePortalSession(r.Context(), tenantID) + if err != nil { + a.billingError(w, r, err) + return + } + writeStatusJSON(w, http.StatusCreated, result) +} + +func (a *API) retryCheckoutSession(w http.ResponseWriter, r *http.Request) { + actor := a.actor(r) + if actor.TenantID == "" { + a.scopeError(w, r) + return + } + result, err := a.billing.RetryCheckout(r.Context(), actor.TenantID, r.PathValue("id"), actor.Email) + if err != nil { + a.billingError(w, r, err) + return + } + writeStatusJSON(w, http.StatusCreated, result) +} + +func (a *API) resolveMissingTopUp(w http.ResponseWriter, r *http.Request) { + var input billing.ResolveMissingTopUpInput + if !decodeBody(w, r, &input) { + return + } + tenantID := r.URL.Query().Get("tenant_id") + if actorTenant := a.actor(r).TenantID; actorTenant != "" { + tenantID = actorTenant + } + if tenantID == "" { + a.scopeError(w, r) + return + } + actor := a.actor(r) + actorType := "console_user" + if actor.Bootstrap { + actorType = "bootstrap" + } + result, err := a.billing.ResolveMissingTopUp(r.Context(), tenantID, r.PathValue("id"), input, + billing.ResolutionActor{ID: actor.ID, Type: actorType}) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) reverseMissingTopUpCredit(w http.ResponseWriter, r *http.Request) { + var input billing.ResolveMissingTopUpInput + if !decodeBody(w, r, &input) { + return + } + tenantID := r.URL.Query().Get("tenant_id") + actor := a.actor(r) + if actor.TenantID != "" { + tenantID = actor.TenantID + } + if tenantID == "" { + a.scopeError(w, r) + return + } + actorType := "console_user" + if actor.Bootstrap { + actorType = "bootstrap" + } + result, err := a.billing.ReverseMissingTopUpCredit(r.Context(), tenantID, r.PathValue("id"), input, + billing.ResolutionActor{ID: actor.ID, Type: actorType}) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) createRefund(w http.ResponseWriter, r *http.Request) { + var input billing.RefundInput + if !decodeBody(w, r, &input) { + return + } + tenantID := r.URL.Query().Get("tenant_id") + if actorTenant := a.actor(r).TenantID; actorTenant != "" { + tenantID = actorTenant + } + if tenantID == "" { + apierror.Write(w, apierror.Error{Status: http.StatusBadRequest, Type: "tenant_required", Message: "tenant_id is required"}, requestID(r)) + return + } + result, err := a.billing.CreateRefund(r.Context(), tenantID, r.PathValue("id"), input) + if err != nil { + a.billingError(w, r, err) + return + } + writeStatusJSON(w, http.StatusAccepted, result) +} + +func (a *API) listRefunds(w http.ResponseWriter, r *http.Request) { + tenantID := a.actor(r).TenantID + if tenantID == "" { + tenantID = r.URL.Query().Get("tenant_id") + } + result, err := a.billing.ListRefunds(r.Context(), tenantID, 200) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) listDisputes(w http.ResponseWriter, r *http.Request) { + tenantID := a.actor(r).TenantID + if tenantID == "" { + tenantID = r.URL.Query().Get("tenant_id") + } + result, err := a.billing.ListDisputes(r.Context(), tenantID, 200) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) listInvoices(w http.ResponseWriter, r *http.Request) { + tenantID := a.actor(r).TenantID + if tenantID == "" { + tenantID = r.URL.Query().Get("tenant_id") + } + result, err := a.billing.ListInvoices(r.Context(), tenantID, 200) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) reconcileBilling(w http.ResponseWriter, r *http.Request) { + result, err := a.billing.Reconcile(r.Context(), 200) + if err != nil { + a.billingError(w, r, err) + return + } + writeJSON(w, result) +} + +func (a *API) exportBillingCSV(w http.ResponseWriter, r *http.Request) { + tenantID := a.actor(r).TenantID + if tenantID == "" { + tenantID = r.URL.Query().Get("tenant_id") + } + w.Header().Set("Content-Type", "text/csv; charset=utf-8") + w.Header().Set("Content-Disposition", `attachment; filename="aigw-financial-ledger.csv"`) + if err := a.billing.WriteFinancialCSV(r.Context(), tenantID, w); err != nil { + a.logger.Error("billing_export_failed", "error", err) + } +} + func (a *API) listTenants(w http.ResponseWriter, r *http.Request) { result, err := a.store.ListTenantsFor(r.Context(), a.actor(r).TenantID) if err != nil { @@ -1185,6 +1356,23 @@ func (a *API) toggleModel(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"id": id, "enabled": input.Enabled}) } +func (a *API) createModelPriceVersion(w http.ResponseWriter, r *http.Request) { + var input controlplane.CreatePriceVersionInput + if !decodeBody(w, r, &input) { + return + } + id := r.PathValue("id") + generation, err := a.store.CreateModelPriceVersion(r.Context(), id, input) + if err != nil { + a.mutationError(w, r, err) + return + } + if !a.changed(w, r, generation, "model_price", id) { + return + } + writeStatusJSON(w, http.StatusCreated, map[string]any{"model_id": id, "generation": generation}) +} + func (a *API) reload(w http.ResponseWriter, r *http.Request) { generation, err := a.manager.Reload(r.Context()) if err != nil { @@ -1375,6 +1563,10 @@ func (a *API) billingError(w http.ResponseWriter, r *http.Request, err error) { status = http.StatusNotFound typeName = "topup_order_not_found" message = "Top-up order was not found" + case errors.Is(err, billing.ErrCannotResolveTopUp): + status = http.StatusConflict + typeName = "topup_order_not_resolvable" + message = err.Error() default: a.logger.Error("admin_billing_error", "error", err) } |
