easyai-ai-gateway/apps/api/internal/httpapi/billing_admin_handlers.go

182 lines
5.3 KiB
Go

package httpapi
import (
"encoding/json"
"errors"
"net"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type walletBalanceRequest struct {
Currency string `json:"currency"`
Balance float64 `json:"balance"`
Reason string `json:"reason"`
IdempotencyKey string `json:"idempotencyKey"`
Metadata map[string]any `json:"metadata"`
}
func (s *Server) setUserWalletBalance(w http.ResponseWriter, r *http.Request) {
actor, _ := auth.UserFromContext(r.Context())
var input walletBalanceRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if input.Balance < 0 {
writeError(w, http.StatusBadRequest, "wallet balance cannot be negative")
return
}
gatewayUserID := strings.TrimSpace(r.PathValue("userID"))
reason := strings.TrimSpace(input.Reason)
if reason == "" {
writeError(w, http.StatusBadRequest, "reason is required")
return
}
var result store.WalletAdjustmentResult
var auditLog store.AuditLog
err := s.store.InTx(r.Context(), func(tx store.Tx) error {
next, err := s.store.SetUserWalletBalanceTx(r.Context(), tx, store.WalletBalanceAdjustmentInput{
GatewayUserID: gatewayUserID,
Currency: input.Currency,
Balance: input.Balance,
Reason: reason,
IdempotencyKey: input.IdempotencyKey,
Metadata: input.Metadata,
})
if err != nil {
return err
}
result = next
record, err := s.store.RecordAuditLogTx(r.Context(), tx, walletAdjustmentAuditInput(r, actor, reason, result))
if err != nil {
return err
}
auditLog = record
return nil
})
if err != nil {
switch {
case store.IsNotFound(err):
writeError(w, http.StatusNotFound, "user not found")
case errors.Is(err, store.ErrWalletBalanceUnchanged):
writeError(w, http.StatusBadRequest, "wallet balance is unchanged")
default:
s.logger.Error("set user wallet balance failed", "error", err)
writeError(w, http.StatusInternalServerError, "set user wallet balance failed")
}
return
}
writeJSON(w, http.StatusOK, map[string]any{
"account": result.Account,
"before": result.Before,
"transaction": result.Transaction,
"auditLog": auditLog,
})
}
func (s *Server) listAuditLogs(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
limit, err := positiveQueryInt(query.Get("limit"), 100)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid limit")
return
}
items, err := s.store.ListAuditLogs(r.Context(), store.AuditLogFilter{
Category: query.Get("category"),
Action: query.Get("action"),
TargetType: query.Get("targetType"),
TargetID: query.Get("targetId"),
Limit: limit,
})
if err != nil {
s.logger.Error("list audit logs failed", "error", err)
writeError(w, http.StatusInternalServerError, "list audit logs failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func walletAdjustmentAuditInput(r *http.Request, actor *auth.User, reason string, result store.WalletAdjustmentResult) store.AuditLogInput {
actorGatewayUserID := ""
actorUserID := ""
actorUsername := ""
actorSource := ""
actorRoles := []string(nil)
if actor != nil {
actorGatewayUserID = uuidText(firstNonEmptyText(actor.GatewayUserID, actor.ID))
actorUserID = actor.ID
actorUsername = actor.Username
actorSource = actor.Source
actorRoles = actor.Roles
}
return store.AuditLogInput{
Category: "billing",
Action: "wallet.balance.set",
ActorGatewayUserID: actorGatewayUserID,
ActorUserID: actorUserID,
ActorUsername: actorUsername,
ActorSource: actorSource,
ActorRoles: actorRoles,
TargetType: "gateway_user",
TargetID: result.Account.GatewayUserID,
TargetGatewayUserID: result.Account.GatewayUserID,
TargetGatewayTenantID: result.Account.GatewayTenantID,
RequestIP: requestIP(r),
UserAgent: r.UserAgent(),
BeforeState: map[string]any{
"walletAccount": result.Before,
},
AfterState: map[string]any{
"walletAccount": result.Account,
"transaction": result.Transaction,
},
Metadata: map[string]any{
"reason": reason,
"currency": result.Account.Currency,
"transactionId": result.Transaction.ID,
"amount": result.Transaction.Amount,
"direction": result.Transaction.Direction,
},
}
}
func requestIP(r *http.Request) string {
if forwarded := strings.TrimSpace(r.Header.Get("X-Forwarded-For")); forwarded != "" {
parts := strings.Split(forwarded, ",")
return strings.TrimSpace(parts[0])
}
if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
return realIP
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return strings.TrimSpace(r.RemoteAddr)
}
func firstNonEmptyText(values ...string) string {
for _, value := range values {
if text := strings.TrimSpace(value); text != "" {
return text
}
}
return ""
}
func uuidText(value string) string {
value = strings.TrimSpace(value)
if len(value) != 36 {
return ""
}
if value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' {
return ""
}
return value
}