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).
286 287 288 |
# File 'app/services/payment/apis/stripe.rb', line 286 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.
136 137 138 139 140 141 142 143 |
# File 'app/services/payment/apis/stripe.rb', line 136 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.
338 339 340 341 342 |
# File 'app/services/payment/apis/stripe.rb', line 338 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.
105 106 107 108 109 110 111 112 113 |
# File 'app/services/payment/apis/stripe.rb', line 105 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
362 363 364 |
# File 'app/services/payment/apis/stripe.rb', line 362 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.
154 155 156 157 158 159 |
# File 'app/services/payment/apis/stripe.rb', line 154 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
261 262 263 264 265 266 |
# File 'app/services/payment/apis/stripe.rb', line 261 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
41 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 |
# File 'app/services/payment/apis/stripe.rb', line 41 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
245 246 247 248 249 250 251 252 253 254 255 256 |
# File 'app/services/payment/apis/stripe.rb', line 245 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
308 309 310 311 312 313 314 315 316 317 |
# File 'app/services/payment/apis/stripe.rb', line 308 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.
294 295 296 |
# File 'app/services/payment/apis/stripe.rb', line 294 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.
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'app/services/payment/apis/stripe.rb', line 193 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.
123 124 125 126 127 |
# File 'app/services/payment/apis/stripe.rb', line 123 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).
301 302 303 |
# File 'app/services/payment/apis/stripe.rb', line 301 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.
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'app/services/payment/apis/stripe.rb', line 226 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
358 359 360 |
# File 'app/services/payment/apis/stripe.rb', line 358 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.
348 349 350 351 352 353 |
# File 'app/services/payment/apis/stripe.rb', line 348 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
329 330 331 |
# File 'app/services/payment/apis/stripe.rb', line 329 def self.retrieve_charge(charge_id, currency:) ::Stripe::Charge.retrieve(charge_id, (currency)) end |
.retrieve_customer(customer_id, currency:) ⇒ Stripe::Customer
271 272 273 |
# File 'app/services/payment/apis/stripe.rb', line 271 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.
89 90 91 92 93 94 |
# File 'app/services/payment/apis/stripe.rb', line 89 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
278 279 280 |
# File 'app/services/payment/apis/stripe.rb', line 278 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.
373 374 375 376 377 378 379 |
# File 'app/services/payment/apis/stripe.rb', line 373 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
322 323 324 |
# File 'app/services/payment/apis/stripe.rb', line 322 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.
173 174 175 176 177 178 179 |
# File 'app/services/payment/apis/stripe.rb', line 173 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 |