package admin import ( "strconv" "time" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/gin-gonic/gin" ) // PaymentHandler handles admin payment management. type PaymentHandler struct { paymentService *service.PaymentService configService *service.PaymentConfigService } // NewPaymentHandler creates a new admin PaymentHandler. func NewPaymentHandler(paymentService *service.PaymentService, configService *service.PaymentConfigService) *PaymentHandler { return &PaymentHandler{ paymentService: paymentService, configService: configService, } } // --- Dashboard --- // GetDashboard returns payment dashboard statistics. // GET /api/v1/admin/payment/dashboard func (h *PaymentHandler) GetDashboard(c *gin.Context) { days := 30 if d := c.Query("days"); d != "" { if v, err := strconv.Atoi(d); err == nil && v > 0 { days = v } } stats, err := h.paymentService.GetDashboardStats(c.Request.Context(), days) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, stats) } // --- Orders --- // ListOrders returns a paginated list of all payment orders. // GET /api/v1/admin/payment/orders func (h *PaymentHandler) ListOrders(c *gin.Context) { page, pageSize := response.ParsePagination(c) var userID int64 if uid := c.Query("user_id"); uid != "" { if v, err := strconv.ParseInt(uid, 10, 64); err == nil { userID = v } } orders, total, err := h.paymentService.AdminListOrders(c.Request.Context(), userID, service.OrderListParams{ Page: page, PageSize: pageSize, Status: c.Query("status"), OrderType: c.Query("order_type"), PaymentType: c.Query("payment_type"), Keyword: c.Query("keyword"), }) if err != nil { response.ErrorFrom(c, err) return } response.Paginated(c, sanitizeAdminPaymentOrdersForResponse(orders), int64(total), page, pageSize) } // GetOrderDetail returns detailed information about a single order. // GET /api/v1/admin/payment/orders/:id func (h *PaymentHandler) GetOrderDetail(c *gin.Context) { orderID, ok := parseIDParam(c, "id") if !ok { return } order, err := h.paymentService.GetOrderByID(c.Request.Context(), orderID) if err != nil { response.ErrorFrom(c, err) return } auditLogs, _ := h.paymentService.GetOrderAuditLogs(c.Request.Context(), orderID) response.Success(c, gin.H{"order": sanitizeAdminPaymentOrderForResponse(order), "auditLogs": auditLogs}) } // CancelOrder cancels a pending order (admin). // POST /api/v1/admin/payment/orders/:id/cancel func (h *PaymentHandler) CancelOrder(c *gin.Context) { orderID, ok := parseIDParam(c, "id") if !ok { return } msg, err := h.paymentService.AdminCancelOrder(c.Request.Context(), orderID) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": msg}) } // RetryFulfillment retries fulfillment for a paid order. // POST /api/v1/admin/payment/orders/:id/retry func (h *PaymentHandler) RetryFulfillment(c *gin.Context) { orderID, ok := parseIDParam(c, "id") if !ok { return } if err := h.paymentService.RetryFulfillment(c.Request.Context(), orderID); err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": "fulfillment retried"}) } type AdminPaymentOrderResult struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` UserEmail string `json:"user_email,omitempty"` UserName string `json:"user_name,omitempty"` UserNotes *string `json:"user_notes,omitempty"` Amount float64 `json:"amount"` PayAmount float64 `json:"pay_amount"` FeeRate float64 `json:"fee_rate"` Currency string `json:"currency"` RechargeCode string `json:"recharge_code,omitempty"` OutTradeNo string `json:"out_trade_no"` PaymentType string `json:"payment_type"` PaymentTradeNo string `json:"payment_trade_no,omitempty"` PayURL *string `json:"pay_url,omitempty"` QRCode *string `json:"qr_code,omitempty"` QRCodeImg *string `json:"qr_code_img,omitempty"` OrderType string `json:"order_type"` PlanID *int64 `json:"plan_id,omitempty"` SubscriptionGroupID *int64 `json:"subscription_group_id,omitempty"` SubscriptionDays *int `json:"subscription_days,omitempty"` ProviderInstanceID *string `json:"provider_instance_id,omitempty"` ProviderKey *string `json:"provider_key,omitempty"` Status string `json:"status"` RefundAmount float64 `json:"refund_amount"` RefundReason *string `json:"refund_reason,omitempty"` RefundAt *time.Time `json:"refund_at,omitempty"` ForceRefund bool `json:"force_refund,omitempty"` RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"` RefundRequestReason *string `json:"refund_request_reason,omitempty"` RefundRequestedBy *string `json:"refund_requested_by,omitempty"` ExpiresAt time.Time `json:"expires_at"` PaidAt *time.Time `json:"paid_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` FailedAt *time.Time `json:"failed_at,omitempty"` FailedReason *string `json:"failed_reason,omitempty"` ClientIP string `json:"client_ip,omitempty"` SrcHost string `json:"src_host,omitempty"` SrcURL *string `json:"src_url,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } func sanitizeAdminPaymentOrdersForResponse(orders []*dbent.PaymentOrder) []*AdminPaymentOrderResult { out := make([]*AdminPaymentOrderResult, 0, len(orders)) for _, order := range orders { if item := sanitizeAdminPaymentOrderForResponse(order); item != nil { out = append(out, item) } } return out } func sanitizeAdminPaymentOrderForResponse(order *dbent.PaymentOrder) *AdminPaymentOrderResult { if order == nil { return nil } return &AdminPaymentOrderResult{ ID: order.ID, UserID: order.UserID, UserEmail: order.UserEmail, UserName: order.UserName, UserNotes: order.UserNotes, Amount: order.Amount, PayAmount: order.PayAmount, FeeRate: order.FeeRate, Currency: service.PaymentOrderCurrency(order), RechargeCode: order.RechargeCode, OutTradeNo: order.OutTradeNo, PaymentType: order.PaymentType, PaymentTradeNo: order.PaymentTradeNo, PayURL: order.PayURL, QRCode: order.QrCode, QRCodeImg: order.QrCodeImg, OrderType: order.OrderType, PlanID: order.PlanID, SubscriptionGroupID: order.SubscriptionGroupID, SubscriptionDays: order.SubscriptionDays, ProviderInstanceID: order.ProviderInstanceID, ProviderKey: order.ProviderKey, Status: order.Status, RefundAmount: order.RefundAmount, RefundReason: order.RefundReason, RefundAt: order.RefundAt, ForceRefund: order.ForceRefund, RefundRequestedAt: order.RefundRequestedAt, RefundRequestReason: order.RefundRequestReason, RefundRequestedBy: order.RefundRequestedBy, ExpiresAt: order.ExpiresAt, PaidAt: order.PaidAt, CompletedAt: order.CompletedAt, FailedAt: order.FailedAt, FailedReason: order.FailedReason, ClientIP: order.ClientIP, SrcHost: order.SrcHost, SrcURL: order.SrcURL, CreatedAt: order.CreatedAt, UpdatedAt: order.UpdatedAt, } } // AdminProcessRefundRequest is the request body for admin refund processing. type AdminProcessRefundRequest struct { Amount float64 `json:"amount"` Reason string `json:"reason"` Force bool `json:"force"` DeductBalance bool `json:"deduct_balance"` } // ProcessRefund processes a refund for an order (admin). // POST /api/v1/admin/payment/orders/:id/refund func (h *PaymentHandler) ProcessRefund(c *gin.Context) { orderID, ok := parseIDParam(c, "id") if !ok { return } var req AdminProcessRefundRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } plan, earlyResult, err := h.paymentService.PrepareRefund(c.Request.Context(), orderID, req.Amount, req.Reason, req.Force, req.DeductBalance) if err != nil { response.ErrorFrom(c, err) return } if earlyResult != nil { response.Success(c, earlyResult) return } result, err := h.paymentService.ExecuteRefund(c.Request.Context(), plan) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, result) } // QueryAndFinalizeRefund queries the provider refund status and finalizes a pending refund. // POST /api/v1/admin/payment/orders/:id/refund/query func (h *PaymentHandler) QueryAndFinalizeRefund(c *gin.Context) { orderID, ok := parseIDParam(c, "id") if !ok { return } result, err := h.paymentService.QueryAndFinalizeRefund(c.Request.Context(), orderID) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, result) } // --- Subscription Plans --- // ListPlans returns all subscription plans. // GET /api/v1/admin/payment/plans func (h *PaymentHandler) ListPlans(c *gin.Context) { plans, err := h.configService.ListPlans(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return } groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans) response.Success(c, adminSubscriptionPlansForResponse(plans, groupInfo)) } type AdminSubscriptionPlanResult struct { ID int64 `json:"id"` GroupID int64 `json:"group_id"` GroupPlatform string `json:"group_platform,omitempty"` GroupName string `json:"group_name,omitempty"` RateMultiplier float64 `json:"rate_multiplier,omitempty"` DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"` WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"` MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"` ModelScopes []string `json:"supported_model_scopes,omitempty"` Name string `json:"name"` Description string `json:"description"` Price float64 `json:"price"` OriginalPrice *float64 `json:"original_price,omitempty"` Currency string `json:"currency,omitempty"` ValidityDays int `json:"validity_days"` ValidityUnit string `json:"validity_unit"` Features string `json:"features"` ProductName string `json:"product_name"` ForSale bool `json:"for_sale"` SortOrder int `json:"sort_order"` CreatedAt time.Time `json:"created_at,omitempty"` UpdatedAt time.Time `json:"updated_at,omitempty"` } func adminSubscriptionPlansForResponse(plans []*dbent.SubscriptionPlan, groupInfo map[int64]service.PlanGroupInfo) []AdminSubscriptionPlanResult { result := make([]AdminSubscriptionPlanResult, 0, len(plans)) for _, p := range plans { if p == nil { continue } gi := groupInfo[p.GroupID] result = append(result, AdminSubscriptionPlanResult{ ID: int64(p.ID), GroupID: p.GroupID, GroupPlatform: gi.Platform, GroupName: gi.Name, RateMultiplier: gi.RateMultiplier, DailyLimitUSD: gi.DailyLimitUSD, WeeklyLimitUSD: gi.WeeklyLimitUSD, MonthlyLimitUSD: gi.MonthlyLimitUSD, ModelScopes: gi.ModelScopes, Name: p.Name, Description: p.Description, Price: p.Price, OriginalPrice: p.OriginalPrice, Currency: p.Currency, ValidityDays: p.ValidityDays, ValidityUnit: p.ValidityUnit, Features: p.Features, ProductName: p.ProductName, ForSale: p.ForSale, SortOrder: p.SortOrder, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt, }) } return result } // CreatePlan creates a new subscription plan. // POST /api/v1/admin/payment/plans func (h *PaymentHandler) CreatePlan(c *gin.Context) { var req service.CreatePlanRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } plan, err := h.configService.CreatePlan(c.Request.Context(), req) if err != nil { response.ErrorFrom(c, err) return } response.Created(c, plan) } // UpdatePlan updates an existing subscription plan. // PUT /api/v1/admin/payment/plans/:id func (h *PaymentHandler) UpdatePlan(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } var req service.UpdatePlanRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } plan, err := h.configService.UpdatePlan(c.Request.Context(), id, req) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, plan) } // DeletePlan deletes a subscription plan. // DELETE /api/v1/admin/payment/plans/:id func (h *PaymentHandler) DeletePlan(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } if err := h.configService.DeletePlan(c.Request.Context(), id); err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": "deleted"}) } // --- Provider Instances --- // ListProviders returns all payment provider instances. // GET /api/v1/admin/payment/providers func (h *PaymentHandler) ListProviders(c *gin.Context) { providers, err := h.configService.ListProviderInstancesWithConfig(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, providers) } // CreateProvider creates a new payment provider instance. // POST /api/v1/admin/payment/providers func (h *PaymentHandler) CreateProvider(c *gin.Context) { var req service.CreateProviderInstanceRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } inst, err := h.configService.CreateProviderInstance(c.Request.Context(), req) if err != nil { response.ErrorFrom(c, err) return } h.paymentService.RefreshProviders(c.Request.Context()) response.Created(c, inst) } // UpdateProvider updates an existing payment provider instance. // PUT /api/v1/admin/payment/providers/:id func (h *PaymentHandler) UpdateProvider(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } var req service.UpdateProviderInstanceRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } inst, err := h.configService.UpdateProviderInstance(c.Request.Context(), id, req) if err != nil { response.ErrorFrom(c, err) return } h.paymentService.RefreshProviders(c.Request.Context()) response.Success(c, inst) } // DeleteProvider deletes a payment provider instance. // DELETE /api/v1/admin/payment/providers/:id func (h *PaymentHandler) DeleteProvider(c *gin.Context) { id, ok := parseIDParam(c, "id") if !ok { return } if err := h.configService.DeleteProviderInstance(c.Request.Context(), id); err != nil { response.ErrorFrom(c, err) return } h.paymentService.RefreshProviders(c.Request.Context()) response.Success(c, gin.H{"message": "deleted"}) } // parseIDParam parses an int64 path parameter. // Returns the parsed ID and true on success; on failure it writes a BadRequest response and returns false. func parseIDParam(c *gin.Context, paramName string) (int64, bool) { id, err := strconv.ParseInt(c.Param(paramName), 10, 64) if err != nil { response.BadRequest(c, "Invalid "+paramName) return 0, false } return id, true } // --- Config --- // GetConfig returns the payment configuration (admin view). // GET /api/v1/admin/payment/config func (h *PaymentHandler) GetConfig(c *gin.Context) { cfg, err := h.configService.GetPaymentConfig(c.Request.Context()) if err != nil { response.ErrorFrom(c, err) return } response.Success(c, cfg) } // UpdateConfig updates the payment configuration. // PUT /api/v1/admin/payment/config func (h *PaymentHandler) UpdateConfig(c *gin.Context) { var req service.UpdatePaymentConfigRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "Invalid request: "+err.Error()) return } if err := h.configService.UpdatePaymentConfig(c.Request.Context(), req); err != nil { response.ErrorFrom(c, err) return } response.Success(c, gin.H{"message": "updated"}) }