stripe.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. package provider
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "strings"
  7. "sync"
  8. "github.com/Wei-Shaw/sub2api/internal/payment"
  9. stripe "github.com/stripe/stripe-go/v85"
  10. "github.com/stripe/stripe-go/v85/webhook"
  11. )
  12. // Stripe constants.
  13. const (
  14. stripeEventPaymentSuccess = "payment_intent.succeeded"
  15. stripeEventPaymentFailed = "payment_intent.payment_failed"
  16. )
  17. // Stripe implements the payment.CancelableProvider interface for Stripe payments.
  18. type Stripe struct {
  19. instanceID string
  20. config map[string]string
  21. mu sync.Mutex
  22. initialized bool
  23. sc *stripe.Client
  24. }
  25. // NewStripe creates a new Stripe provider instance.
  26. func NewStripe(instanceID string, config map[string]string) (*Stripe, error) {
  27. if config["secretKey"] == "" {
  28. return nil, fmt.Errorf("stripe config missing required key: secretKey")
  29. }
  30. cfg := cloneStringMap(config)
  31. currency, err := payment.NormalizePaymentCurrency(cfg["currency"])
  32. if err != nil {
  33. return nil, fmt.Errorf("stripe config currency: %w", err)
  34. }
  35. cfg["currency"] = currency
  36. return &Stripe{
  37. instanceID: instanceID,
  38. config: cfg,
  39. }, nil
  40. }
  41. func (s *Stripe) ensureInit() {
  42. s.mu.Lock()
  43. defer s.mu.Unlock()
  44. if !s.initialized {
  45. s.sc = stripe.NewClient(s.config["secretKey"])
  46. s.initialized = true
  47. }
  48. }
  49. // GetPublishableKey returns the publishable key for frontend use.
  50. func (s *Stripe) GetPublishableKey() string {
  51. return s.config["publishableKey"]
  52. }
  53. func (s *Stripe) Name() string { return "Stripe" }
  54. func (s *Stripe) ProviderKey() string { return payment.TypeStripe }
  55. func (s *Stripe) SupportedTypes() []payment.PaymentType {
  56. return []payment.PaymentType{payment.TypeStripe}
  57. }
  58. func (s *Stripe) MerchantIdentityMetadata() map[string]string {
  59. if s == nil {
  60. return nil
  61. }
  62. return map[string]string{"currency": s.currency()}
  63. }
  64. func (s *Stripe) currency() string {
  65. if s == nil {
  66. return payment.DefaultPaymentCurrency
  67. }
  68. currency, err := payment.NormalizePaymentCurrency(s.config["currency"])
  69. if err != nil {
  70. return payment.DefaultPaymentCurrency
  71. }
  72. return currency
  73. }
  74. // stripePaymentMethodTypes maps our PaymentType to Stripe payment_method_types.
  75. var stripePaymentMethodTypes = map[string][]string{
  76. payment.TypeCard: {"card"},
  77. payment.TypeAlipay: {"alipay"},
  78. payment.TypeWxpay: {"wechat_pay"},
  79. payment.TypeLink: {"link"},
  80. }
  81. // CreatePayment creates a Stripe PaymentIntent.
  82. func (s *Stripe) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
  83. s.ensureInit()
  84. currency := s.currency()
  85. amountInMinorUnit, err := payment.AmountToMinorUnit(req.Amount, currency)
  86. if err != nil {
  87. return nil, fmt.Errorf("stripe create payment: %w", err)
  88. }
  89. // Collect all Stripe payment_method_types from the instance's configured sub-methods
  90. methods := resolveStripeMethodTypes(req.InstanceSubMethods)
  91. pmTypes := make([]*string, len(methods))
  92. for i, m := range methods {
  93. pmTypes[i] = stripe.String(m)
  94. }
  95. params := &stripe.PaymentIntentCreateParams{
  96. Amount: stripe.Int64(amountInMinorUnit),
  97. Currency: stripe.String(strings.ToLower(currency)),
  98. PaymentMethodTypes: pmTypes,
  99. Description: stripe.String(req.Subject),
  100. Metadata: map[string]string{"orderId": req.OrderID},
  101. }
  102. // WeChat Pay requires payment_method_options with client type
  103. if hasStripeMethod(methods, "wechat_pay") {
  104. params.PaymentMethodOptions = &stripe.PaymentIntentCreatePaymentMethodOptionsParams{
  105. WeChatPay: &stripe.PaymentIntentCreatePaymentMethodOptionsWeChatPayParams{
  106. Client: stripe.String("web"),
  107. },
  108. }
  109. }
  110. params.SetIdempotencyKey(fmt.Sprintf("pi-%s", req.OrderID))
  111. params.Context = ctx
  112. pi, err := s.sc.V1PaymentIntents.Create(ctx, params)
  113. if err != nil {
  114. return nil, fmt.Errorf("stripe create payment: %w", err)
  115. }
  116. return &payment.CreatePaymentResponse{
  117. TradeNo: pi.ID,
  118. ClientSecret: pi.ClientSecret,
  119. Currency: currency,
  120. }, nil
  121. }
  122. // QueryOrder retrieves a PaymentIntent by ID.
  123. func (s *Stripe) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
  124. s.ensureInit()
  125. pi, err := s.sc.V1PaymentIntents.Retrieve(ctx, tradeNo, nil)
  126. if err != nil {
  127. return nil, fmt.Errorf("stripe query order: %w", err)
  128. }
  129. status := payment.ProviderStatusPending
  130. switch pi.Status {
  131. case stripe.PaymentIntentStatusSucceeded:
  132. status = payment.ProviderStatusPaid
  133. case stripe.PaymentIntentStatusCanceled:
  134. status = payment.ProviderStatusFailed
  135. }
  136. currency := stripeIntentCurrency(pi.Currency, s.currency())
  137. return &payment.QueryOrderResponse{
  138. TradeNo: pi.ID,
  139. Status: status,
  140. Amount: payment.MinorUnitToAmount(pi.Amount, currency),
  141. Metadata: map[string]string{
  142. "currency": currency,
  143. },
  144. }, nil
  145. }
  146. // VerifyNotification verifies a Stripe webhook event.
  147. func (s *Stripe) VerifyNotification(_ context.Context, rawBody string, headers map[string]string) (*payment.PaymentNotification, error) {
  148. s.ensureInit()
  149. webhookSecret := s.config["webhookSecret"]
  150. if webhookSecret == "" {
  151. return nil, fmt.Errorf("stripe webhookSecret not configured")
  152. }
  153. sig := headers["stripe-signature"]
  154. if sig == "" {
  155. return nil, fmt.Errorf("stripe notification missing stripe-signature header")
  156. }
  157. event, err := webhook.ConstructEvent([]byte(rawBody), sig, webhookSecret)
  158. if err != nil {
  159. return nil, fmt.Errorf("stripe verify notification: %w", err)
  160. }
  161. switch event.Type {
  162. case stripeEventPaymentSuccess:
  163. return parseStripePaymentIntent(&event, payment.ProviderStatusSuccess, rawBody)
  164. case stripeEventPaymentFailed:
  165. return parseStripePaymentIntent(&event, payment.ProviderStatusFailed, rawBody)
  166. }
  167. return nil, nil
  168. }
  169. func parseStripePaymentIntent(event *stripe.Event, status string, rawBody string) (*payment.PaymentNotification, error) {
  170. var pi stripe.PaymentIntent
  171. if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
  172. return nil, fmt.Errorf("stripe parse payment_intent: %w", err)
  173. }
  174. currency := stripeIntentCurrency(pi.Currency, payment.DefaultPaymentCurrency)
  175. return &payment.PaymentNotification{
  176. TradeNo: pi.ID,
  177. OrderID: pi.Metadata["orderId"],
  178. Amount: payment.MinorUnitToAmount(pi.Amount, currency),
  179. Status: status,
  180. RawData: rawBody,
  181. Metadata: map[string]string{
  182. "currency": currency,
  183. },
  184. }, nil
  185. }
  186. // Refund creates a Stripe refund.
  187. func (s *Stripe) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
  188. s.ensureInit()
  189. amountInMinorUnit, err := payment.AmountToMinorUnit(req.Amount, s.currency())
  190. if err != nil {
  191. return nil, fmt.Errorf("stripe refund: %w", err)
  192. }
  193. params := &stripe.RefundCreateParams{
  194. PaymentIntent: stripe.String(req.TradeNo),
  195. Amount: stripe.Int64(amountInMinorUnit),
  196. Reason: stripe.String(string(stripe.RefundReasonRequestedByCustomer)),
  197. }
  198. params.SetIdempotencyKey(fmt.Sprintf("re-%s-%d", req.OrderID, amountInMinorUnit))
  199. params.Context = ctx
  200. r, err := s.sc.V1Refunds.Create(ctx, params)
  201. if err != nil {
  202. return nil, fmt.Errorf("stripe refund: %w", err)
  203. }
  204. refundStatus := payment.ProviderStatusPending
  205. if r.Status == stripe.RefundStatusSucceeded {
  206. refundStatus = payment.ProviderStatusSuccess
  207. }
  208. return &payment.RefundResponse{
  209. RefundID: r.ID,
  210. Status: refundStatus,
  211. }, nil
  212. }
  213. // QueryRefund retrieves a Stripe refund by refund ID when available, otherwise
  214. // falls back to the latest refund for the PaymentIntent.
  215. func (s *Stripe) QueryRefund(ctx context.Context, req payment.RefundQueryRequest) (*payment.RefundResponse, error) {
  216. s.ensureInit()
  217. var r *stripe.Refund
  218. var err error
  219. if refundID := strings.TrimSpace(req.RefundID); refundID != "" {
  220. r, err = s.sc.V1Refunds.Retrieve(ctx, refundID, nil)
  221. if err != nil {
  222. return nil, fmt.Errorf("stripe query refund: %w", err)
  223. }
  224. } else {
  225. tradeNo := strings.TrimSpace(req.TradeNo)
  226. if tradeNo == "" {
  227. return nil, fmt.Errorf("stripe query refund: missing payment intent id")
  228. }
  229. params := &stripe.RefundListParams{PaymentIntent: stripe.String(tradeNo)}
  230. params.Limit = stripe.Int64(1)
  231. list := s.sc.V1Refunds.List(ctx, params)
  232. if list.Err() != nil {
  233. return nil, fmt.Errorf("stripe query refund: %w", list.Err())
  234. }
  235. refunds := list.Data()
  236. if len(refunds) == 0 {
  237. return nil, fmt.Errorf("stripe query refund: no refund found")
  238. }
  239. r = refunds[0]
  240. }
  241. return &payment.RefundResponse{RefundID: r.ID, Status: stripeRefundProviderStatus(r.Status)}, nil
  242. }
  243. func stripeRefundProviderStatus(status stripe.RefundStatus) string {
  244. switch status {
  245. case stripe.RefundStatusSucceeded:
  246. return payment.ProviderStatusSuccess
  247. case stripe.RefundStatusFailed, stripe.RefundStatusCanceled:
  248. return payment.ProviderStatusFailed
  249. default:
  250. return payment.ProviderStatusPending
  251. }
  252. }
  253. func stripeIntentCurrency(raw stripe.Currency, fallback string) string {
  254. currency, err := payment.NormalizePaymentCurrency(string(raw))
  255. if err != nil || currency == payment.DefaultPaymentCurrency && strings.TrimSpace(string(raw)) == "" {
  256. normalizedFallback, fallbackErr := payment.NormalizePaymentCurrency(fallback)
  257. if fallbackErr == nil {
  258. return normalizedFallback
  259. }
  260. return payment.DefaultPaymentCurrency
  261. }
  262. return currency
  263. }
  264. // resolveStripeMethodTypes converts instance supported_types (comma-separated)
  265. // into Stripe API payment_method_types. Falls back to ["card"] if empty.
  266. func resolveStripeMethodTypes(instanceSubMethods string) []string {
  267. if instanceSubMethods == "" {
  268. return []string{"card"}
  269. }
  270. var methods []string
  271. for _, t := range strings.Split(instanceSubMethods, ",") {
  272. t = strings.TrimSpace(t)
  273. if mapped, ok := stripePaymentMethodTypes[t]; ok {
  274. methods = append(methods, mapped...)
  275. }
  276. }
  277. if len(methods) == 0 {
  278. return []string{"card"}
  279. }
  280. return methods
  281. }
  282. // hasStripeMethod checks if the given Stripe method list contains the target method.
  283. func hasStripeMethod(methods []string, target string) bool {
  284. for _, m := range methods {
  285. if m == target {
  286. return true
  287. }
  288. }
  289. return false
  290. }
  291. // CancelPayment cancels a pending PaymentIntent.
  292. func (s *Stripe) CancelPayment(ctx context.Context, tradeNo string) error {
  293. s.ensureInit()
  294. _, err := s.sc.V1PaymentIntents.Cancel(ctx, tradeNo, nil)
  295. if err != nil {
  296. return fmt.Errorf("stripe cancel payment: %w", err)
  297. }
  298. return nil
  299. }
  300. // Ensure interface compliance.
  301. var (
  302. _ payment.Provider = (*Stripe)(nil)
  303. _ payment.CancelableProvider = (*Stripe)(nil)
  304. _ payment.MerchantIdentityProvider = (*Stripe)(nil)
  305. )