Add a full payment and subscription system supporting EasyPay (Alipay/WeChat), Stripe, and direct Alipay/WeChat Pay providers with multi-instance load balancing.
324 lines
8.9 KiB
Go
324 lines
8.9 KiB
Go
package admin
|
|
|
|
import (
|
|
"strconv"
|
|
|
|
"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, 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": 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"})
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// --- 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
|
|
}
|
|
response.Success(c, plans)
|
|
}
|
|
|
|
// 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"})
|
|
}
|