pay 2.7.0 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pay might be problematic. Click here for more details.

Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -53
  3. data/db/migrate/20200603134434_add_data_to_pay_models.rb +2 -11
  4. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +1 -10
  5. data/lib/generators/active_record/billable_generator.rb +44 -0
  6. data/lib/generators/active_record/merchant_generator.rb +44 -0
  7. data/lib/generators/active_record/templates/billable_migration.rb +16 -0
  8. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  9. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  10. data/lib/generators/pay/merchant_generator.rb +17 -0
  11. data/lib/generators/pay/orm_helpers.rb +10 -6
  12. data/lib/pay/adapter.rb +9 -0
  13. data/lib/pay/env.rb +8 -0
  14. data/lib/pay/stripe.rb +6 -0
  15. data/lib/pay/stripe/billable.rb +6 -27
  16. data/lib/pay/stripe/charge.rb +29 -0
  17. data/lib/pay/stripe/subscription.rb +29 -0
  18. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  19. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  20. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +13 -0
  21. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +13 -0
  22. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +2 -8
  23. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -36
  24. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -9
  25. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -6
  26. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -29
  27. data/lib/pay/version.rb +1 -1
  28. metadata +10 -5
  29. data/lib/generators/active_record/pay_generator.rb +0 -58
  30. data/lib/generators/active_record/templates/migration.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d696d24ee2bb2b4827f6d6e31db738752ebb2eadca4ea0e79144518fc9375be9
4
- data.tar.gz: 8aad550b117bee7e28c9231832f4f344c37054db4f1a7d0176f034c780d0abed
3
+ metadata.gz: a90781f9b476b2396d1ab4ea8d1a6317b46ac6289f5c4afef96dc991af035fd0
4
+ data.tar.gz: 8d20e7c6edcb13eb62d5ad398c4ab12a353e3be784c7651d795efd9feeb18402
5
5
  SHA512:
6
- metadata.gz: 5dc610b8d7b67ed4073fea783b290d3cd6ada3f4df1ed7bac26a0c4770f4a5ab854bf492fe6541f14b069ae61b10369024689a9f832d625ba2201a14deb24fc1
7
- data.tar.gz: 3801e3d1da5a833869ed46d28e04d633df5e1b55e51900bcae1a8b437663b93ee1b5ab45698cb54e00a145fb1752cb4c195ec877070b367b79d8c3b03ce81e8d
6
+ metadata.gz: 11f281fe1df7c9eabfef2d4a0242257001115b3f7eaaa5571d73cbdacb1810891a0a57ab24a23caa01458de399093b3c82a3c251237e9d3d13f993e670f06b04
7
+ data.tar.gz: 11d760da77af12107686c4e91fcbec0f7bf4aa91e99d41281f1510bedabc00406146ade7656477ffa5ee4dda4e7b07af55a9dd0ae7675d1e780e8f4f121d6cc5
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Pay - Payments engine for Ruby on Rails
4
4
 
5
- [![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions) [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
5
+ [![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions) [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
6
6
 
7
7
  <img src="docs/images/stripe_partner_badge.svg" height="26px">
8
8
 
@@ -51,61 +51,49 @@ And then execute:
51
51
  bundle
52
52
  ```
53
53
 
54
- Make sure you've configured your ActionMailer default_url_options so Pay can generate links to for features like Stripe Checkout.
54
+ Next, we need to add migrations to your application, run the following migration:
55
55
 
56
- ```ruby
57
- # config/application.rb
58
- config.action_mailer.default_url_options = { host: "example.com" }
59
- ```
60
-
61
- #### Migrations
62
-
63
- To add the migrations to your application, run the following migration:
56
+ ````bash
57
+ bin/rails pay:install:migrations
58
+ ````
64
59
 
65
- `bin/rails pay:install:migrations`
66
-
67
- >If your models rely on non integer ids (uuids for example) you will need to alter the `create_pay_subscriptions` and `create_pay_charges` migrations to ensure the `owner_id` column's type matches the model's id's format.
68
- > As commented in the migrations adding a type parameter to the `t.references :owner` line will ensure the `owner_id` column is of the correct type.
60
+ >If your models rely on non integer ids (uuids for example) you will need to alter the `create_pay_subscriptions` and `create_pay_charges` migrations.
69
61
 
70
62
  We also need to run migrations to add Pay to the User, Account, Team, etc models that we want to make payments in our app.
71
63
 
72
- `bin/rails g pay User`
64
+ ```bash
65
+ bin/rails g pay:billable User
66
+ ```
73
67
 
74
68
  This will generate a migration to add Pay fields to our User model and automatically includes the `Pay::Billable` module in our `User` model. Repeat this for all the models you want to make payments in your app.
75
69
 
76
- Finally, run the migrations
77
-
78
- `rake db:migrate`
70
+ **Note:** An `email` attribute or method on your `Billable` model is required.
79
71
 
80
- #### Getting NoMethodError?
72
+ To sync customer names, your `Billable` model should respond to the `first_name` and `last_name` methods. Pay will sync these over to your Customer objects in Stripe and Braintree.
81
73
 
82
- `NoMethodError (undefined method 'stripe_customer' for #<User:0x00007fbc34b9bf20>)`
74
+ Finally, run the migrations
83
75
 
84
- Fully restart your Rails application `bin/spring stop && rails s`
76
+ ```bash
77
+ rake db:migrate
78
+ ```
85
79
 
86
- ## Usage
80
+ > If you run into `NoMethodError (undefined method 'stripe_customer' for #<User:0x00007fbc34b9bf20>)`, fully restart your Rails application `bin/spring stop && rails s`
87
81
 
88
- The `Pay::Billable` module should be included in the models you want to make payments and subscriptions.
82
+ Lastly, make sure you've configured your ActionMailer default_url_options so Pay can generate links to for features like Stripe Checkout.
89
83
 
90
84
  ```ruby
91
- # app/models/user.rb
92
- class User < ActiveRecord::Base
93
- include Pay::Billable
94
- end
85
+ # config/application.rb
86
+ config.action_mailer.default_url_options = { host: "example.com" }
95
87
  ```
96
88
 
97
- An `email` attribute or method on your `Billable` model is required.
98
-
99
- To sync over customer names, your `Billable` model should respond to the `first_name` and `last_name` methods. Pay will sync these over to your Customer objects in Stripe and Braintree.
100
-
101
89
  ## Configuration
102
90
 
103
91
  Need to make some changes to how Pay is used? You can create an initializer `config/initializers/pay.rb`
104
92
 
105
93
  ```ruby
106
94
  Pay.setup do |config|
107
- config.chargeable_class = 'Pay::Charge'
108
- config.chargeable_table = 'pay_charges'
95
+ # config.chargeable_class = 'Pay::Charge'
96
+ # config.chargeable_table = 'pay_charges'
109
97
 
110
98
  # For use in the receipt/refund/renewal mailers
111
99
  config.business_name = "Business Name"
@@ -139,29 +127,59 @@ end
139
127
 
140
128
  ### Credentials
141
129
 
142
- You'll need to add your private Stripe API key to your Rails secrets `config/secrets.yml`, credentials `rails credentials:edit`
130
+ Pay automatically looks up credentials for each payment provider. We recommend storing them in the Rails credentials.
131
+
132
+ ##### Rails Credentials & Secrets
133
+
134
+ You'll need to add your API keys to your Rails credentials. You can do this by running:
135
+
136
+ ```bash
137
+ rails credentials:edit --environment=development
138
+ ```
139
+
140
+ They should be formatted like the following:
141
+
142
+ ```yaml
143
+ stripe:
144
+ private_key: xxxx
145
+ public_key: yyyy
146
+ signing_secret: zzzz
147
+ braintree:
148
+ private_key: xxxx
149
+ public_key: yyyy
150
+ merchant_id: aaaa
151
+ environment: sandbox
152
+ paddle:
153
+ vendor_id: xxxx
154
+ vendor_auth_code: yyyy
155
+ public_key_base64: MII...==
156
+ environment: sandbox
157
+ ```
158
+
159
+ You can also nest these credentials under the Rails environment if using a shared credentials file or secrets.
143
160
 
144
161
  ```yaml
145
162
  development:
146
163
  stripe:
147
164
  private_key: xxxx
148
- public_key: yyyy
149
- signing_secret: zzzz
150
- braintree:
151
- private_key: xxxx
152
- public_key: yyyy
153
- merchant_id: aaaa
154
- environment: sandbox
155
- paddle:
156
- vendor_id: xxxx
157
- vendor_auth_code: yyyy
158
- public_key_base64: MII...==
159
- environment: sandbox
165
+ # ...
160
166
  ```
161
167
 
162
- For Stripe, you can also use the `STRIPE_PUBLIC_KEY`, `STRIPE_PRIVATE_KEY` and `STRIPE_SIGNING_SECRET` environment variables.
163
- For Braintree, you can also use `BRAINTREE_MERCHANT_ID`, `BRAINTREE_PUBLIC_KEY`, `BRAINTREE_PRIVATE_KEY`, and `BRAINTREE_ENVIRONMENT` environment variables.
164
- For Paddle, you can also use `PADDLE_VENDOR_ID`, `PADDLE_VENDOR_AUTH_CODE`, `PADDLE_PUBLIC_KEY_BASE64` and `PADDLE_ENVIRONMENT` environment variables.
168
+ ##### Environment Variables
169
+
170
+ Pay will also check environment variables for API keys:
171
+
172
+ * `STRIPE_PUBLIC_KEY`
173
+ * `STRIPE_PRIVATE_KEY`
174
+ * `STRIPE_SIGNING_SECRET`
175
+ * `BRAINTREE_MERCHANT_ID`
176
+ * `BRAINTREE_PUBLIC_KEY`
177
+ * `BRAINTREE_PRIVATE_KEY`
178
+ * `BRAINTREE_ENVIRONMENT`
179
+ * `PADDLE_VENDOR_ID`
180
+ * `PADDLE_VENDOR_AUTH_CODE`
181
+ * `PADDLE_PUBLIC_KEY_BASE64`
182
+ * `PADDLE_ENVIRONMENT`
165
183
 
166
184
  ### Generators
167
185
 
@@ -179,11 +197,13 @@ bin/rails generate pay:email_views
179
197
 
180
198
  ### Emails
181
199
 
182
- Emails can be enabled/disabled using the `send_emails` configuration option (enabled per default). When enabled, the following emails will be sent:
200
+ Emails can be enabled/disabled using the `send_emails` configuration option (enabled by default).
201
+
202
+ When enabled, the following emails will be sent when:
183
203
 
184
- - When a charge succeeded
185
- - When a charge was refunded
186
- - When a subscription is about to renew
204
+ - A charge succeeded
205
+ - A charge was refunded
206
+ - A subscription is about to renew
187
207
 
188
208
 
189
209
  ## Billable API
@@ -1,15 +1,6 @@
1
1
  class AddDataToPayModels < ActiveRecord::Migration[4.2]
2
2
  def change
3
- add_column :pay_subscriptions, :data, data_column_type
4
- add_column :pay_charges, :data, data_column_type
5
- end
6
-
7
- def data_column_type
8
- case Pay::Adapter.current_adapter
9
- when "postgresql"
10
- :jsonb
11
- else
12
- :json
13
- end
3
+ add_column :pay_subscriptions, :data, Pay::Adapter.json_column_type
4
+ add_column :pay_charges, :data, Pay::Adapter.json_column_type
14
5
  end
15
6
  end
@@ -4,16 +4,7 @@ class AddDataToPayBillable < ActiveRecord::Migration[4.2]
4
4
  Rails.application.eager_load!
5
5
 
6
6
  Pay.billable_models.each do |model|
7
- add_column model.table_name, :pay_data, data_column_type
8
- end
9
- end
10
-
11
- def data_column_type
12
- case Pay::Adapter.current_adapter
13
- when "postgresql"
14
- :jsonb
15
- else
16
- :json
7
+ add_column model.table_name, :pay_data, Pay::Adapter.json_column_type
17
8
  end
18
9
  end
19
10
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record"
4
+ require "generators/pay/orm_helpers"
5
+
6
+ module ActiveRecord
7
+ module Generators
8
+ class BillableGenerator < ActiveRecord::Generators::Base
9
+ include Pay::Generators::OrmHelpers
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ def copy_pay_billable_migration
13
+ if (behavior == :invoke && model_exists?) || (behavior == :revoke && migration_exists?(table_name))
14
+ migration_template "billable_migration.rb", "#{migration_path}/add_pay_billable_to_#{table_name}.rb", migration_version: migration_version
15
+ else
16
+ say "#{model_path} does not exist.", :red
17
+ say "⚠️ Make sure the #{name} model exists before running this generator."
18
+ end
19
+ end
20
+
21
+ # If the file already contains the contents, the user will receive this warning:
22
+ #
23
+ # File unchanged! The supplied flag value not found!
24
+ #
25
+ # This can be ignored as it just means the contents already exist and the file is unchanged.
26
+ # Thor will be updated to improve this message: https://github.com/rails/thor/issues/706
27
+ def inject_pay_billable_content
28
+ return unless model_exists?
29
+
30
+ content = model_contents
31
+ class_path = (namespaced? ? class_name.to_s.split("::") : [class_name])
32
+ indent_depth = class_path.size - 1
33
+ content = content.split("\n").map { |line| " " * indent_depth + line }.join("\n") << "\n"
34
+ inject_into_class(model_path, class_path.last, content)
35
+ end
36
+
37
+ private
38
+
39
+ def model_contents
40
+ " include Pay::Billable"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/active_record"
4
+ require "generators/pay/orm_helpers"
5
+
6
+ module ActiveRecord
7
+ module Generators
8
+ class MerchantGenerator < ActiveRecord::Generators::Base
9
+ include Pay::Generators::OrmHelpers
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ def copy_pay_merchant_migration
13
+ if (behavior == :invoke && model_exists?) || (behavior == :revoke && migration_exists?(table_name))
14
+ migration_template "merchant_migration.rb", "#{migration_path}/add_pay_merchant_to_#{table_name}.rb", migration_version: migration_version
15
+ else
16
+ say "#{model_path} does not exist.", :red
17
+ say "⚠️ Make sure the #{name} model exists before running this generator."
18
+ end
19
+ end
20
+
21
+ # If the file already contains the contents, the user will receive this warning:
22
+ #
23
+ # File unchanged! The supplied flag value not found!
24
+ #
25
+ # This can be ignored as it just means the contents already exist and the file is unchanged.
26
+ # Thor will be updated to improve this message: https://github.com/rails/thor/issues/706
27
+ def inject_pay_merchant_content
28
+ return unless model_exists?
29
+
30
+ content = model_contents
31
+ class_path = (namespaced? ? class_name.to_s.split("::") : [class_name])
32
+ indent_depth = class_path.size - 1
33
+ content = content.split("\n").map { |line| " " * indent_depth + line }.join("\n") << "\n"
34
+ inject_into_class(model_path, class_path.last, content)
35
+ end
36
+
37
+ private
38
+
39
+ def model_contents
40
+ " include Pay::Merchant"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPayBillableTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ change_table :<%= table_name %>, bulk: true do |t|
6
+ t.string :processor
7
+ t.string :processor_id
8
+ t.datetime :trial_ends_at
9
+ t.string :card_type
10
+ t.string :card_last4
11
+ t.string :card_exp_month
12
+ t.string :card_exp_year
13
+ t.text :extra_billing_info
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPayMerchantTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ add_column table_name, :merchant_processor, :string
6
+
7
+ # We may already have the pay_data column if a Pay::Billable object is also a Pay::Merchant
8
+ unless ActiveRecord::Base.connection.column_exists?(table_name, :pay_data)
9
+ add_column :<%= table_name %>, :pay_data, Pay::Adapter.json_column_type
10
+ end
11
+ end
12
+ end
@@ -4,13 +4,12 @@ require "rails/generators/named_base"
4
4
 
5
5
  module Pay
6
6
  module Generators
7
- class PayGenerator < Rails::Generators::NamedBase
7
+ class BillableGenerator < Rails::Generators::NamedBase
8
8
  include Rails::Generators::ResourceHelpers
9
9
 
10
- namespace "pay"
11
10
  source_root File.expand_path("../templates", __FILE__)
12
11
 
13
- desc "Generates a migration to add Billable fields to a model."
12
+ desc "Generates a migration to add Pay::Billable fields to a model."
14
13
 
15
14
  hook_for :orm
16
15
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Pay
6
+ module Generators
7
+ class MerchantGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("../templates", __FILE__)
11
+
12
+ desc "Generates a migration to add Pay::Merchant fields to a model."
13
+
14
+ hook_for :orm
15
+ end
16
+ end
17
+ end
@@ -3,12 +3,6 @@
3
3
  module Pay
4
4
  module Generators
5
5
  module OrmHelpers
6
- def model_contents
7
- <<-CONTENT
8
- include Pay::Billable
9
- CONTENT
10
- end
11
-
12
6
  private
13
7
 
14
8
  def model_exists?
@@ -30,6 +24,16 @@ module Pay
30
24
  def model_path
31
25
  @model_path ||= File.join("app", "models", "#{file_path}.rb")
32
26
  end
27
+
28
+ def migration_version
29
+ if rails5_and_up?
30
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
31
+ end
32
+ end
33
+
34
+ def rails5_and_up?
35
+ Rails::VERSION::MAJOR >= 5
36
+ end
33
37
  end
34
38
  end
35
39
  end
data/lib/pay/adapter.rb CHANGED
@@ -9,5 +9,14 @@ module Pay
9
9
  ActiveRecord::Base.connection_config[:adapter]
10
10
  end
11
11
  end
12
+
13
+ def self.json_column_type
14
+ case current_adapter
15
+ when "postgresql"
16
+ :jsonb
17
+ else
18
+ :json
19
+ end
20
+ end
12
21
  end
13
22
  end
data/lib/pay/env.rb CHANGED
@@ -17,6 +17,14 @@ module Pay
17
17
  secrets&.dig(env, scope, name) ||
18
18
  credentials&.dig(scope, name) ||
19
19
  secrets&.dig(scope, name)
20
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
21
+ Rails.logger.error <<~MESSAGE
22
+ Rails was unable to decrypt credentials. Pay checks the Rails credentials to look for API keys for payment processors.
23
+
24
+ Make sure to set the `RAILS_MASTER_KEY` env variable or in the .key file. To learn more, run "bin/rails credentials:help"
25
+
26
+ If you're not using Rails credentials, you can delete `config/credentials.yml.enc` and `config/credentials/`.
27
+ MESSAGE
20
28
  end
21
29
 
22
30
  def env
data/lib/pay/stripe.rb CHANGED
@@ -10,6 +10,8 @@ module Pay
10
10
  autoload :AccountUpdated, "pay/stripe/webhooks/account_updated"
11
11
  autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
12
12
  autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
13
+ autoload :CheckoutSessionCompleted, "pay/stripe/webhooks/checkout_session_completed"
14
+ autoload :CheckoutSessionAsyncPaymentSucceeded, "pay/stripe/webhooks/checkout_session_async_payment_succeeded"
13
15
  autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
14
16
  autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
15
17
  autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
@@ -86,6 +88,10 @@ module Pay
86
88
 
87
89
  # If an account is updated in stripe, we should update it as well
88
90
  events.subscribe "stripe.account.updated", Pay::Stripe::Webhooks::AccountUpdated.new
91
+
92
+ # Handle subscriptions in Stripe Checkout Sessions
93
+ events.subscribe "stripe.checkout.session.completed", Pay::Stripe::Webhooks::CheckoutSessionCompleted.new
94
+ events.subscribe "stripe.checkout.session.async_payment_succeeded", Pay::Stripe::Webhooks::CheckoutSessionAsyncPaymentSucceeded.new
89
95
  end
90
96
  end
91
97
  end
@@ -65,7 +65,8 @@ module Pay
65
65
  Pay::Payment.new(payment_intent).validate
66
66
 
67
67
  # Create a new charge object
68
- save_pay_charge(payment_intent.charges.first)
68
+ charge = payment_intent.charges.first
69
+ Pay::Stripe::Charge.sync(charge.id, object: charge)
69
70
  rescue ::Stripe::StripeError => e
70
71
  raise Pay::Stripe::Error, e
71
72
  end
@@ -87,8 +88,11 @@ module Pay
87
88
  # Load the Stripe customer to verify it exists and update card if needed
88
89
  opts[:customer] = customer.id
89
90
 
91
+ # Create subscription on Stripe
90
92
  stripe_sub = ::Stripe::Subscription.create(opts, {stripe_account: stripe_account})
91
- subscription = billable.create_pay_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity, stripe_account: stripe_account, application_fee_percent: stripe_sub.application_fee_percent)
93
+
94
+ # Save Pay::Subscription
95
+ subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
92
96
 
93
97
  # No trial, card requires SCA
94
98
  if subscription.incomplete?
@@ -168,31 +172,6 @@ module Pay
168
172
  billable.card_token = nil
169
173
  end
170
174
 
171
- def save_pay_charge(object)
172
- charge = billable.charges.find_or_initialize_by(processor: :stripe, processor_id: object.id)
173
-
174
- attrs = {
175
- amount: object.amount,
176
- card_last4: object.payment_method_details.card.last4,
177
- card_type: object.payment_method_details.card.brand,
178
- card_exp_month: object.payment_method_details.card.exp_month,
179
- card_exp_year: object.payment_method_details.card.exp_year,
180
- created_at: Time.zone.at(object.created),
181
- currency: object.currency,
182
- stripe_account: stripe_account,
183
- application_fee_amount: object.application_fee_amount
184
- }
185
-
186
- # Associate charge with subscription if we can
187
- if object.invoice
188
- invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
189
- attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
190
- end
191
-
192
- charge.update(attrs)
193
- charge
194
- end
195
-
196
175
  # https://stripe.com/docs/api/checkout/sessions/create
197
176
  #
198
177
  # checkout(mode: "payment")
@@ -5,6 +5,35 @@ module Pay
5
5
 
6
6
  delegate :processor_id, :owner, :stripe_account, to: :pay_charge
7
7
 
8
+ def self.sync(charge_id, object: nil)
9
+ object ||= ::Stripe::Charge.retrieve(id: charge_id)
10
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
11
+ return unless owner
12
+
13
+ attrs = {
14
+ amount: object.amount,
15
+ amount_refunded: object.amount_refunded,
16
+ application_fee_amount: object.application_fee_amount,
17
+ card_exp_month: object.payment_method_details.card.exp_month,
18
+ card_exp_year: object.payment_method_details.card.exp_year,
19
+ card_last4: object.payment_method_details.card.last4,
20
+ card_type: object.payment_method_details.card.brand,
21
+ created_at: Time.at(object.created),
22
+ currency: object.currency,
23
+ stripe_account: owner.stripe_account
24
+ }
25
+
26
+ # Associate charge with subscription if we can
27
+ if object.invoice
28
+ invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
29
+ attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
30
+ end
31
+
32
+ pay_charge = owner.charges.find_or_initialize_by(processor: :stripe, processor_id: object.id)
33
+ pay_charge.update(attrs)
34
+ pay_charge
35
+ end
36
+
8
37
  def initialize(pay_charge)
9
38
  @pay_charge = pay_charge
10
39
  end
@@ -20,6 +20,35 @@ module Pay
20
20
  :trial_ends_at,
21
21
  to: :pay_subscription
22
22
 
23
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
24
+ # Skip loading the latest subscription details from the API if we already have it
25
+ object ||= ::Stripe::Subscription.retrieve(id: subscription_id, expand: ["pending_setup_intent", "latest_invoice.payment_intent"])
26
+
27
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
28
+ return unless owner
29
+
30
+ attributes = {
31
+ application_fee_percent: object.application_fee_percent,
32
+ processor_plan: object.plan.id,
33
+ quantity: object.quantity,
34
+ name: name,
35
+ status: object.status,
36
+ stripe_account: owner.stripe_account,
37
+ trial_ends_at: (object.trial_end ? Time.at(object.trial_end) : nil)
38
+ }
39
+
40
+ # Subscriptions cancelling in the future
41
+ attributes[:ends_at] = Time.at(object.current_period_end) if object.cancel_at_period_end
42
+
43
+ # Fully cancelled subscription
44
+ attributes[:ends_at] = Time.at(object.ended_at) if object.ended_at
45
+
46
+ # Update or create the subscription
47
+ pay_subscription = owner.subscriptions.find_or_initialize_by(processor: :stripe, processor_id: object.id)
48
+ pay_subscription.update(attributes)
49
+ pay_subscription
50
+ end
51
+
23
52
  def initialize(pay_subscription)
24
53
  @pay_subscription = pay_subscription
25
54
  end
@@ -3,13 +3,8 @@ module Pay
3
3
  module Webhooks
4
4
  class ChargeRefunded
5
5
  def call(event)
6
- object = event.data.object
7
- charge = Pay.charge_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- return unless charge.present?
10
-
11
- charge.update(amount_refunded: object.amount_refunded)
12
- notify_user(charge.owner, charge)
6
+ pay_charge = Pay::Stripe::Charge.sync(event.data.object.id)
7
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
13
8
  end
14
9
 
15
10
  def notify_user(billable, charge)
@@ -3,14 +3,8 @@ module Pay
3
3
  module Webhooks
4
4
  class ChargeSucceeded
5
5
  def call(event)
6
- object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
-
9
- return unless billable.present?
10
- return if billable.charges.where(processor_id: object.id).any?
11
-
12
- charge = Pay::Stripe::Billable.new(billable).save_pay_charge(object)
13
- notify_user(billable, charge)
6
+ pay_charge = Pay::Stripe::Charge.sync(event.data.object.id)
7
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
14
8
  end
15
9
 
16
10
  def notify_user(billable, charge)
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionAsyncPaymentSucceeded
5
+ def call(event)
6
+ if event.data.object.subscription
7
+ Pay::Stripe::Subscription.sync(event.data.object.subscription)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionCompleted
5
+ def call(event)
6
+ if event.data.object.subscription
7
+ Pay::Stripe::Subscription.sync(event.data.object.subscription)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -4,15 +4,9 @@ module Pay
4
4
  class PaymentIntentSucceeded
5
5
  def call(event)
6
6
  object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
-
9
- return unless billable.present?
10
-
11
7
  object.charges.data.each do |charge|
12
- next if billable.charges.where(processor_id: charge.id).any?
13
-
14
- charge = Pay::Stripe::Billable.new(billable).save_pay_charge(charge)
15
- notify_user(billable, charge)
8
+ pay_charge = Pay::Stripe::Charge.sync(charge.id)
9
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
16
10
  end
17
11
  end
18
12
 
@@ -3,42 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionCreated
5
5
  def call(event)
6
- object = event.data.object
7
-
8
- # We may already have the subscription in the database, so we can update that record
9
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
10
-
11
- # Create the subscription in the database if we don't have it already
12
- if subscription.nil?
13
- # The customer should already be in the database
14
- owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
15
-
16
- if owner.nil?
17
- Rails.logger.error("[Pay] Unable to find Pay::Billable with processor: :stripe and processor_id: '#{object.customer}'. Searched these models: #{Pay.billable_models.join(", ")}")
18
- return
19
- end
20
-
21
- subscription = Pay.subscription_model.new(name: Pay.default_product_name, owner: owner, processor: :stripe, processor_id: object.id)
22
- end
23
-
24
- subscription.application_fee_percent = object.application_fee_percent
25
- subscription.processor_plan = object.plan.id
26
- subscription.quantity = object.quantity
27
- subscription.status = object.status
28
- subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
29
-
30
- # If user was on trial, their subscription ends at the end of the trial
31
- subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
32
- subscription.trial_ends_at
33
-
34
- # User wasn't on trial, so subscription ends at period end
35
- elsif object.cancel_at_period_end
36
- Time.at(object.current_period_end)
37
-
38
- # Subscription isn't marked to cancel at period end
39
- end
40
-
41
- subscription.save!
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
42
7
  end
43
8
  end
44
9
  end
@@ -3,15 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionDeleted
5
5
  def call(event)
6
- object = event.data.object
7
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- # We couldn't find the subscription for some reason, maybe it's from another service
10
- return if subscription.nil?
11
-
12
- # User canceled subscriptions have an ends_at
13
- # Automatically canceled subscriptions need this value set
14
- subscription.update!(ends_at: Time.at(object.ended_at)) if subscription.ends_at.blank? && object.ended_at.present?
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
15
7
  end
16
8
  end
17
9
  end
@@ -5,12 +5,10 @@ module Pay
5
5
  def call(event)
6
6
  # Event is of type "invoice" see:
7
7
  # https://stripe.com/docs/api/invoices/object
8
- subscription = Pay.subscription_model.find_by(
9
- processor: :stripe,
10
- processor_id: event.data.object.subscription
11
- )
12
- date = Time.zone.at(event.data.object.next_payment_attempt)
13
- notify_user(subscription.owner, subscription, date) if subscription.present?
8
+ subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: event.data.object.subscription)
9
+ return unless subscription
10
+
11
+ notify_user(subscription.owner, subscription, Time.zone.at(event.data.object.next_payment_attempt))
14
12
  end
15
13
 
16
14
  def notify_user(billable, subscription, date)
@@ -3,35 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionUpdated
5
5
  def call(event)
6
- object = event.data.object
7
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- return if subscription.nil?
10
-
11
- # Delete any subscription attempts that have expired
12
- if object.status == "incomplete_expired"
13
- subscription.destroy
14
- return
15
- end
16
-
17
- subscription.application_fee_percent = object.application_fee_percent
18
- subscription.processor_plan = object.plan.id
19
- subscription.quantity = object.quantity
20
- subscription.status = object.status
21
- subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
22
-
23
- # If user was on trial, their subscription ends at the end of the trial
24
- subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
25
- subscription.trial_ends_at
26
-
27
- # User wasn't on trial, so subscription ends at period end
28
- elsif object.cancel_at_period_end
29
- Time.at(object.current_period_end)
30
-
31
- # Subscription isn't marked to cancel at period end
32
- end
33
-
34
- subscription.save!
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
35
7
  end
36
8
  end
37
9
  end
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "2.7.0"
2
+ VERSION = "2.7.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2021-05-19 00:00:00.000000000 Z
12
+ date: 2021-06-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -145,11 +145,14 @@ files:
145
145
  - db/migrate/20210309004259_add_data_to_pay_billable.rb
146
146
  - db/migrate/20210406215234_add_currency_to_pay_charges.rb
147
147
  - db/migrate/20210406215506_add_application_fee_to_pay_models.rb
148
- - lib/generators/active_record/pay_generator.rb
149
- - lib/generators/active_record/templates/migration.rb
148
+ - lib/generators/active_record/billable_generator.rb
149
+ - lib/generators/active_record/merchant_generator.rb
150
+ - lib/generators/active_record/templates/billable_migration.rb
151
+ - lib/generators/active_record/templates/merchant_migration.rb
152
+ - lib/generators/pay/billable_generator.rb
150
153
  - lib/generators/pay/email_views_generator.rb
154
+ - lib/generators/pay/merchant_generator.rb
151
155
  - lib/generators/pay/orm_helpers.rb
152
- - lib/generators/pay/pay_generator.rb
153
156
  - lib/generators/pay/views_generator.rb
154
157
  - lib/pay.rb
155
158
  - lib/pay/adapter.rb
@@ -199,6 +202,8 @@ files:
199
202
  - lib/pay/stripe/webhooks/account_updated.rb
200
203
  - lib/pay/stripe/webhooks/charge_refunded.rb
201
204
  - lib/pay/stripe/webhooks/charge_succeeded.rb
205
+ - lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb
206
+ - lib/pay/stripe/webhooks/checkout_session_completed.rb
202
207
  - lib/pay/stripe/webhooks/customer_deleted.rb
203
208
  - lib/pay/stripe/webhooks/customer_updated.rb
204
209
  - lib/pay/stripe/webhooks/payment_action_required.rb
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "rails/generators/active_record"
4
- require "generators/pay/orm_helpers"
5
-
6
- module ActiveRecord
7
- module Generators
8
- class PayGenerator < ActiveRecord::Generators::Base
9
- include Pay::Generators::OrmHelpers
10
- source_root File.expand_path("../templates", __FILE__)
11
-
12
- def copy_pay_billable_migration
13
- if (behavior == :invoke && model_exists?) || (behavior == :revoke && migration_exists?(table_name))
14
- migration_template "migration.rb", "#{migration_path}/add_pay_billable_to_#{table_name}.rb", migration_version: migration_version
15
- end
16
- # TODO: Throw error here that model should already exist if it doesn't
17
- end
18
-
19
- def inject_pay_billable_content
20
- content = model_contents
21
-
22
- class_path = if namespaced?
23
- class_name.to_s.split("::")
24
- else
25
- [class_name]
26
- end
27
-
28
- indent_depth = class_path.size - 1
29
- content = content.split("\n").map { |line| " " * indent_depth + line }.join("\n") << "\n"
30
-
31
- inject_into_class(model_path, class_path.last, content) if model_exists?
32
- end
33
-
34
- def migration_data
35
- <<RUBY
36
- t.string :processor
37
- t.string :processor_id
38
- t.datetime :trial_ends_at
39
- t.string :card_type
40
- t.string :card_last4
41
- t.string :card_exp_month
42
- t.string :card_exp_year
43
- t.text :extra_billing_info
44
- RUBY
45
- end
46
-
47
- def rails5_and_up?
48
- Rails::VERSION::MAJOR >= 5
49
- end
50
-
51
- def migration_version
52
- if rails5_and_up?
53
- "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddPayBillableTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
- def change
5
- change_table :<%= table_name %>, bulk: true do |t|
6
- <%= migration_data -%>
7
- end
8
- end
9
- end