payment_order_provider_snapshot.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. dbent "github.com/Wei-Shaw/sub2api/ent"
  8. "github.com/Wei-Shaw/sub2api/internal/payment"
  9. )
  10. type paymentOrderProviderSnapshot struct {
  11. SchemaVersion int
  12. ProviderInstanceID string
  13. ProviderKey string
  14. PaymentMode string
  15. MerchantAppID string
  16. MerchantID string
  17. Currency string
  18. }
  19. func psOrderProviderSnapshot(order *dbent.PaymentOrder) *paymentOrderProviderSnapshot {
  20. if order == nil || len(order.ProviderSnapshot) == 0 {
  21. return nil
  22. }
  23. snapshot := &paymentOrderProviderSnapshot{
  24. SchemaVersion: psSnapshotIntValue(order.ProviderSnapshot["schema_version"]),
  25. ProviderInstanceID: psSnapshotStringValue(order.ProviderSnapshot["provider_instance_id"]),
  26. ProviderKey: psSnapshotStringValue(order.ProviderSnapshot["provider_key"]),
  27. PaymentMode: psSnapshotStringValue(order.ProviderSnapshot["payment_mode"]),
  28. MerchantAppID: psSnapshotStringValue(order.ProviderSnapshot["merchant_app_id"]),
  29. MerchantID: psSnapshotStringValue(order.ProviderSnapshot["merchant_id"]),
  30. Currency: psSnapshotStringValue(order.ProviderSnapshot["currency"]),
  31. }
  32. if snapshot.SchemaVersion == 0 &&
  33. snapshot.ProviderInstanceID == "" &&
  34. snapshot.ProviderKey == "" &&
  35. snapshot.PaymentMode == "" &&
  36. snapshot.MerchantAppID == "" &&
  37. snapshot.MerchantID == "" &&
  38. snapshot.Currency == "" {
  39. return nil
  40. }
  41. return snapshot
  42. }
  43. func psSnapshotStringValue(value any) string {
  44. switch typed := value.(type) {
  45. case string:
  46. return strings.TrimSpace(typed)
  47. default:
  48. return ""
  49. }
  50. }
  51. func psSnapshotIntValue(value any) int {
  52. switch typed := value.(type) {
  53. case int:
  54. return typed
  55. case int32:
  56. return int(typed)
  57. case int64:
  58. return int(typed)
  59. case float32:
  60. return int(typed)
  61. case float64:
  62. return int(typed)
  63. case string:
  64. n, err := strconv.Atoi(strings.TrimSpace(typed))
  65. if err == nil {
  66. return n
  67. }
  68. }
  69. return 0
  70. }
  71. func (s *PaymentService) resolveSnapshotOrderProviderInstance(ctx context.Context, order *dbent.PaymentOrder, snapshot *paymentOrderProviderSnapshot) (*dbent.PaymentProviderInstance, error) {
  72. if s == nil || s.entClient == nil || order == nil || snapshot == nil {
  73. return nil, nil
  74. }
  75. snapshotInstanceID := strings.TrimSpace(snapshot.ProviderInstanceID)
  76. columnInstanceID := strings.TrimSpace(psStringValue(order.ProviderInstanceID))
  77. if snapshotInstanceID == "" {
  78. snapshotInstanceID = columnInstanceID
  79. }
  80. if snapshotInstanceID == "" {
  81. return nil, fmt.Errorf("order %d provider snapshot is missing provider_instance_id", order.ID)
  82. }
  83. if columnInstanceID != "" && snapshot.ProviderInstanceID != "" && !strings.EqualFold(columnInstanceID, snapshot.ProviderInstanceID) {
  84. return nil, fmt.Errorf("order %d provider snapshot instance mismatch: snapshot=%s order=%s", order.ID, snapshot.ProviderInstanceID, columnInstanceID)
  85. }
  86. instID, err := strconv.ParseInt(snapshotInstanceID, 10, 64)
  87. if err != nil {
  88. return nil, fmt.Errorf("order %d provider snapshot instance id is invalid: %s", order.ID, snapshotInstanceID)
  89. }
  90. inst, err := s.entClient.PaymentProviderInstance.Get(ctx, instID)
  91. if err != nil {
  92. if dbent.IsNotFound(err) {
  93. return nil, fmt.Errorf("order %d provider snapshot instance %s is missing", order.ID, snapshotInstanceID)
  94. }
  95. return nil, err
  96. }
  97. if snapshot.ProviderKey != "" && !strings.EqualFold(strings.TrimSpace(inst.ProviderKey), snapshot.ProviderKey) {
  98. return nil, fmt.Errorf("order %d provider snapshot key mismatch: snapshot=%s instance=%s", order.ID, snapshot.ProviderKey, inst.ProviderKey)
  99. }
  100. return inst, nil
  101. }
  102. func expectedNotificationProviderKeyForOrder(registry *payment.Registry, order *dbent.PaymentOrder, instanceProviderKey string) string {
  103. if order == nil {
  104. return strings.TrimSpace(instanceProviderKey)
  105. }
  106. orderProviderKey := psStringValue(order.ProviderKey)
  107. if snapshot := psOrderProviderSnapshot(order); snapshot != nil && snapshot.ProviderKey != "" {
  108. orderProviderKey = snapshot.ProviderKey
  109. }
  110. return expectedNotificationProviderKey(registry, order.PaymentType, orderProviderKey, instanceProviderKey)
  111. }
  112. func validateProviderSnapshotMetadata(order *dbent.PaymentOrder, providerKey string, metadata map[string]string) error {
  113. if order == nil || len(metadata) == 0 {
  114. return nil
  115. }
  116. snapshot := psOrderProviderSnapshot(order)
  117. if snapshot == nil {
  118. return nil
  119. }
  120. switch strings.TrimSpace(providerKey) {
  121. case payment.TypeWxpay:
  122. if expected := strings.TrimSpace(snapshot.MerchantAppID); expected != "" {
  123. actual := strings.TrimSpace(metadata["appid"])
  124. if actual == "" {
  125. return fmt.Errorf("wxpay notification missing appid")
  126. }
  127. if !strings.EqualFold(expected, actual) {
  128. return fmt.Errorf("wxpay appid mismatch: expected %s, got %s", expected, actual)
  129. }
  130. }
  131. if expected := strings.TrimSpace(snapshot.MerchantID); expected != "" {
  132. actual := strings.TrimSpace(metadata["mchid"])
  133. if actual == "" {
  134. return fmt.Errorf("wxpay notification missing mchid")
  135. }
  136. if !strings.EqualFold(expected, actual) {
  137. return fmt.Errorf("wxpay mchid mismatch: expected %s, got %s", expected, actual)
  138. }
  139. }
  140. if expected := strings.TrimSpace(snapshot.Currency); expected != "" {
  141. actual := strings.ToUpper(strings.TrimSpace(metadata["currency"]))
  142. if actual == "" {
  143. return fmt.Errorf("wxpay notification missing currency")
  144. }
  145. if !strings.EqualFold(expected, actual) {
  146. return fmt.Errorf("wxpay currency mismatch: expected %s, got %s", expected, actual)
  147. }
  148. }
  149. if actual := strings.TrimSpace(metadata["trade_state"]); actual != "" && !strings.EqualFold(actual, "SUCCESS") {
  150. return fmt.Errorf("wxpay trade_state mismatch: expected SUCCESS, got %s", actual)
  151. }
  152. case payment.TypeAlipay:
  153. if expected := strings.TrimSpace(snapshot.MerchantAppID); expected != "" {
  154. actual := strings.TrimSpace(metadata["app_id"])
  155. if actual == "" {
  156. return fmt.Errorf("alipay app_id missing")
  157. }
  158. if !strings.EqualFold(expected, actual) {
  159. return fmt.Errorf("alipay app_id mismatch: expected %s, got %s", expected, actual)
  160. }
  161. }
  162. case payment.TypeEasyPay:
  163. if expected := strings.TrimSpace(snapshot.MerchantID); expected != "" {
  164. actual := strings.TrimSpace(metadata["pid"])
  165. if actual == "" {
  166. return fmt.Errorf("easypay pid missing")
  167. }
  168. if !strings.EqualFold(expected, actual) {
  169. return fmt.Errorf("easypay pid mismatch: expected %s, got %s", expected, actual)
  170. }
  171. }
  172. case payment.TypeStripe:
  173. if expected := strings.TrimSpace(snapshot.Currency); expected != "" {
  174. actual := strings.ToUpper(strings.TrimSpace(metadata["currency"]))
  175. if actual == "" {
  176. return fmt.Errorf("stripe notification missing currency")
  177. }
  178. if !strings.EqualFold(expected, actual) {
  179. return fmt.Errorf("stripe currency mismatch: expected %s, got %s", expected, actual)
  180. }
  181. }
  182. case payment.TypeAirwallex:
  183. if expected := strings.TrimSpace(snapshot.MerchantID); expected != "" {
  184. actual := strings.TrimSpace(metadata["account_id"])
  185. if actual == "" {
  186. return fmt.Errorf("airwallex account_id missing")
  187. }
  188. if !strings.EqualFold(expected, actual) {
  189. return fmt.Errorf("airwallex account_id mismatch: expected %s, got %s", expected, actual)
  190. }
  191. }
  192. if expected := strings.TrimSpace(snapshot.Currency); expected != "" {
  193. actual := strings.ToUpper(strings.TrimSpace(metadata["currency"]))
  194. if actual == "" {
  195. return fmt.Errorf("airwallex notification missing currency")
  196. }
  197. if !strings.EqualFold(expected, actual) {
  198. return fmt.Errorf("airwallex currency mismatch: expected %s, got %s", expected, actual)
  199. }
  200. }
  201. if actual := strings.TrimSpace(metadata["status"]); actual != "" && !strings.EqualFold(actual, "SUCCEEDED") {
  202. return fmt.Errorf("airwallex status mismatch: expected SUCCEEDED, got %s", actual)
  203. }
  204. }
  205. return nil
  206. }
  207. func providerMerchantIdentityMetadata(prov payment.Provider) map[string]string {
  208. if prov == nil {
  209. return nil
  210. }
  211. reporter, ok := prov.(payment.MerchantIdentityProvider)
  212. if !ok {
  213. return nil
  214. }
  215. return reporter.MerchantIdentityMetadata()
  216. }