payment_webhook_provider.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "log/slog"
  6. "strings"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/ent/paymentorder"
  9. "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
  10. "github.com/Wei-Shaw/sub2api/internal/payment"
  11. )
  12. // GetWebhookProvider returns the provider instance that should verify a webhook.
  13. // It resolves the original provider instance from the order whenever possible and
  14. // only falls back to a registry provider for legacy/single-instance scenarios.
  15. func (s *PaymentService) GetWebhookProvider(ctx context.Context, providerKey, outTradeNo string) (payment.Provider, error) {
  16. providers, err := s.GetWebhookProviders(ctx, providerKey, outTradeNo)
  17. if err != nil {
  18. return nil, err
  19. }
  20. if len(providers) == 0 {
  21. return nil, payment.ErrProviderNotFound
  22. }
  23. return providers[0], nil
  24. }
  25. // GetWebhookProviders returns provider candidates that can verify the webhook.
  26. // Official WeChat Pay may require multiple candidates because the callback body
  27. // cannot be bound to a merchant before decryption.
  28. func (s *PaymentService) GetWebhookProviders(ctx context.Context, providerKey, outTradeNo string) ([]payment.Provider, error) {
  29. if outTradeNo != "" {
  30. order, err := s.entClient.PaymentOrder.Query().Where(paymentorder.OutTradeNo(outTradeNo)).Only(ctx)
  31. if err == nil {
  32. if psHasPinnedProviderInstance(order) {
  33. prov, err := s.getPinnedOrderProvider(ctx, order)
  34. if err != nil {
  35. return nil, err
  36. }
  37. return []payment.Provider{prov}, nil
  38. }
  39. inst, err := s.getOrderProviderInstance(ctx, order)
  40. if err != nil {
  41. return nil, fmt.Errorf("load order provider instance: %w", err)
  42. }
  43. if inst != nil {
  44. prov, err := s.createProviderFromInstance(ctx, inst)
  45. if err != nil {
  46. return nil, err
  47. }
  48. return []payment.Provider{prov}, nil
  49. }
  50. if strings.TrimSpace(providerKey) == payment.TypeWxpay {
  51. return s.getEnabledWebhookProvidersByKey(ctx, providerKey)
  52. }
  53. if !s.webhookRegistryFallbackAllowed(ctx, providerKey) {
  54. return nil, fmt.Errorf("webhook provider fallback is ambiguous for %s", providerKey)
  55. }
  56. s.EnsureProviders(ctx)
  57. prov, err := s.registry.GetProviderByKey(providerKey)
  58. if err != nil {
  59. return nil, err
  60. }
  61. return []payment.Provider{prov}, nil
  62. }
  63. }
  64. if strings.TrimSpace(providerKey) == payment.TypeWxpay {
  65. return s.getEnabledWebhookProvidersByKey(ctx, providerKey)
  66. }
  67. if !s.webhookRegistryFallbackAllowed(ctx, providerKey) {
  68. return nil, fmt.Errorf("webhook provider fallback is ambiguous for %s", providerKey)
  69. }
  70. s.EnsureProviders(ctx)
  71. prov, err := s.registry.GetProviderByKey(providerKey)
  72. if err != nil {
  73. return nil, err
  74. }
  75. return []payment.Provider{prov}, nil
  76. }
  77. func (s *PaymentService) getPinnedOrderProvider(ctx context.Context, o *dbent.PaymentOrder) (payment.Provider, error) {
  78. inst, err := s.getOrderProviderInstance(ctx, o)
  79. if err != nil {
  80. return nil, fmt.Errorf("load order provider instance: %w", err)
  81. }
  82. if inst == nil {
  83. return nil, fmt.Errorf("order %d provider instance is missing", o.ID)
  84. }
  85. return s.createProviderFromInstance(ctx, inst)
  86. }
  87. func (s *PaymentService) webhookRegistryFallbackAllowed(ctx context.Context, providerKey string) bool {
  88. providerKey = strings.TrimSpace(providerKey)
  89. if providerKey == "" || s == nil || s.entClient == nil {
  90. return false
  91. }
  92. count, err := s.entClient.PaymentProviderInstance.Query().
  93. Where(
  94. paymentproviderinstance.ProviderKeyEQ(providerKey),
  95. paymentproviderinstance.EnabledEQ(true),
  96. ).
  97. Count(ctx)
  98. if err != nil {
  99. slog.Warn("payment webhook fallback instance count failed", "provider", providerKey, "error", err)
  100. return false
  101. }
  102. return count <= 1
  103. }
  104. func psHasPinnedProviderInstance(order *dbent.PaymentOrder) bool {
  105. return order != nil && (psOrderProviderSnapshot(order) != nil || (order.ProviderInstanceID != nil && strings.TrimSpace(*order.ProviderInstanceID) != ""))
  106. }
  107. func (s *PaymentService) getEnabledWebhookProvidersByKey(ctx context.Context, providerKey string) ([]payment.Provider, error) {
  108. providerKey = strings.TrimSpace(providerKey)
  109. instances, err := s.entClient.PaymentProviderInstance.Query().
  110. Where(
  111. paymentproviderinstance.ProviderKeyEQ(providerKey),
  112. paymentproviderinstance.EnabledEQ(true),
  113. ).
  114. Order(dbent.Asc(paymentproviderinstance.FieldSortOrder)).
  115. All(ctx)
  116. if err != nil {
  117. return nil, fmt.Errorf("query webhook provider instances: %w", err)
  118. }
  119. if len(instances) == 0 {
  120. return nil, payment.ErrProviderNotFound
  121. }
  122. providers := make([]payment.Provider, 0, len(instances))
  123. for _, inst := range instances {
  124. prov, provErr := s.createProviderFromInstance(ctx, inst)
  125. if provErr != nil {
  126. slog.Warn("skip webhook provider instance", "provider", providerKey, "instanceID", inst.ID, "error", provErr)
  127. continue
  128. }
  129. providers = append(providers, prov)
  130. }
  131. if len(providers) == 0 {
  132. return nil, payment.ErrProviderNotFound
  133. }
  134. return providers, nil
  135. }