| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503 |
- 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"})
- }
|