payment_handler.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. package admin
  2. import (
  3. "strconv"
  4. "time"
  5. dbent "github.com/Wei-Shaw/sub2api/ent"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  7. "github.com/Wei-Shaw/sub2api/internal/service"
  8. "github.com/gin-gonic/gin"
  9. )
  10. // PaymentHandler handles admin payment management.
  11. type PaymentHandler struct {
  12. paymentService *service.PaymentService
  13. configService *service.PaymentConfigService
  14. }
  15. // NewPaymentHandler creates a new admin PaymentHandler.
  16. func NewPaymentHandler(paymentService *service.PaymentService, configService *service.PaymentConfigService) *PaymentHandler {
  17. return &PaymentHandler{
  18. paymentService: paymentService,
  19. configService: configService,
  20. }
  21. }
  22. // --- Dashboard ---
  23. // GetDashboard returns payment dashboard statistics.
  24. // GET /api/v1/admin/payment/dashboard
  25. func (h *PaymentHandler) GetDashboard(c *gin.Context) {
  26. days := 30
  27. if d := c.Query("days"); d != "" {
  28. if v, err := strconv.Atoi(d); err == nil && v > 0 {
  29. days = v
  30. }
  31. }
  32. stats, err := h.paymentService.GetDashboardStats(c.Request.Context(), days)
  33. if err != nil {
  34. response.ErrorFrom(c, err)
  35. return
  36. }
  37. response.Success(c, stats)
  38. }
  39. // --- Orders ---
  40. // ListOrders returns a paginated list of all payment orders.
  41. // GET /api/v1/admin/payment/orders
  42. func (h *PaymentHandler) ListOrders(c *gin.Context) {
  43. page, pageSize := response.ParsePagination(c)
  44. var userID int64
  45. if uid := c.Query("user_id"); uid != "" {
  46. if v, err := strconv.ParseInt(uid, 10, 64); err == nil {
  47. userID = v
  48. }
  49. }
  50. orders, total, err := h.paymentService.AdminListOrders(c.Request.Context(), userID, service.OrderListParams{
  51. Page: page,
  52. PageSize: pageSize,
  53. Status: c.Query("status"),
  54. OrderType: c.Query("order_type"),
  55. PaymentType: c.Query("payment_type"),
  56. Keyword: c.Query("keyword"),
  57. })
  58. if err != nil {
  59. response.ErrorFrom(c, err)
  60. return
  61. }
  62. response.Paginated(c, sanitizeAdminPaymentOrdersForResponse(orders), int64(total), page, pageSize)
  63. }
  64. // GetOrderDetail returns detailed information about a single order.
  65. // GET /api/v1/admin/payment/orders/:id
  66. func (h *PaymentHandler) GetOrderDetail(c *gin.Context) {
  67. orderID, ok := parseIDParam(c, "id")
  68. if !ok {
  69. return
  70. }
  71. order, err := h.paymentService.GetOrderByID(c.Request.Context(), orderID)
  72. if err != nil {
  73. response.ErrorFrom(c, err)
  74. return
  75. }
  76. auditLogs, _ := h.paymentService.GetOrderAuditLogs(c.Request.Context(), orderID)
  77. response.Success(c, gin.H{"order": sanitizeAdminPaymentOrderForResponse(order), "auditLogs": auditLogs})
  78. }
  79. // CancelOrder cancels a pending order (admin).
  80. // POST /api/v1/admin/payment/orders/:id/cancel
  81. func (h *PaymentHandler) CancelOrder(c *gin.Context) {
  82. orderID, ok := parseIDParam(c, "id")
  83. if !ok {
  84. return
  85. }
  86. msg, err := h.paymentService.AdminCancelOrder(c.Request.Context(), orderID)
  87. if err != nil {
  88. response.ErrorFrom(c, err)
  89. return
  90. }
  91. response.Success(c, gin.H{"message": msg})
  92. }
  93. // RetryFulfillment retries fulfillment for a paid order.
  94. // POST /api/v1/admin/payment/orders/:id/retry
  95. func (h *PaymentHandler) RetryFulfillment(c *gin.Context) {
  96. orderID, ok := parseIDParam(c, "id")
  97. if !ok {
  98. return
  99. }
  100. if err := h.paymentService.RetryFulfillment(c.Request.Context(), orderID); err != nil {
  101. response.ErrorFrom(c, err)
  102. return
  103. }
  104. response.Success(c, gin.H{"message": "fulfillment retried"})
  105. }
  106. type AdminPaymentOrderResult struct {
  107. ID int64 `json:"id"`
  108. UserID int64 `json:"user_id"`
  109. UserEmail string `json:"user_email,omitempty"`
  110. UserName string `json:"user_name,omitempty"`
  111. UserNotes *string `json:"user_notes,omitempty"`
  112. Amount float64 `json:"amount"`
  113. PayAmount float64 `json:"pay_amount"`
  114. FeeRate float64 `json:"fee_rate"`
  115. Currency string `json:"currency"`
  116. RechargeCode string `json:"recharge_code,omitempty"`
  117. OutTradeNo string `json:"out_trade_no"`
  118. PaymentType string `json:"payment_type"`
  119. PaymentTradeNo string `json:"payment_trade_no,omitempty"`
  120. PayURL *string `json:"pay_url,omitempty"`
  121. QRCode *string `json:"qr_code,omitempty"`
  122. QRCodeImg *string `json:"qr_code_img,omitempty"`
  123. OrderType string `json:"order_type"`
  124. PlanID *int64 `json:"plan_id,omitempty"`
  125. SubscriptionGroupID *int64 `json:"subscription_group_id,omitempty"`
  126. SubscriptionDays *int `json:"subscription_days,omitempty"`
  127. ProviderInstanceID *string `json:"provider_instance_id,omitempty"`
  128. ProviderKey *string `json:"provider_key,omitempty"`
  129. Status string `json:"status"`
  130. RefundAmount float64 `json:"refund_amount"`
  131. RefundReason *string `json:"refund_reason,omitempty"`
  132. RefundAt *time.Time `json:"refund_at,omitempty"`
  133. ForceRefund bool `json:"force_refund,omitempty"`
  134. RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"`
  135. RefundRequestReason *string `json:"refund_request_reason,omitempty"`
  136. RefundRequestedBy *string `json:"refund_requested_by,omitempty"`
  137. ExpiresAt time.Time `json:"expires_at"`
  138. PaidAt *time.Time `json:"paid_at,omitempty"`
  139. CompletedAt *time.Time `json:"completed_at,omitempty"`
  140. FailedAt *time.Time `json:"failed_at,omitempty"`
  141. FailedReason *string `json:"failed_reason,omitempty"`
  142. ClientIP string `json:"client_ip,omitempty"`
  143. SrcHost string `json:"src_host,omitempty"`
  144. SrcURL *string `json:"src_url,omitempty"`
  145. CreatedAt time.Time `json:"created_at"`
  146. UpdatedAt time.Time `json:"updated_at"`
  147. }
  148. func sanitizeAdminPaymentOrdersForResponse(orders []*dbent.PaymentOrder) []*AdminPaymentOrderResult {
  149. out := make([]*AdminPaymentOrderResult, 0, len(orders))
  150. for _, order := range orders {
  151. if item := sanitizeAdminPaymentOrderForResponse(order); item != nil {
  152. out = append(out, item)
  153. }
  154. }
  155. return out
  156. }
  157. func sanitizeAdminPaymentOrderForResponse(order *dbent.PaymentOrder) *AdminPaymentOrderResult {
  158. if order == nil {
  159. return nil
  160. }
  161. return &AdminPaymentOrderResult{
  162. ID: order.ID,
  163. UserID: order.UserID,
  164. UserEmail: order.UserEmail,
  165. UserName: order.UserName,
  166. UserNotes: order.UserNotes,
  167. Amount: order.Amount,
  168. PayAmount: order.PayAmount,
  169. FeeRate: order.FeeRate,
  170. Currency: service.PaymentOrderCurrency(order),
  171. RechargeCode: order.RechargeCode,
  172. OutTradeNo: order.OutTradeNo,
  173. PaymentType: order.PaymentType,
  174. PaymentTradeNo: order.PaymentTradeNo,
  175. PayURL: order.PayURL,
  176. QRCode: order.QrCode,
  177. QRCodeImg: order.QrCodeImg,
  178. OrderType: order.OrderType,
  179. PlanID: order.PlanID,
  180. SubscriptionGroupID: order.SubscriptionGroupID,
  181. SubscriptionDays: order.SubscriptionDays,
  182. ProviderInstanceID: order.ProviderInstanceID,
  183. ProviderKey: order.ProviderKey,
  184. Status: order.Status,
  185. RefundAmount: order.RefundAmount,
  186. RefundReason: order.RefundReason,
  187. RefundAt: order.RefundAt,
  188. ForceRefund: order.ForceRefund,
  189. RefundRequestedAt: order.RefundRequestedAt,
  190. RefundRequestReason: order.RefundRequestReason,
  191. RefundRequestedBy: order.RefundRequestedBy,
  192. ExpiresAt: order.ExpiresAt,
  193. PaidAt: order.PaidAt,
  194. CompletedAt: order.CompletedAt,
  195. FailedAt: order.FailedAt,
  196. FailedReason: order.FailedReason,
  197. ClientIP: order.ClientIP,
  198. SrcHost: order.SrcHost,
  199. SrcURL: order.SrcURL,
  200. CreatedAt: order.CreatedAt,
  201. UpdatedAt: order.UpdatedAt,
  202. }
  203. }
  204. // AdminProcessRefundRequest is the request body for admin refund processing.
  205. type AdminProcessRefundRequest struct {
  206. Amount float64 `json:"amount"`
  207. Reason string `json:"reason"`
  208. Force bool `json:"force"`
  209. DeductBalance bool `json:"deduct_balance"`
  210. }
  211. // ProcessRefund processes a refund for an order (admin).
  212. // POST /api/v1/admin/payment/orders/:id/refund
  213. func (h *PaymentHandler) ProcessRefund(c *gin.Context) {
  214. orderID, ok := parseIDParam(c, "id")
  215. if !ok {
  216. return
  217. }
  218. var req AdminProcessRefundRequest
  219. if err := c.ShouldBindJSON(&req); err != nil {
  220. response.BadRequest(c, "Invalid request: "+err.Error())
  221. return
  222. }
  223. plan, earlyResult, err := h.paymentService.PrepareRefund(c.Request.Context(), orderID, req.Amount, req.Reason, req.Force, req.DeductBalance)
  224. if err != nil {
  225. response.ErrorFrom(c, err)
  226. return
  227. }
  228. if earlyResult != nil {
  229. response.Success(c, earlyResult)
  230. return
  231. }
  232. result, err := h.paymentService.ExecuteRefund(c.Request.Context(), plan)
  233. if err != nil {
  234. response.ErrorFrom(c, err)
  235. return
  236. }
  237. response.Success(c, result)
  238. }
  239. // QueryAndFinalizeRefund queries the provider refund status and finalizes a pending refund.
  240. // POST /api/v1/admin/payment/orders/:id/refund/query
  241. func (h *PaymentHandler) QueryAndFinalizeRefund(c *gin.Context) {
  242. orderID, ok := parseIDParam(c, "id")
  243. if !ok {
  244. return
  245. }
  246. result, err := h.paymentService.QueryAndFinalizeRefund(c.Request.Context(), orderID)
  247. if err != nil {
  248. response.ErrorFrom(c, err)
  249. return
  250. }
  251. response.Success(c, result)
  252. }
  253. // --- Subscription Plans ---
  254. // ListPlans returns all subscription plans.
  255. // GET /api/v1/admin/payment/plans
  256. func (h *PaymentHandler) ListPlans(c *gin.Context) {
  257. plans, err := h.configService.ListPlans(c.Request.Context())
  258. if err != nil {
  259. response.ErrorFrom(c, err)
  260. return
  261. }
  262. groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans)
  263. response.Success(c, adminSubscriptionPlansForResponse(plans, groupInfo))
  264. }
  265. type AdminSubscriptionPlanResult struct {
  266. ID int64 `json:"id"`
  267. GroupID int64 `json:"group_id"`
  268. GroupPlatform string `json:"group_platform,omitempty"`
  269. GroupName string `json:"group_name,omitempty"`
  270. RateMultiplier float64 `json:"rate_multiplier,omitempty"`
  271. DailyLimitUSD *float64 `json:"daily_limit_usd,omitempty"`
  272. WeeklyLimitUSD *float64 `json:"weekly_limit_usd,omitempty"`
  273. MonthlyLimitUSD *float64 `json:"monthly_limit_usd,omitempty"`
  274. ModelScopes []string `json:"supported_model_scopes,omitempty"`
  275. Name string `json:"name"`
  276. Description string `json:"description"`
  277. Price float64 `json:"price"`
  278. OriginalPrice *float64 `json:"original_price,omitempty"`
  279. Currency string `json:"currency,omitempty"`
  280. ValidityDays int `json:"validity_days"`
  281. ValidityUnit string `json:"validity_unit"`
  282. Features string `json:"features"`
  283. ProductName string `json:"product_name"`
  284. ForSale bool `json:"for_sale"`
  285. SortOrder int `json:"sort_order"`
  286. CreatedAt time.Time `json:"created_at,omitempty"`
  287. UpdatedAt time.Time `json:"updated_at,omitempty"`
  288. }
  289. func adminSubscriptionPlansForResponse(plans []*dbent.SubscriptionPlan, groupInfo map[int64]service.PlanGroupInfo) []AdminSubscriptionPlanResult {
  290. result := make([]AdminSubscriptionPlanResult, 0, len(plans))
  291. for _, p := range plans {
  292. if p == nil {
  293. continue
  294. }
  295. gi := groupInfo[p.GroupID]
  296. result = append(result, AdminSubscriptionPlanResult{
  297. ID: int64(p.ID),
  298. GroupID: p.GroupID,
  299. GroupPlatform: gi.Platform,
  300. GroupName: gi.Name,
  301. RateMultiplier: gi.RateMultiplier,
  302. DailyLimitUSD: gi.DailyLimitUSD,
  303. WeeklyLimitUSD: gi.WeeklyLimitUSD,
  304. MonthlyLimitUSD: gi.MonthlyLimitUSD,
  305. ModelScopes: gi.ModelScopes,
  306. Name: p.Name,
  307. Description: p.Description,
  308. Price: p.Price,
  309. OriginalPrice: p.OriginalPrice,
  310. Currency: p.Currency,
  311. ValidityDays: p.ValidityDays,
  312. ValidityUnit: p.ValidityUnit,
  313. Features: p.Features,
  314. ProductName: p.ProductName,
  315. ForSale: p.ForSale,
  316. SortOrder: p.SortOrder,
  317. CreatedAt: p.CreatedAt,
  318. UpdatedAt: p.UpdatedAt,
  319. })
  320. }
  321. return result
  322. }
  323. // CreatePlan creates a new subscription plan.
  324. // POST /api/v1/admin/payment/plans
  325. func (h *PaymentHandler) CreatePlan(c *gin.Context) {
  326. var req service.CreatePlanRequest
  327. if err := c.ShouldBindJSON(&req); err != nil {
  328. response.BadRequest(c, "Invalid request: "+err.Error())
  329. return
  330. }
  331. plan, err := h.configService.CreatePlan(c.Request.Context(), req)
  332. if err != nil {
  333. response.ErrorFrom(c, err)
  334. return
  335. }
  336. response.Created(c, plan)
  337. }
  338. // UpdatePlan updates an existing subscription plan.
  339. // PUT /api/v1/admin/payment/plans/:id
  340. func (h *PaymentHandler) UpdatePlan(c *gin.Context) {
  341. id, ok := parseIDParam(c, "id")
  342. if !ok {
  343. return
  344. }
  345. var req service.UpdatePlanRequest
  346. if err := c.ShouldBindJSON(&req); err != nil {
  347. response.BadRequest(c, "Invalid request: "+err.Error())
  348. return
  349. }
  350. plan, err := h.configService.UpdatePlan(c.Request.Context(), id, req)
  351. if err != nil {
  352. response.ErrorFrom(c, err)
  353. return
  354. }
  355. response.Success(c, plan)
  356. }
  357. // DeletePlan deletes a subscription plan.
  358. // DELETE /api/v1/admin/payment/plans/:id
  359. func (h *PaymentHandler) DeletePlan(c *gin.Context) {
  360. id, ok := parseIDParam(c, "id")
  361. if !ok {
  362. return
  363. }
  364. if err := h.configService.DeletePlan(c.Request.Context(), id); err != nil {
  365. response.ErrorFrom(c, err)
  366. return
  367. }
  368. response.Success(c, gin.H{"message": "deleted"})
  369. }
  370. // --- Provider Instances ---
  371. // ListProviders returns all payment provider instances.
  372. // GET /api/v1/admin/payment/providers
  373. func (h *PaymentHandler) ListProviders(c *gin.Context) {
  374. providers, err := h.configService.ListProviderInstancesWithConfig(c.Request.Context())
  375. if err != nil {
  376. response.ErrorFrom(c, err)
  377. return
  378. }
  379. response.Success(c, providers)
  380. }
  381. // CreateProvider creates a new payment provider instance.
  382. // POST /api/v1/admin/payment/providers
  383. func (h *PaymentHandler) CreateProvider(c *gin.Context) {
  384. var req service.CreateProviderInstanceRequest
  385. if err := c.ShouldBindJSON(&req); err != nil {
  386. response.BadRequest(c, "Invalid request: "+err.Error())
  387. return
  388. }
  389. inst, err := h.configService.CreateProviderInstance(c.Request.Context(), req)
  390. if err != nil {
  391. response.ErrorFrom(c, err)
  392. return
  393. }
  394. h.paymentService.RefreshProviders(c.Request.Context())
  395. response.Created(c, inst)
  396. }
  397. // UpdateProvider updates an existing payment provider instance.
  398. // PUT /api/v1/admin/payment/providers/:id
  399. func (h *PaymentHandler) UpdateProvider(c *gin.Context) {
  400. id, ok := parseIDParam(c, "id")
  401. if !ok {
  402. return
  403. }
  404. var req service.UpdateProviderInstanceRequest
  405. if err := c.ShouldBindJSON(&req); err != nil {
  406. response.BadRequest(c, "Invalid request: "+err.Error())
  407. return
  408. }
  409. inst, err := h.configService.UpdateProviderInstance(c.Request.Context(), id, req)
  410. if err != nil {
  411. response.ErrorFrom(c, err)
  412. return
  413. }
  414. h.paymentService.RefreshProviders(c.Request.Context())
  415. response.Success(c, inst)
  416. }
  417. // DeleteProvider deletes a payment provider instance.
  418. // DELETE /api/v1/admin/payment/providers/:id
  419. func (h *PaymentHandler) DeleteProvider(c *gin.Context) {
  420. id, ok := parseIDParam(c, "id")
  421. if !ok {
  422. return
  423. }
  424. if err := h.configService.DeleteProviderInstance(c.Request.Context(), id); err != nil {
  425. response.ErrorFrom(c, err)
  426. return
  427. }
  428. h.paymentService.RefreshProviders(c.Request.Context())
  429. response.Success(c, gin.H{"message": "deleted"})
  430. }
  431. // parseIDParam parses an int64 path parameter.
  432. // Returns the parsed ID and true on success; on failure it writes a BadRequest response and returns false.
  433. func parseIDParam(c *gin.Context, paramName string) (int64, bool) {
  434. id, err := strconv.ParseInt(c.Param(paramName), 10, 64)
  435. if err != nil {
  436. response.BadRequest(c, "Invalid "+paramName)
  437. return 0, false
  438. }
  439. return id, true
  440. }
  441. // --- Config ---
  442. // GetConfig returns the payment configuration (admin view).
  443. // GET /api/v1/admin/payment/config
  444. func (h *PaymentHandler) GetConfig(c *gin.Context) {
  445. cfg, err := h.configService.GetPaymentConfig(c.Request.Context())
  446. if err != nil {
  447. response.ErrorFrom(c, err)
  448. return
  449. }
  450. response.Success(c, cfg)
  451. }
  452. // UpdateConfig updates the payment configuration.
  453. // PUT /api/v1/admin/payment/config
  454. func (h *PaymentHandler) UpdateConfig(c *gin.Context) {
  455. var req service.UpdatePaymentConfigRequest
  456. if err := c.ShouldBindJSON(&req); err != nil {
  457. response.BadRequest(c, "Invalid request: "+err.Error())
  458. return
  459. }
  460. if err := h.configService.UpdatePaymentConfig(c.Request.Context(), req); err != nil {
  461. response.ErrorFrom(c, err)
  462. return
  463. }
  464. response.Success(c, gin.H{"message": "updated"})
  465. }