Skip to content

Commit

Permalink
Separate authorize and capture support for Stripe. Closes pay-rails#457
Browse files Browse the repository at this point in the history
  • Loading branch information
excid3 authored Apr 19, 2022
1 parent 1942680 commit f89ea67
Show file tree
Hide file tree
Showing 10 changed files with 2,015 additions and 12 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ end
* Stripe.max_network_retries is now set to 2 by default. - @excid3
This adds idempotency keys automatically to each request so that they can be safely retried.
* Stripe Subscriptons can now be paused and resumed - @excid3
* Separate authorize and capture is now supported on Stripe - @excid3
```ruby
pay_charge = pay_customer.authorize(75_00)
pay_charge.capture
pay_charge.capture(amount_to_capture: 50_00) # or with an amount
```
* Store `stripe_receipt_url` on Pay::Charge - @mguidetti
* Replace `update_email!` with `update_customer!` - @excid3
* Add options for `cancel_now!` to support `invoice_now` and `prorate` flags for Stripe - @excid3
Expand Down
8 changes: 8 additions & 0 deletions app/models/pay/charge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class Charge < Pay::ApplicationRecord
store_accessor :data, :username # Venmo
store_accessor :data, :bank

store_accessor :data, :amount_captured
store_accessor :data, :payment_intent_id
store_accessor :data, :period_start
store_accessor :data, :period_end
store_accessor :data, :line_items
Expand All @@ -48,6 +50,8 @@ class Charge < Pay::ApplicationRecord
scope processor_name, -> { where(processor: processor_name) }
end

delegate :capture, to: :payment_processor

def self.find_by_processor_and_id(processor, processor_id)
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
end
Expand All @@ -64,6 +68,10 @@ def processor_charge
payment_processor.charge
end

def captured?
amount_captured > 0
end

def refund!(refund_amount = nil)
refund_amount ||= amount
payment_processor.refund!(refund_amount)
Expand Down
5 changes: 4 additions & 1 deletion lib/pay/stripe/billable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ def charge(amount, options = {})
args = {
amount: amount,
confirm: true,
confirmation_method: :automatic,
currency: "usd",
customer: processor_id,
payment_method: payment_method&.processor_id
Expand Down Expand Up @@ -268,6 +267,10 @@ def billing_portal(**options)
::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
end

def authorize(amount, options = {})
charge(amount, options.merge(capture_method: :manual))
end

private

# Options for Stripe requests
Expand Down
37 changes: 27 additions & 10 deletions lib/pay/stripe/charge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ module Stripe
class Charge
attr_reader :pay_charge

delegate :processor_id, :stripe_account, to: :pay_charge
delegate :amount,
:amount_captured,
:payment_intent_id,
:processor_id,
:stripe_account,
to: :pay_charge

def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
# Skip loading the latest charge details from the API if we already have it
Expand All @@ -15,22 +20,24 @@ def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
payment_method = object.payment_method_details.send(object.payment_method_details.type)
attrs = {
amount: object.amount,
amount_captured: object.amount_captured,
amount_refunded: object.amount_refunded,
application_fee_amount: object.application_fee_amount,
bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
brand: payment_method.try(:brand)&.capitalize,
created_at: Time.at(object.created),
currency: object.currency,
stripe_account: pay_customer.stripe_account,
stripe_receipt_url: object.receipt_url,
metadata: object.metadata,
payment_method_type: object.payment_method_details.type,
brand: payment_method.try(:brand)&.capitalize,
last4: payment_method.try(:last4).to_s,
discounts: [],
exp_month: payment_method.try(:exp_month).to_s,
exp_year: payment_method.try(:exp_year).to_s,
bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
last4: payment_method.try(:last4).to_s,
line_items: [],
total_tax_amounts: [],
discounts: []
metadata: object.metadata,
payment_intent_id: object.payment_intent,
payment_method_type: object.payment_method_details.type,
stripe_account: pay_customer.stripe_account,
stripe_receipt_url: object.receipt_url,
total_tax_amounts: []
}

# Associate charge with subscription if we can
Expand Down Expand Up @@ -110,6 +117,16 @@ def refund!(amount_to_refund, **options)
raise Pay::Stripe::Error, e
end

# https://stripe.com/docs/payments/capture-later
#
# capture
# capture(amount_to_capture: 15_00)
def capture(**options)
raise Pay::Stripe::Error, "no payment_intent_id on charge" unless payment_intent_id.present?
::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
self.class.sync(processor_id)
end

private

# Options for Stripe requests
Expand Down
17 changes: 17 additions & 0 deletions test/pay/stripe/billable_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,23 @@ class Pay::Stripe::BillableTest < ActiveSupport::TestCase
assert_nil @pay_subscription.pause_resumes_at
end

test "stripe can authorize a charge" do
@pay_customer.payment_method_token = payment_method
charge = @pay_customer.authorize(29_00)
assert_equal Pay::Charge, charge.class
assert_equal 0, charge.amount_captured
end

test "stripe can capture an authorized charge" do
@pay_customer.payment_method_token = payment_method
charge = @pay_customer.authorize(29_00)
assert_equal 0, charge.amount_captured

charge = charge.capture
assert charge.captured?
assert_equal 29_00, charge.amount_captured
end

private

def payment_method
Expand Down
2 changes: 2 additions & 0 deletions test/pay/stripe/charge_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ def fake_stripe_charge(**values)
id: "ch_123",
customer: "cus_1234",
amount: 19_00,
amount_captured: 19_00,
amount_refunded: nil,
application_fee_amount: 0,
created: 1546332337,
currency: "usd",
invoice: nil,
payment_intent: "pm_1234",
payment_method_details: {
card: {
exp_month: 1,
Expand Down
2 changes: 1 addition & 1 deletion test/support/fixtures/stripe/charge.refunded.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"order": null,
"outcome": null,
"paid": true,
"payment_intent": null,
"payment_intent": "pm_1234",
"payment_method": "cc_0dUM4C5X9gpJbg",
"payment_method_details": {
"card": {
Expand Down
2 changes: 2 additions & 0 deletions test/support/fixtures/stripe/charge.succeeded.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
"object": {
"id": "ch_chargeid",
"amount": 500,
"amount_captured": 500,
"amount_refunded": 0,
"application_fee_amount": null,
"created": 1546332337,
"currency": "usd",
"customer": "cus_customerid",
"invoice": null,
"metadata": {},
"payment_intent": "pm_1234",
"payment_method_details": {
"type": "card",
"card": {
Expand Down
Loading

0 comments on commit f89ea67

Please sign in to comment.