payment_visible_method_instances.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
  9. "github.com/Wei-Shaw/sub2api/internal/payment"
  10. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  11. )
  12. func enabledVisibleMethodsForProvider(providerKey, supportedTypes string) []string {
  13. methodSet := make(map[string]struct{}, 2)
  14. addMethod := func(method string) {
  15. method = NormalizeVisibleMethod(method)
  16. if method != "" {
  17. methodSet[method] = struct{}{}
  18. }
  19. }
  20. switch strings.TrimSpace(providerKey) {
  21. case payment.TypeAlipay:
  22. if strings.TrimSpace(supportedTypes) == "" {
  23. addMethod(payment.TypeAlipay)
  24. break
  25. }
  26. for _, supportedType := range splitTypes(supportedTypes) {
  27. if NormalizeVisibleMethod(supportedType) == payment.TypeAlipay {
  28. addMethod(payment.TypeAlipay)
  29. break
  30. }
  31. }
  32. case payment.TypeWxpay:
  33. if strings.TrimSpace(supportedTypes) == "" {
  34. addMethod(payment.TypeWxpay)
  35. break
  36. }
  37. for _, supportedType := range splitTypes(supportedTypes) {
  38. if NormalizeVisibleMethod(supportedType) == payment.TypeWxpay {
  39. addMethod(payment.TypeWxpay)
  40. break
  41. }
  42. }
  43. case payment.TypeEasyPay:
  44. for _, supportedType := range splitTypes(supportedTypes) {
  45. addMethod(supportedType)
  46. }
  47. }
  48. methods := make([]string, 0, len(methodSet))
  49. for _, method := range []string{payment.TypeAlipay, payment.TypeWxpay} {
  50. if _, ok := methodSet[method]; ok {
  51. methods = append(methods, method)
  52. delete(methodSet, method)
  53. }
  54. }
  55. for _, supportedType := range splitTypes(supportedTypes) {
  56. method := NormalizeVisibleMethod(supportedType)
  57. if _, ok := methodSet[method]; ok {
  58. methods = append(methods, method)
  59. delete(methodSet, method)
  60. }
  61. }
  62. return methods
  63. }
  64. func providerSupportsVisibleMethod(inst *dbent.PaymentProviderInstance, method string) bool {
  65. if inst == nil || !inst.Enabled {
  66. return false
  67. }
  68. method = NormalizeVisibleMethod(method)
  69. for _, candidate := range enabledVisibleMethodsForProvider(inst.ProviderKey, inst.SupportedTypes) {
  70. if candidate == method {
  71. return true
  72. }
  73. }
  74. return false
  75. }
  76. func filterEnabledVisibleMethodInstances(instances []*dbent.PaymentProviderInstance, method string) []*dbent.PaymentProviderInstance {
  77. filtered := make([]*dbent.PaymentProviderInstance, 0, len(instances))
  78. for _, inst := range instances {
  79. if providerSupportsVisibleMethod(inst, method) {
  80. filtered = append(filtered, inst)
  81. }
  82. }
  83. return filtered
  84. }
  85. func filterVisibleMethodInstancesByProviderKey(instances []*dbent.PaymentProviderInstance, method string, providerKey string) []*dbent.PaymentProviderInstance {
  86. filtered := make([]*dbent.PaymentProviderInstance, 0, len(instances))
  87. for _, inst := range instances {
  88. if !providerSupportsVisibleMethod(inst, method) {
  89. continue
  90. }
  91. if !strings.EqualFold(strings.TrimSpace(inst.ProviderKey), strings.TrimSpace(providerKey)) {
  92. continue
  93. }
  94. filtered = append(filtered, inst)
  95. }
  96. return filtered
  97. }
  98. func distinctVisibleMethodProviderKeys(instances []*dbent.PaymentProviderInstance) []string {
  99. seen := make(map[string]struct{}, len(instances))
  100. keys := make([]string, 0, len(instances))
  101. for _, inst := range instances {
  102. if inst == nil {
  103. continue
  104. }
  105. key := strings.TrimSpace(inst.ProviderKey)
  106. if key == "" {
  107. continue
  108. }
  109. normalized := strings.ToLower(key)
  110. if _, ok := seen[normalized]; ok {
  111. continue
  112. }
  113. seen[normalized] = struct{}{}
  114. keys = append(keys, key)
  115. }
  116. return keys
  117. }
  118. func selectVisibleMethodInstanceByProviderKey(instances []*dbent.PaymentProviderInstance, providerKey string) *dbent.PaymentProviderInstance {
  119. providerKey = strings.TrimSpace(providerKey)
  120. if providerKey == "" {
  121. return nil
  122. }
  123. for _, inst := range instances {
  124. if strings.EqualFold(strings.TrimSpace(inst.ProviderKey), providerKey) {
  125. return inst
  126. }
  127. }
  128. return nil
  129. }
  130. func (s *PaymentConfigService) validateVisibleMethodEnablementConflicts(
  131. ctx context.Context,
  132. excludeID int64,
  133. providerKey string,
  134. supportedTypes string,
  135. enabled bool,
  136. ) error {
  137. // Visible methods are selected by configured source (official/easypay),
  138. // so multiple enabled providers can intentionally claim the same user-facing
  139. // method. Order creation and limits will route through the configured source.
  140. _, _, _, _, _ = ctx, excludeID, providerKey, supportedTypes, enabled
  141. return nil
  142. }
  143. func (s *PaymentConfigService) resolveVisibleMethodSourceProviderKey(ctx context.Context, method string) (string, error) {
  144. method = NormalizeVisibleMethod(method)
  145. sourceKey := visibleMethodSourceSettingKey(method)
  146. rawSource := ""
  147. if s != nil && s.settingRepo != nil && sourceKey != "" {
  148. value, err := s.settingRepo.GetValue(ctx, sourceKey)
  149. if err != nil {
  150. if !errors.Is(err, ErrSettingNotFound) {
  151. return "", fmt.Errorf("get %s: %w", sourceKey, err)
  152. }
  153. } else {
  154. rawSource = value
  155. }
  156. }
  157. normalizedSource, err := normalizeVisibleMethodSettingSource(method, rawSource, true)
  158. if err != nil {
  159. return "", err
  160. }
  161. if normalizedSource == "" {
  162. return "", nil
  163. }
  164. providerKey, ok := VisibleMethodProviderKeyForSource(method, normalizedSource)
  165. if !ok {
  166. return "", infraerrors.BadRequest(
  167. "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
  168. fmt.Sprintf("%s source must be one of the supported payment providers", method),
  169. )
  170. }
  171. return providerKey, nil
  172. }
  173. func (s *PaymentConfigService) resolveVisibleMethodProviderKey(
  174. ctx context.Context,
  175. method string,
  176. matching []*dbent.PaymentProviderInstance,
  177. ) (string, error) {
  178. switch providerKeys := distinctVisibleMethodProviderKeys(matching); len(providerKeys) {
  179. case 0:
  180. return "", nil
  181. case 1:
  182. return strings.TrimSpace(providerKeys[0]), nil
  183. default:
  184. providerKey, err := s.resolveVisibleMethodSourceProviderKey(ctx, method)
  185. if err != nil {
  186. return "", err
  187. }
  188. if providerKey == "" {
  189. return "", nil
  190. }
  191. selected := selectVisibleMethodInstanceByProviderKey(matching, providerKey)
  192. if selected == nil {
  193. return "", infraerrors.BadRequest(
  194. "INVALID_PAYMENT_VISIBLE_METHOD_SOURCE",
  195. fmt.Sprintf("%s source has no enabled provider instance", method),
  196. )
  197. }
  198. return strings.TrimSpace(selected.ProviderKey), nil
  199. }
  200. }
  201. func (s *PaymentConfigService) resolveEnabledVisibleMethodInstance(
  202. ctx context.Context,
  203. method string,
  204. ) (*dbent.PaymentProviderInstance, error) {
  205. if s == nil || s.entClient == nil {
  206. return nil, nil
  207. }
  208. method = NormalizeVisibleMethod(method)
  209. if method == "" {
  210. return nil, nil
  211. }
  212. instances, err := s.entClient.PaymentProviderInstance.Query().
  213. Where(paymentproviderinstance.EnabledEQ(true)).
  214. Order(paymentproviderinstance.BySortOrder()).
  215. All(ctx)
  216. if err != nil {
  217. return nil, fmt.Errorf("query enabled payment providers: %w", err)
  218. }
  219. matching := filterEnabledVisibleMethodInstances(instances, method)
  220. providerKey, err := s.resolveVisibleMethodProviderKey(ctx, method, matching)
  221. if err != nil {
  222. return nil, err
  223. }
  224. if providerKey == "" {
  225. if len(matching) == 0 {
  226. return nil, nil
  227. }
  228. return &dbent.PaymentProviderInstance{ProviderKey: ""}, nil
  229. }
  230. return selectVisibleMethodInstanceByProviderKey(matching, providerKey), nil
  231. }
  232. // UsesOfficialAlipayVisibleMethod reports whether the user-facing Alipay method
  233. // currently resolves to an enabled official Alipay provider instance.
  234. func (s *PaymentConfigService) UsesOfficialAlipayVisibleMethod(ctx context.Context) (bool, error) {
  235. instance, err := s.resolveEnabledVisibleMethodInstance(ctx, payment.TypeAlipay)
  236. if err != nil {
  237. return false, err
  238. }
  239. return isOfficialAlipayProviderInstance(instance), nil
  240. }
  241. func isOfficialAlipayProviderInstance(instance *dbent.PaymentProviderInstance) bool {
  242. return instance != nil && strings.EqualFold(strings.TrimSpace(instance.ProviderKey), payment.TypeAlipay)
  243. }