Class: Payment::Apis::Stripe
- Inherits:
-
BaseService
- Object
- BaseService
- Payment::Apis::Stripe
- Includes:
- Singleton
- Defined in:
- app/services/payment/apis/stripe.rb
Overview
Thin wrapper around the Stripe Ruby SDK that routes USD requests to
the WY-US Stripe account and CAD/CA requests to the WY-CA Stripe
account. Used by Gateways::CreditCard for every Stripe
call (PaymentIntent, Charge, Refund, Customer, PaymentMethod,
SetupIntent).
Constant Summary collapse
- FLEXIBLE_CARD_FEATURES =
{ usa: { multicapture: true, incremental_authorization: true, extended_authorization: true, overcapture: false }.freeze, can: { multicapture: true, incremental_authorization: true, extended_authorization: true, overcapture: false }.freeze }.freeze
Class Method Summary collapse
-
.api_key(currency_or_country) ⇒ String
Stripe secret API key for the account that handles
currency_or_country. -
.attach_payment_method(payment_method_id, customer_id:, currency:) ⇒ Stripe::PaymentMethod
Attach a PaymentMethod to a Stripe customer (used during vault creation in Gateways::CreditCard#store).
-
.cancel_payment_intent(payment_intent_id, currency:, reason: nil) ⇒ Stripe::PaymentIntent, Stripe::Refund
Cancel an authorized but un-captured PaymentIntent.
-
.capture_charge(charge_id, currency:, amount_cents: nil) ⇒ Stripe::Charge
Legacy ActiveMerchant
ch_xxxcharge capture path used when a PaymentIntent doesn't exist yet. -
.capture_payment_intent(payment_intent_id, currency:, amount_cents: nil, final_capture: true) ⇒ Stripe::PaymentIntent, Stripe::Charge
Capture an authorized PaymentIntent.
- .charge?(id) ⇒ Boolean
-
.confirm_payment_intent(payment_intent_id, currency:, payment_method: nil, return_url: nil) ⇒ Stripe::PaymentIntent
Confirm a PaymentIntent (typically after the customer authorizes it via 3DS).
-
.create_customer(currency:, email: nil, name: nil, metadata: {}) ⇒ Object
======================================== Customer Operations ========================================.
-
.create_payment_intent(amount_cents:, currency:, customer_id: nil, payment_method: nil, capture_method: 'manual', confirm: false, off_session: false, setup_future_usage: nil, metadata: {}, description: nil, statement_descriptor: nil, shipping: nil, return_url: nil, payment_method_types: nil, allow_redirects: 'never', payment_method_options: nil) ⇒ Object
======================================== PaymentIntent Operations ========================================.
- .create_refund(payment_intent_id:, currency:, amount_cents: nil, reason: nil) ⇒ Object
-
.create_setup_intent(currency:, customer_id: nil, payment_method_types: ['card'], usage: 'off_session', metadata: {}) ⇒ Object
======================================== SetupIntent Operations ========================================.
-
.detach_payment_method(payment_method_id, currency:) ⇒ Stripe::PaymentMethod
Detach a PaymentMethod from its customer; called when a vault is discarded.
-
.each_payment_intent(query, currency:, limit: 100) {|pi| ... } ⇒ Enumerator<Stripe::PaymentIntent>
Iterate every PaymentIntent matching
queryacross all pages. -
.increment_authorization(payment_intent_id, amount_cents:, currency:, description: nil) ⇒ Stripe::PaymentIntent
Step a PaymentIntent's authorized amount up.
-
.list_payment_methods(customer_id, currency:, type: 'card') ⇒ Stripe::ListObject
List PaymentMethods on a customer (defaults to cards).
-
.list_refunds(currency:, created_gte: nil, limit: 100, &block) ⇒ Enumerator<Stripe::Refund>
List refunds on the Stripe account that handles
currency. -
.payment_intent?(id) ⇒ Boolean
======================================== Helpers ========================================.
-
.publishable_key(currency_or_country) ⇒ String
Stripe publishable key for the matching account; used in the storefront when initializing Stripe.js.
-
.refund_charge(charge_id, currency:, amount_cents: nil, reason: nil) ⇒ Stripe::Refund
Legacy
ch_xxxrefund path; created via the Refunds API for backward compatibility with pre-PI payments. -
.retrieve_charge(charge_id, currency:) ⇒ Object
======================================== Charge Operations (backward compat for old ch_xxx payments) ========================================.
- .retrieve_customer(customer_id, currency:) ⇒ Stripe::Customer
-
.retrieve_payment_intent(payment_intent_id, currency:, expand: %w[latest_charge latest_charge.payment_method_details])) ⇒ Stripe::PaymentIntent
Retrieve a PaymentIntent.
-
.retrieve_payment_method(payment_method_id, currency:) ⇒ Object
======================================== PaymentMethod Operations ========================================.
-
.retrieve_payment_object(id, currency:) ⇒ Stripe::PaymentIntent, ...
Retrieve whichever upstream object a
pi_xxxorch_xxxauthorization_codecorresponds to. - .retrieve_setup_intent(setup_intent_id, currency:) ⇒ Stripe::SetupIntent
-
.search_payment_intents(query, currency:, limit: 10) ⇒ Array<Stripe::PaymentIntent>
Search PaymentIntents on the Stripe account that handles
currency.
Class Method Details
.api_key(currency_or_country) ⇒ String
Stripe secret API key for the account that handles
currency_or_country. USD/USA → WY-US, CAD/CAN → WY-CA.
23 24 25 26 |
# File 'app/services/payment/apis/stripe.rb', line 23 def self.api_key(currency_or_country) region = region_for(currency_or_country) Heatwave::Configuration.fetch(:stripe, region, :login) end |
.attach_payment_method(payment_method_id, customer_id:, currency:) ⇒ Stripe::PaymentMethod
Attach a PaymentMethod to a Stripe customer (used during vault
creation in Gateways::CreditCard#store).
289 290 291 |
# File 'app/services/payment/apis/stripe.rb', line 289 def self.attach_payment_method(payment_method_id, customer_id:, currency:) ::Stripe::PaymentMethod.attach(payment_method_id, { customer: customer_id }, (currency)) end |
.cancel_payment_intent(payment_intent_id, currency:, reason: nil) ⇒ Stripe::PaymentIntent, Stripe::Refund
Cancel an authorized but un-captured PaymentIntent. Falls back to
refund_charge for legacy ch_xxx ids.
137 138 139 140 141 142 143 144 |
# File 'app/services/payment/apis/stripe.rb', line 137 def self.cancel_payment_intent(payment_intent_id, currency:, reason: nil) resolved_id = resolve_payment_intent_id(payment_intent_id, currency: currency) return refund_charge(payment_intent_id, currency: currency) if resolved_id.nil? params = {} params[:cancellation_reason] = reason if reason.present? ::Stripe::PaymentIntent.cancel(resolved_id, params, (currency)) end |
.capture_charge(charge_id, currency:, amount_cents: nil) ⇒ Stripe::Charge
Legacy ActiveMerchant ch_xxx charge capture path used when a
PaymentIntent doesn't exist yet. Should fade out as old payments
cycle through.
343 344 345 346 347 |
# File 'app/services/payment/apis/stripe.rb', line 343 def self.capture_charge(charge_id, currency:, amount_cents: nil) params = {} params[:amount] = amount_cents if amount_cents.present? ::Stripe::Charge.capture(charge_id, params, (currency)) end |
.capture_payment_intent(payment_intent_id, currency:, amount_cents: nil, final_capture: true) ⇒ Stripe::PaymentIntent, Stripe::Charge
Capture an authorized PaymentIntent. Falls back to
capture_charge for legacy ActiveMerchant ch_xxx ids that have
no parent PI.
106 107 108 109 110 111 112 113 114 |
# File 'app/services/payment/apis/stripe.rb', line 106 def self.capture_payment_intent(payment_intent_id, currency:, amount_cents: nil, final_capture: true) resolved_id = resolve_payment_intent_id(payment_intent_id, currency: currency) return capture_charge(payment_intent_id, currency: currency, amount_cents: amount_cents) if resolved_id.nil? params = {} params[:amount_to_capture] = amount_cents if amount_cents.present? params[:final_capture] = false unless final_capture ::Stripe::PaymentIntent.capture(resolved_id, params, (currency)) end |
.charge?(id) ⇒ Boolean
368 369 370 |
# File 'app/services/payment/apis/stripe.rb', line 368 def self.charge?(id) id.to_s.start_with?('ch_') end |
.confirm_payment_intent(payment_intent_id, currency:, payment_method: nil, return_url: nil) ⇒ Stripe::PaymentIntent
Confirm a PaymentIntent (typically after the customer authorizes it
via 3DS). Optional payment_method/return_url are only set if
provided.
155 156 157 158 159 160 |
# File 'app/services/payment/apis/stripe.rb', line 155 def self.confirm_payment_intent(payment_intent_id, currency:, payment_method: nil, return_url: nil) params = {} params[:payment_method] = payment_method if payment_method.present? params[:return_url] = return_url if return_url.present? ::Stripe::PaymentIntent.confirm(payment_intent_id, params, (currency)) end |
.create_customer(currency:, email: nil, name: nil, metadata: {}) ⇒ Object
========================================
Customer Operations
263 264 265 266 267 268 |
# File 'app/services/payment/apis/stripe.rb', line 263 def self.create_customer(currency:, email: nil, name: nil, metadata: {}) params = { metadata: } params[:email] = email if email.present? params[:name] = name if name.present? ::Stripe::Customer.create(params, (currency)) end |
.create_payment_intent(amount_cents:, currency:, customer_id: nil, payment_method: nil, capture_method: 'manual', confirm: false, off_session: false, setup_future_usage: nil, metadata: {}, description: nil, statement_descriptor: nil, shipping: nil, return_url: nil, payment_method_types: nil, allow_redirects: 'never', payment_method_options: nil) ⇒ Object
========================================
PaymentIntent Operations
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'app/services/payment/apis/stripe.rb', line 42 def self.create_payment_intent(amount_cents:, currency:, customer_id: nil, payment_method: nil, capture_method: 'manual', confirm: false, off_session: false, setup_future_usage: nil, metadata: {}, description: nil, statement_descriptor: nil, shipping: nil, return_url: nil, payment_method_types: nil, allow_redirects: 'never', payment_method_options: nil) params = { amount: amount_cents, currency: currency.downcase, capture_method: capture_method, metadata: } merged_pmo = if capture_method == 'manual' base = (currency) base ? base.deep_merge( || {}) : else end params[:payment_method_options] = merged_pmo if merged_pmo.present? params[:customer] = customer_id if customer_id.present? params[:payment_method] = payment_method if payment_method.present? params[:confirm] = confirm if confirm params[:off_session] = off_session if off_session params[:setup_future_usage] = setup_future_usage if setup_future_usage.present? params[:description] = description if description.present? params[:statement_descriptor_suffix] = truncate_descriptor(statement_descriptor) if statement_descriptor.present? params[:shipping] = shipping if shipping.present? params[:return_url] = return_url if return_url.present? if payment_method_types.present? params[:payment_method_types] = payment_method_types elsif confirm && off_session params[:payment_method_types] = ['card'] else params[:automatic_payment_methods] = { enabled: true, allow_redirects: allow_redirects } end ::Stripe::PaymentIntent.create(params, (currency)) end |
.create_refund(payment_intent_id:, currency:, amount_cents: nil, reason: nil) ⇒ Object
246 247 248 249 250 251 252 253 254 255 256 257 |
# File 'app/services/payment/apis/stripe.rb', line 246 def self.create_refund(payment_intent_id:, currency:, amount_cents: nil, reason: nil) resolved_id = resolve_payment_intent_id(payment_intent_id, currency: currency) params = {} if resolved_id.nil? params[:charge] = payment_intent_id else params[:payment_intent] = resolved_id end params[:amount] = amount_cents if amount_cents.present? params[:reason] = reason if reason.present? ::Stripe::Refund.create(params, (currency)) end |
.create_setup_intent(currency:, customer_id: nil, payment_method_types: ['card'], usage: 'off_session', metadata: {}) ⇒ Object
========================================
SetupIntent Operations
312 313 314 315 316 317 318 319 320 321 |
# File 'app/services/payment/apis/stripe.rb', line 312 def self.create_setup_intent(currency:, customer_id: nil, payment_method_types: ['card'], usage: 'off_session', metadata: {}) params = { payment_method_types: payment_method_types, usage: usage, metadata: } params[:customer] = customer_id if customer_id.present? ::Stripe::SetupIntent.create(params, (currency)) end |
.detach_payment_method(payment_method_id, currency:) ⇒ Stripe::PaymentMethod
Detach a PaymentMethod from its customer; called when a vault is
discarded.
297 298 299 |
# File 'app/services/payment/apis/stripe.rb', line 297 def self.detach_payment_method(payment_method_id, currency:) ::Stripe::PaymentMethod.detach(payment_method_id, {}, (currency)) end |
.each_payment_intent(query, currency:, limit: 100) {|pi| ... } ⇒ Enumerator<Stripe::PaymentIntent>
Iterate every PaymentIntent matching query across all pages. Uses
Stripe's search-API next_page token (not the starting_after
cursor that list endpoints use).
Used by OrphanPaymentIntentReconciliationWorker to walk
every succeeded PI in the lookback window.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'app/services/payment/apis/stripe.rb', line 194 def self.each_payment_intent(query, currency:, limit: 100, &block) return enum_for(:each_payment_intent, query, currency: currency, limit: limit) unless block page_token = nil loop do page_params = { query: query, limit: limit } page_params[:page] = page_token if page_token page = ::Stripe::PaymentIntent.search(page_params, (currency)) data = page.respond_to?(:data) ? page.data.to_a : Array(page) data.each(&block) break unless page.respond_to?(:has_more) && page.has_more && page.respond_to?(:next_page) && page.next_page page_token = page.next_page end end |
.increment_authorization(payment_intent_id, amount_cents:, currency:, description: nil) ⇒ Stripe::PaymentIntent
Step a PaymentIntent's authorized amount up. Stripe enforces
that the PI supports incremental_authorization.
124 125 126 127 128 |
# File 'app/services/payment/apis/stripe.rb', line 124 def self.(payment_intent_id, amount_cents:, currency:, description: nil) params = { amount: amount_cents } params[:description] = description if description.present? ::Stripe::PaymentIntent.(payment_intent_id, params, (currency)) end |
.list_payment_methods(customer_id, currency:, type: 'card') ⇒ Stripe::ListObject
List PaymentMethods on a customer (defaults to cards).
304 305 306 |
# File 'app/services/payment/apis/stripe.rb', line 304 def self.list_payment_methods(customer_id, currency:, type: 'card') ::Stripe::Customer.list_payment_methods(customer_id, { type: type }, (currency)) end |
.list_refunds(currency:, created_gte: nil, limit: 100, &block) ⇒ Enumerator<Stripe::Refund>
List refunds on the Stripe account that handles currency. Yields
each Stripe::Refund across all pages so callers don't have to
thread has_more / starting_after themselves.
Used by StripeRefundReconciliationWorker to sync
Stripe-side refunds (created on the Stripe dashboard by ops) back
into Heatwave's Payment state.
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'app/services/payment/apis/stripe.rb', line 227 def self.list_refunds(currency:, created_gte: nil, limit: 100, &block) return enum_for(:list_refunds, currency: currency, created_gte: created_gte, limit: limit) unless block params = { limit: limit } params[:created] = { gte: created_gte } if created_gte starting_after = nil loop do page_params = params.dup page_params[:starting_after] = starting_after if starting_after page = ::Stripe::Refund.list(page_params, (currency)) data = page.respond_to?(:data) ? page.data.to_a : Array(page) data.each(&block) break unless page.respond_to?(:has_more) && page.has_more && data.any? starting_after = data.last.id end end |
.payment_intent?(id) ⇒ Boolean
========================================
Helpers
364 365 366 |
# File 'app/services/payment/apis/stripe.rb', line 364 def self.payment_intent?(id) id.to_s.start_with?('pi_') end |
.publishable_key(currency_or_country) ⇒ String
Stripe publishable key for the matching account; used in the
storefront when initializing Stripe.js.
33 34 35 36 |
# File 'app/services/payment/apis/stripe.rb', line 33 def self.publishable_key(currency_or_country) region = region_for(currency_or_country) Heatwave::Configuration.fetch(:stripe, region, :key) end |
.refund_charge(charge_id, currency:, amount_cents: nil, reason: nil) ⇒ Stripe::Refund
Legacy ch_xxx refund path; created via the Refunds API for
backward compatibility with pre-PI payments.
353 354 355 356 357 358 |
# File 'app/services/payment/apis/stripe.rb', line 353 def self.refund_charge(charge_id, currency:, amount_cents: nil, reason: nil) params = { charge: charge_id } params[:amount] = amount_cents if amount_cents.present? params[:reason] = reason if reason.present? ::Stripe::Refund.create(params, (currency)) end |
.retrieve_charge(charge_id, currency:) ⇒ Object
========================================
Charge Operations (backward compat for old ch_xxx payments)
334 335 336 |
# File 'app/services/payment/apis/stripe.rb', line 334 def self.retrieve_charge(charge_id, currency:) ::Stripe::Charge.retrieve(charge_id, (currency)) end |
.retrieve_customer(customer_id, currency:) ⇒ Stripe::Customer
273 274 275 |
# File 'app/services/payment/apis/stripe.rb', line 273 def self.retrieve_customer(customer_id, currency:) ::Stripe::Customer.retrieve(customer_id, (currency)) end |
.retrieve_payment_intent(payment_intent_id, currency:, expand: %w[latest_charge latest_charge.payment_method_details])) ⇒ Stripe::PaymentIntent
Retrieve a PaymentIntent. Defaults to expanding the latest charge
and its payment_method_details so callers can read card metadata
without an extra round-trip.
90 91 92 93 94 95 |
# File 'app/services/payment/apis/stripe.rb', line 90 def self.retrieve_payment_intent(payment_intent_id, currency:, expand: %w[latest_charge latest_charge.payment_method_details]) ::Stripe::PaymentIntent.retrieve( { id: payment_intent_id, expand: }, (currency) ) end |
.retrieve_payment_method(payment_method_id, currency:) ⇒ Object
========================================
PaymentMethod Operations
281 282 283 |
# File 'app/services/payment/apis/stripe.rb', line 281 def self.retrieve_payment_method(payment_method_id, currency:) ::Stripe::PaymentMethod.retrieve(payment_method_id, (currency)) end |
.retrieve_payment_object(id, currency:) ⇒ Stripe::PaymentIntent, ...
Retrieve whichever upstream object a pi_xxx or ch_xxx
authorization_code corresponds to. Returns nil for unknown
prefixes.
379 380 381 382 383 384 385 |
# File 'app/services/payment/apis/stripe.rb', line 379 def self.retrieve_payment_object(id, currency:) if payment_intent?(id) retrieve_payment_intent(id, currency: currency) elsif charge?(id) retrieve_charge(id, currency: currency) end end |
.retrieve_setup_intent(setup_intent_id, currency:) ⇒ Stripe::SetupIntent
326 327 328 |
# File 'app/services/payment/apis/stripe.rb', line 326 def self.retrieve_setup_intent(setup_intent_id, currency:) ::Stripe::SetupIntent.retrieve(setup_intent_id, (currency)) end |
.search_payment_intents(query, currency:, limit: 10) ⇒ Array<Stripe::PaymentIntent>
Search PaymentIntents on the Stripe account that handles currency.
Used by DuplicateChargeGuard to detect a possible
duplicate charge before sending a fresh one. Stripe's search API
is eventually consistent (up to ~1 minute lag) — fine for the
operator-retry case the guard targets, since the second click
tends to come at least that long after the first.
174 175 176 177 178 179 180 |
# File 'app/services/payment/apis/stripe.rb', line 174 def self.search_payment_intents(query, currency:, limit: 10) page = ::Stripe::PaymentIntent.search( { query: query, limit: limit }, (currency) ) page.respond_to?(:data) ? page.data.to_a : Array(page) end |