pay 2.0.3 → 2.2.0

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 300877c7b5a4a24f3796a7fe32c6deff0c2851a9a43d2c30ea86b69892384865
4
- data.tar.gz: 6c6a7b986a3fe4fd2731dcf21ee9d3adb1a4fb05f6cc07060b51a0aa50566459
3
+ metadata.gz: ba84d79847e4ef8659bcf7fce74b966c07bb93fa8e2f29f902a243c9d6e5c8bf
4
+ data.tar.gz: 78e829fc4d218aabee2544a969b184e29e2621bf0d965318d1b33e4e750ebae4
5
5
  SHA512:
6
- metadata.gz: 2b0b709fb6e3a3f51b3f74774908775af8a9ef3ab71ca8b2981c9fc010ede85cf0b71d779659e0e3dfb185ba20dcc16d90b71140992bc5e7241711be9334add2
7
- data.tar.gz: e22547e8e914cb70cc75d95bfd8414d1c5ca203e2cc7a8717416296c538b2123762499841b34aff655d74d7c7548a71d3f5de9f467af91acf6ee36d223f3b7d1
6
+ metadata.gz: 2c044f842d680969455c49a24124c48e2e80a1b06486db11782032ebdd0db810c1836151f97087bb1e06e083106524e9250520768a96462ff24fdc0723a4c5dd
7
+ data.tar.gz: 7dbe7db2d4c6161b9516f73a1496092b6ffb9ebf2c5471fe061ccfd79b1335bc0a3238c1e407f621e3717cbd0cf28496790153b884a8540557428b6cf33c665c
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
- <p align="center"><img src="logo.png"></p>
1
+ <p align="center"><img src="docs/logo.svg" height="50px"></p>
2
2
 
3
- ## Pay
3
+ ## Pay - Payments engine for Ruby on Rails
4
4
 
5
- ### Payments engine for Ruby on Rails
6
-
7
- [![Build Status](https://github.com/excid3/pay/workflows/Tests/badge.svg)](https://github.com/excid3/pay/actions)
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)
8
6
 
9
7
  Pay is a payments engine for Ruby on Rails 4.2 and higher.
10
8
 
@@ -15,6 +13,14 @@ Pay is a payments engine for Ruby on Rails 4.2 and higher.
15
13
 
16
14
  Want to add a new payment provider? Contributions are welcome and the instructions [are here](https://github.com/jasoncharnes/pay/wiki/New-Payment-Provider).
17
15
 
16
+ **Check the CHANGELOG for any required migrations or changes needed if you're upgrading from a previous version of Pay.**
17
+
18
+ ## Tutorial
19
+
20
+ Want to see how Pay works? Check out our video getting started guide.
21
+
22
+ <a href="https://www.youtube.com/watch?v=hYlOmqyJIgc" target="_blank"><img width="50%" src="http://i3.ytimg.com/vi/hYlOmqyJIgc/maxresdefault.jpg"></a>
23
+
18
24
  ## Installation
19
25
 
20
26
  Add these lines to your application's Gemfile:
@@ -39,52 +45,21 @@ And then execute:
39
45
  bundle
40
46
  ```
41
47
 
42
- Or install it yourself as:
43
-
44
- ```bash
45
- gem install pay
46
- ```
47
-
48
- ## Setup
49
-
50
- ### Migrations
51
-
52
- This engine will create a subscription model and the neccessary migrations for the model you want to make "billable." The most common use case for the billable model is a User.
48
+ #### Migrations
53
49
 
54
50
  To add the migrations to your application, run the following migration:
55
51
 
56
- `$ bin/rails pay:install:migrations`
52
+ `bin/rails pay:install:migrations`
57
53
 
58
- This will install four migrations:
54
+ 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.
59
55
 
60
- - db/migrate/create_subscriptions.pay.rb
61
- - db/migrate/add_fields_to_users.pay.rb
62
- - db/migrate/create_charges.pay.rb
63
- - db/migrate/add_status_to_subscriptions.pay.rb
56
+ `bin/rails g pay User`
64
57
 
65
- ### The Billable Module
58
+ 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.
66
59
 
67
- To enable payments for a model, you simply include the `Pay::Billable`
68
- module in it. By default, we assume this is `User`.
60
+ Finally, run the migrations
69
61
 
70
- If you'd like to use a different model, you can configure it in an
71
- initializer:
72
-
73
- ```ruby
74
- Pay.setup do |config|
75
- # Make the billable class the same name as your ActiveRecord model
76
- config.billable_class = "Team"
77
-
78
- # Make the billable table the same name as your ActiveRecord table name for the model
79
- # This is optional.
80
- # Once you update the billable class, the table name will use the ActiveRecord inflected table name
81
- config.billable_table = "teams"
82
- end
83
- ```
84
-
85
- #### Run the Migrations
86
-
87
- Finally, run the migrations with `$ rake db:migrate`
62
+ `rake db:migrate`
88
63
 
89
64
  #### Getting NoMethodError?
90
65
 
@@ -92,78 +67,9 @@ Finally, run the migrations with `$ rake db:migrate`
92
67
 
93
68
  Fully restart your Rails application `bin/spring stop && rails s`
94
69
 
95
- ## Payment Providers
96
-
97
- We support both Stripe and Braintree and make our best attempt to
98
- standardize the two. They function differently so keep that in mind if
99
- you plan on doing more complex payments. It would be best to stick with
100
- a single payment provider in that case so you don't run into
101
- discrepancies.
102
-
103
- #### Braintree
104
-
105
- ```yaml
106
- development:
107
- braintree:
108
- private_key: xxxx
109
- public_key: yyyy
110
- merchant_id: zzzz
111
- environment: sandbox
112
- ```
113
-
114
- #### Stripe
115
-
116
- You'll need to add your private Stripe API key to your Rails secrets `config/secrets.yml`, credentials `rails credentials:edit`
117
-
118
- ```yaml
119
- development:
120
- stripe:
121
- private_key: xxxx
122
- public_key: yyyy
123
- signing_secret: zzzz
124
- ```
125
-
126
- You can also use the `STRIPE_PRIVATE_KEY` and `STRIPE_SIGNING_SECRET` environment variables.
127
-
128
- **To see how to use Stripe Elements JS & Devise, [click here](https://github.com/jasoncharnes/pay/wiki/Using-Stripe-Elements-and-Devise).**
129
-
130
- ##### Strong Customer Authentication (SCA)
131
-
132
- Our Stripe integration **requires** the use of Payment Method objects to correctly support Strong Customer Authentication with Stripe. If you've previously been using card tokens, you'll need to upgrade your Javascript integration.
133
-
134
- Subscriptions that require SCA are marked as `incomplete` by default.
135
- Once payment is authenticated, Stripe will send a webhook updating the
136
- status of the subscription. You'll need to use the [Stripe CLI](https://github.com/stripe/stripe-cli) to forward
137
- webhooks to your application to make sure your subscriptions work
138
- correctly for SCA payments.
139
-
140
- ```bash
141
- stripe listen --forward-to localhost:3000/pay/webhooks/stripe
142
- ```
143
-
144
- You should use `stripe.handleCardSetup` on the client to collect card information anytime you want to save the card and charge them later (adding a card, then charging them on the next page for example). Use `stripe.handleCardPayment` if you'd like to charge the customer immediately (think checking out of a shopping cart).
145
-
146
- The Javascript will now need to use createPaymentMethod instead of createToken. https://stripe.com/docs/js/payment_intents/create_payment_method
147
-
148
- The Javascript also needs to have a PaymentIntent or SetupIntent created server-side and the ID passed into the Javascript to do this. That way it knows how to safely handle the card tokenization if it meets the SCA requirements.
149
-
150
- **Payment Confirmations**
151
-
152
- Sometimes you'll have a payment that requires extra authentication. In this case, Pay provides a webhook and action for handling these payments. It will automatically email the customer and provide a link with the PaymentIntent ID in the url where the customer will be asked to fill out their name and card number to confirm the payment. Once done, they'll be redirected back to your application.
153
-
154
- If you'd like to change the views of the payment confirmation page, you can install the views using the generator and modify the template.
155
-
156
- [<img src="https://d1jfzjx68gj8xs.cloudfront.net/items/2s3Z0J3Z3b1J1v2K2O1a/Screen%20Shot%202019-10-10%20at%2012.56.32%20PM.png?X-CloudApp-Visitor-Id=51470" alt="Stripe SCA Payment Confirmation" style="zoom: 25%;" />](https://d1jfzjx68gj8xs.cloudfront.net/items/2s3Z0J3Z3b1J1v2K2O1a/Screen%20Shot%202019-10-10%20at%2012.56.32%20PM.png)
157
-
158
- #### Background jobs
159
-
160
- If a user's email is updated and they have a `processor_id` set, Pay will enqueue a background job (EmailSyncJob) to sync the email with the payment processor.
161
-
162
- It's important you set a queue_adapter for this to happen. If you don't, the code will be executed immediately upon user update. [More information here](https://guides.rubyonrails.org/v4.2/active_job_basics.html#backends)
163
-
164
70
  ## Usage
165
71
 
166
- Include the `Pay::Billable` module in the model you want to know about subscriptions.
72
+ The `Pay::Billable` module should be included in the models you want to make payments and subscriptions.
167
73
 
168
74
  ```ruby
169
75
  # app/models/user.rb
@@ -172,6 +78,8 @@ class User < ActiveRecord::Base
172
78
  end
173
79
  ```
174
80
 
81
+ An `email` attribute or method on your `Billable` model is required.
82
+
175
83
  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.
176
84
 
177
85
  ## Configuration
@@ -180,9 +88,6 @@ Need to make some changes to how Pay is used? You can create an initializer `con
180
88
 
181
89
  ```ruby
182
90
  Pay.setup do |config|
183
- config.billable_class = 'User'
184
- config.billable_table = 'users'
185
-
186
91
  config.chargeable_class = 'Pay::Charge'
187
92
  config.chargeable_table = 'pay_charges'
188
93
 
@@ -255,6 +160,7 @@ Emails can be enabled/disabled using the `send_emails` configuration option (ena
255
160
  - When a charge was refunded
256
161
  - When a subscription is about to renew
257
162
 
163
+
258
164
  ## Billable API
259
165
 
260
166
  #### Trials
@@ -310,6 +216,9 @@ You may pass optional arguments that will be directly passed on to
310
216
  either Stripe or Braintree. You can use these options to charge
311
217
  different currencies, etc.
312
218
 
219
+ On failure, a `Pay::Error` will be raised with details about the payment
220
+ failure.
221
+
313
222
  #### Creating a Subscription
314
223
 
315
224
  ```ruby
@@ -330,13 +239,20 @@ def subscribe(name: 'default', plan: 'default', **options)
330
239
  end
331
240
  ```
332
241
 
242
+ For example, you can pass the `quantity` option to subscribe to a plan with for per-seat pricing.
243
+
244
+ ```ruby
245
+
246
+ user.subscribe(name: "default", plan: "default", quantity: 3)
247
+ ```
248
+
333
249
  ##### Name
334
250
 
335
251
  Name is an internally used name for the subscription.
336
252
 
337
253
  ##### Plan
338
254
 
339
- Plan is the plan ID from the payment processor.
255
+ Plan is the plan ID or price ID from the payment processor. For example: `plan_xxxxx` or `price_xxxxx`
340
256
 
341
257
  ##### Options
342
258
 
@@ -555,6 +471,74 @@ If you just want to modify where the engine mounts it's routes then you can chan
555
471
  config.routes_path = '/secret-webhook-path'
556
472
  ```
557
473
 
474
+ ## Payment Providers
475
+
476
+ We support both Stripe and Braintree and make our best attempt to
477
+ standardize the two. They function differently so keep that in mind if
478
+ you plan on doing more complex payments. It would be best to stick with
479
+ a single payment provider in that case so you don't run into
480
+ discrepancies.
481
+
482
+ #### Braintree
483
+
484
+ ```yaml
485
+ development:
486
+ braintree:
487
+ private_key: xxxx
488
+ public_key: yyyy
489
+ merchant_id: zzzz
490
+ environment: sandbox
491
+ ```
492
+
493
+ #### Stripe
494
+
495
+ You'll need to add your private Stripe API key to your Rails secrets `config/secrets.yml`, credentials `rails credentials:edit`
496
+
497
+ ```yaml
498
+ development:
499
+ stripe:
500
+ private_key: xxxx
501
+ public_key: yyyy
502
+ signing_secret: zzzz
503
+ ```
504
+
505
+ You can also use the `STRIPE_PRIVATE_KEY` and `STRIPE_SIGNING_SECRET` environment variables.
506
+
507
+ **To see how to use Stripe Elements JS & Devise, [click here](https://github.com/jasoncharnes/pay/wiki/Using-Stripe-Elements-and-Devise).**
508
+
509
+ ##### Strong Customer Authentication (SCA)
510
+
511
+ Our Stripe integration **requires** the use of Payment Method objects to correctly support Strong Customer Authentication with Stripe. If you've previously been using card tokens, you'll need to upgrade your Javascript integration.
512
+
513
+ Subscriptions that require SCA are marked as `incomplete` by default.
514
+ Once payment is authenticated, Stripe will send a webhook updating the
515
+ status of the subscription. You'll need to use the [Stripe CLI](https://github.com/stripe/stripe-cli) to forward
516
+ webhooks to your application to make sure your subscriptions work
517
+ correctly for SCA payments.
518
+
519
+ ```bash
520
+ stripe listen --forward-to localhost:3000/pay/webhooks/stripe
521
+ ```
522
+
523
+ You should use `stripe.confirmCardSetup` on the client to collect card information anytime you want to save the card and charge them later (adding a card, then charging them on the next page for example). Use `stripe.confirmCardPayment` if you'd like to charge the customer immediately (think checking out of a shopping cart).
524
+
525
+ The Javascript also needs to have a PaymentIntent or SetupIntent created server-side and the ID passed into the Javascript to do this. That way it knows how to safely handle the card tokenization if it meets the SCA requirements.
526
+
527
+ **Payment Confirmations**
528
+
529
+ Sometimes you'll have a payment that requires extra authentication. In this case, Pay provides a webhook and action for handling these payments. It will automatically email the customer and provide a link with the PaymentIntent ID in the url where the customer will be asked to fill out their name and card number to confirm the payment. Once done, they'll be redirected back to your application.
530
+
531
+ If you'd like to change the views of the payment confirmation page, you can install the views using the generator and modify the template.
532
+
533
+ [<img src="https://d1jfzjx68gj8xs.cloudfront.net/items/2s3Z0J3Z3b1J1v2K2O1a/Screen%20Shot%202019-10-10%20at%2012.56.32%20PM.png?X-CloudApp-Visitor-Id=51470" alt="Stripe SCA Payment Confirmation" style="zoom: 25%;" />](https://d1jfzjx68gj8xs.cloudfront.net/items/2s3Z0J3Z3b1J1v2K2O1a/Screen%20Shot%202019-10-10%20at%2012.56.32%20PM.png)
534
+
535
+ #### Background jobs
536
+
537
+ If a user's email is updated and they have a `processor_id` set, Pay will enqueue a background job (EmailSyncJob) to sync the email with the payment processor.
538
+
539
+ It's important you set a queue_adapter for this to happen. If you don't, the code will be executed immediately upon user update. [More information here](https://guides.rubyonrails.org/v4.2/active_job_basics.html#backends)
540
+
541
+
558
542
  ## Contributors
559
543
 
560
544
  - [Jason Charnes](https://twitter.com/jmcharnes)
data/Rakefile CHANGED
@@ -14,27 +14,21 @@ RDoc::Task.new(:rdoc) do |rdoc|
14
14
  rdoc.rdoc_files.include("lib/**/*.rb")
15
15
  end
16
16
 
17
- APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
18
-
17
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
19
18
  load "rails/tasks/engine.rake"
19
+
20
20
  load "rails/tasks/statistics.rake"
21
21
 
22
- require "bundler/gem_tasks"
22
+ unless Rails.env.test?
23
+ require "bundler/gem_tasks"
24
+ end
25
+
23
26
  require "rake/testtask"
24
27
 
25
28
  Rake::TestTask.new(:test) do |t|
26
- t.libs << "lib"
27
29
  t.libs << "test"
28
30
  t.pattern = "test/**/*_test.rb"
29
31
  t.verbose = false
30
32
  end
31
33
 
32
34
  task default: :test
33
-
34
- task :console do
35
- require "irb"
36
- require "irb/completion"
37
- require "pay"
38
- ARGV.clear
39
- IRB.start
40
- end
@@ -16,7 +16,7 @@ module Pay
16
16
  end
17
17
 
18
18
  render json: {success: true}, status: :ok
19
- rescue ::Braintree::InvalidSignature => e
19
+ rescue ::Braintree::InvalidSignature
20
20
  head :ok
21
21
  end
22
22
 
@@ -29,11 +29,11 @@ module Pay
29
29
  pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
30
30
  return unless pay_subscription.present?
31
31
 
32
- user = pay_subscription.owner
33
- charge = user.save_braintree_transaction(subscription.transactions.first)
32
+ billable = pay_subscription.owner
33
+ charge = billable.save_braintree_transaction(subscription.transactions.first)
34
34
 
35
35
  if Pay.send_emails
36
- Pay::UserMailer.receipt(user, charge).deliver_later
36
+ Pay::UserMailer.receipt(billable, charge).deliver_later
37
37
  end
38
38
  end
39
39
 
@@ -41,11 +41,14 @@ module Pay
41
41
  subscription = event.subscription
42
42
  return if subscription.nil?
43
43
 
44
- user = Pay.user_model.find_by(processor: :braintree, processor_id: subscription.id)
45
- return unless user.present?
44
+ pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
45
+ return unless pay_subscription.present?
46
+
47
+ billable = pay_subscription.owner
48
+ return if billable.nil?
46
49
 
47
50
  # User canceled or failed to make payments
48
- user.update(braintree_subscription_id: nil)
51
+ billable.update(braintree_subscription_id: nil)
49
52
  end
50
53
 
51
54
  def subscription_trial_ended(event)
@@ -2,11 +2,11 @@ module Pay
2
2
  class EmailSyncJob < ApplicationJob
3
3
  queue_as :default
4
4
 
5
- def perform(id)
6
- billable = Pay.user_model.find(id)
5
+ def perform(id, class_name)
6
+ billable = class_name.constantize.find(id)
7
7
  billable.sync_email_with_processor
8
8
  rescue ActiveRecord::RecordNotFound
9
- Rails.logger.info "Couldn't find a #{Pay.billable_class} with ID = #{id}"
9
+ Rails.logger.info "Couldn't find a #{class_name} with ID = #{id}"
10
10
  end
11
11
  end
12
12
  end
@@ -9,7 +9,7 @@ module Pay
9
9
 
10
10
  mail(
11
11
  to: to(user),
12
- subject: Pay.email_receipt_subject,
12
+ subject: Pay.email_receipt_subject
13
13
  )
14
14
  end
15
15
 
@@ -18,7 +18,7 @@ module Pay
18
18
 
19
19
  mail(
20
20
  to: to(user),
21
- subject: Pay.email_refund_subject,
21
+ subject: Pay.email_refund_subject
22
22
  )
23
23
  end
24
24
 
@@ -27,7 +27,7 @@ module Pay
27
27
 
28
28
  mail(
29
29
  to: to(user),
30
- subject: Pay.email_renewing_subject,
30
+ subject: Pay.email_renewing_subject
31
31
  )
32
32
  end
33
33
 
@@ -44,8 +44,8 @@ module Pay
44
44
  private
45
45
 
46
46
  def to(user)
47
- if user.respond_to?(:name)
48
- "#{user.name} <#{user.email}>"
47
+ if user.respond_to?(:customer_name)
48
+ "#{user.customer_name} <#{user.email}>"
49
49
  else
50
50
  user.email
51
51
  end
@@ -3,7 +3,7 @@ module Pay
3
3
  self.table_name = Pay.chargeable_table
4
4
 
5
5
  # Associations
6
- belongs_to :owner, class_name: Pay.billable_class, foreign_key: :owner_id
6
+ belongs_to :owner, polymorphic: true
7
7
 
8
8
  # Scopes
9
9
  scope :sorted, -> { order(created_at: :desc) }
@@ -5,7 +5,7 @@ module Pay
5
5
  STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid]
6
6
 
7
7
  # Associations
8
- belongs_to :owner, class_name: Pay.billable_class, foreign_key: :owner_id
8
+ belongs_to :owner, polymorphic: true
9
9
 
10
10
  # Validations
11
11
  validates :name, presence: true
@@ -1,7 +1,8 @@
1
1
  class CreatePaySubscriptions < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table :pay_subscriptions do |t|
4
- t.references :owner
4
+ # Some Billable objects use string as ID, add `type: :string` if needed
5
+ t.references :owner, polymorphic: true
5
6
  t.string :name, null: false
6
7
  t.string :processor, null: false
7
8
  t.string :processor_id, null: false
@@ -1,7 +1,8 @@
1
1
  class CreatePayCharges < ActiveRecord::Migration[4.2]
2
2
  def change
3
3
  create_table :pay_charges do |t|
4
- t.references :owner
4
+ # Some Billable objects use string as ID, add `type: :string` if needed
5
+ t.references :owner, polymorphic: true
5
6
  t.string :processor, null: false
6
7
  t.string :processor_id, null: false
7
8
  t.integer :amount, null: false
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddPayBillableTo<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ change_table :<%= table_name %> do |t|
6
+ <%= migration_data -%>
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pay
4
+ module Generators
5
+ module OrmHelpers
6
+ def model_contents
7
+ buffer = <<-CONTENT
8
+ include Pay::Billable
9
+ CONTENT
10
+ buffer
11
+ end
12
+
13
+ private
14
+
15
+ def model_exists?
16
+ File.exist?(File.join(destination_root, model_path))
17
+ end
18
+
19
+ def migration_exists?(table_name)
20
+ Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_devise_to_#{table_name}.rb$/).first
21
+ end
22
+
23
+ def migration_path
24
+ if Rails.version >= "5.0.3"
25
+ db_migrate_path
26
+ else
27
+ @migration_path ||= File.join("db", "migrate")
28
+ end
29
+ end
30
+
31
+ def model_path
32
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Pay
6
+ module Generators
7
+ class PayGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ namespace "pay"
11
+ source_root File.expand_path("../templates", __FILE__)
12
+
13
+ desc "Generates a migration to add Billable fields to a model."
14
+
15
+ hook_for :orm
16
+ end
17
+ end
18
+ end
data/lib/pay.rb CHANGED
@@ -51,7 +51,23 @@ module Pay
51
51
  yield self
52
52
  end
53
53
 
54
+ def self.billable_models
55
+ Pay::Billable.includers
56
+ end
57
+
58
+ def self.find_billable(processor:, processor_id:)
59
+ billable_models.each do |model|
60
+ if (record = model.find_by(processor: processor, processor_id: processor_id))
61
+ return record
62
+ end
63
+ end
64
+
65
+ nil
66
+ end
67
+
54
68
  def self.user_model
69
+ ActiveSupport::Deprecation.warn("Pay.user_model is deprecated and will be removed in v3. Instead, use `Pay.billable_models` now to support more than one billable model.")
70
+
55
71
  if Rails.application.config.cache_classes
56
72
  @@user_model ||= billable_class.constantize
57
73
  else
@@ -86,6 +102,20 @@ module Pay
86
102
  class Error < StandardError
87
103
  end
88
104
 
105
+ class BraintreeError < Error
106
+ attr_reader :result
107
+
108
+ def initialize(result=nil)
109
+ @result = result
110
+ end
111
+ end
112
+
113
+ class BraintreeAuthorizationError < BraintreeError
114
+ def message
115
+ "Either the data you submitted is malformed and does not match the API or the API key you used may not be authorized to perform this action."
116
+ end
117
+ end
118
+
89
119
  class InvalidPaymentMethod < Error
90
120
  attr_reader :payment
91
121
 
@@ -4,8 +4,21 @@ module Pay
4
4
  module Billable
5
5
  extend ActiveSupport::Concern
6
6
 
7
- included do
7
+ # Keep track of which Billable models we have
8
+ class << self
9
+ attr_reader :includers
10
+ end
11
+
12
+ def self.included(base = nil, &block)
13
+ @includers ||= []
14
+ @includers << base if base
15
+ super
16
+ end
17
+
18
+ included do |base|
8
19
  include Pay::Billable::SyncEmail
20
+ include Pay::Stripe::Billable if defined? ::Stripe
21
+ include Pay::Braintree::Billable if defined? ::Braintree
9
22
 
10
23
  has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner
11
24
  has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner
@@ -112,7 +125,7 @@ module Pay
112
125
  private
113
126
 
114
127
  def check_for_processor
115
- raise StandardError, "No payment processor selected. Make sure to set the #{Pay.billable_class}'s `processor` attribute to either 'stripe' or 'braintree'." unless processor
128
+ raise StandardError, "No payment processor selected. Make sure to set the #{self.class.name}'s `processor` attribute to either 'stripe' or 'braintree'." unless processor
116
129
  end
117
130
 
118
131
  # Used for creating a Pay::Subscription in the database
@@ -125,7 +138,7 @@ module Pay
125
138
  processor_id: subscription.id,
126
139
  processor_plan: plan,
127
140
  trial_ends_at: send("#{processor}_trial_end_date", subscription),
128
- ends_at: nil,
141
+ ends_at: nil
129
142
  )
130
143
  subscriptions.create!(options)
131
144
  end
@@ -32,7 +32,7 @@ module Pay
32
32
  # Only update if the processor id is the same
33
33
  # This prevents duplicate API hits if this is their first time
34
34
  if processor_id? && !saved_change_to_processor_id? && saved_change_to_email?
35
- EmailSyncJob.perform_later(id)
35
+ EmailSyncJob.perform_later(id, self.class.name)
36
36
  end
37
37
  end
38
38
  end
@@ -19,7 +19,7 @@ module Pay
19
19
 
20
20
  Pay.charge_model.include Pay::Braintree::Charge
21
21
  Pay.subscription_model.include Pay::Braintree::Subscription
22
- Pay.user_model.include Pay::Braintree::Billable
22
+ Pay.billable_models.each { |model| model.include Pay::Braintree::Billable }
23
23
  end
24
24
 
25
25
  def public_key
@@ -12,9 +12,9 @@ module Pay
12
12
  email: email,
13
13
  first_name: try(:first_name),
14
14
  last_name: try(:last_name),
15
- payment_method_nonce: card_token,
15
+ payment_method_nonce: card_token
16
16
  )
17
- raise Pay::Error.new(result.message) unless result.success?
17
+ raise BraintreeError.new(result), result.message unless result.success?
18
18
 
19
19
  update(processor: "braintree", processor_id: result.customer.id)
20
20
 
@@ -24,8 +24,10 @@ module Pay
24
24
 
25
25
  result.customer
26
26
  end
27
+ rescue ::Braintree::AuthorizationError => e
28
+ raise BraintreeAuthorizationError
27
29
  rescue ::Braintree::BraintreeError => e
28
- raise Error, e.message
30
+ raise BraintreeError, e.message
29
31
  end
30
32
 
31
33
  # Handles Billable#charge
@@ -35,13 +37,17 @@ module Pay
35
37
  args = {
36
38
  amount: amount / 100.0,
37
39
  customer_id: customer.id,
38
- options: {submit_for_settlement: true},
40
+ options: {submit_for_settlement: true}
39
41
  }.merge(options)
40
42
 
41
43
  result = gateway.transaction.sale(args)
42
- save_braintree_transaction(result.transaction) if result.success?
43
- rescue ::BraintreeError => e
44
- raise Error, e.message
44
+ raise BraintreeError.new(result), result.message unless result.success?
45
+
46
+ save_braintree_transaction(result.transaction)
47
+ rescue ::Braintree::AuthorizationError => e
48
+ raise BraintreeAuthorizationError
49
+ rescue ::Braintree::BraintreeError => e
50
+ raise BraintreeError, e.message
45
51
  end
46
52
 
47
53
  # Handles Billable#subscribe
@@ -62,11 +68,13 @@ module Pay
62
68
  )
63
69
 
64
70
  result = gateway.subscription.create(subscription_options)
65
- raise Pay::Error.new(result.message) unless result.success?
71
+ raise BraintreeError.new(result), result.message unless result.success?
66
72
 
67
73
  create_subscription(result.subscription, "braintree", name, plan, status: :active)
74
+ rescue ::Braintree::AuthorizationError => e
75
+ raise BraintreeAuthorizationError
68
76
  rescue ::Braintree::BraintreeError => e
69
- raise Error, e.message
77
+ raise BraintreeError, e.message
70
78
  end
71
79
 
72
80
  # Handles Billable#update_card
@@ -78,23 +86,25 @@ module Pay
78
86
  payment_method_nonce: token,
79
87
  options: {
80
88
  make_default: true,
81
- verify_card: true,
89
+ verify_card: true
82
90
  }
83
91
  )
84
- raise Pay::Error.new(result.message) unless result.success?
92
+ raise BraintreeError.new(result), result.message unless result.success?
85
93
 
86
94
  update_braintree_card_on_file result.payment_method
87
95
  update_subscriptions_to_payment_method(result.payment_method.token)
88
96
  true
97
+ rescue ::Braintree::AuthorizationError => e
98
+ raise BraintreeAuthorizationError
89
99
  rescue ::Braintree::BraintreeError => e
90
- raise Error, e.message
100
+ raise BraintreeError, e.message
91
101
  end
92
102
 
93
103
  def update_braintree_email!
94
104
  braintree_customer.update(
95
105
  email: email,
96
106
  first_name: try(:first_name),
97
- last_name: try(:last_name),
107
+ last_name: try(:last_name)
98
108
  )
99
109
  end
100
110
 
@@ -171,7 +181,7 @@ module Pay
171
181
  card_type: payment_method.card_type,
172
182
  card_last4: payment_method.last_4,
173
183
  card_exp_month: payment_method.expiration_month,
174
- card_exp_year: payment_method.expiration_year,
184
+ card_exp_year: payment_method.expiration_year
175
185
  }
176
186
 
177
187
  when "paypal_account"
@@ -179,7 +189,7 @@ module Pay
179
189
  card_type: "PayPal",
180
190
  card_last4: transaction.paypal_details.payer_email,
181
191
  card_exp_month: nil,
182
- card_exp_year: nil,
192
+ card_exp_year: nil
183
193
  }
184
194
 
185
195
  when "android_pay_card"
@@ -188,7 +198,7 @@ module Pay
188
198
  card_type: payment_method.source_card_type,
189
199
  card_last4: payment_method.source_card_last_4,
190
200
  card_exp_month: payment_method.expiration_month,
191
- card_exp_year: payment_method.expiration_year,
201
+ card_exp_year: payment_method.expiration_year
192
202
  }
193
203
 
194
204
  when "venmo_account"
@@ -196,7 +206,7 @@ module Pay
196
206
  card_type: "Venmo",
197
207
  card_last4: transaction.venmo_account_details.username,
198
208
  card_exp_month: nil,
199
- card_exp_year: nil,
209
+ card_exp_year: nil
200
210
  }
201
211
 
202
212
  when "apple_pay_card"
@@ -205,7 +215,7 @@ module Pay
205
215
  card_type: payment_method.card_type,
206
216
  card_last4: payment_method.last_4,
207
217
  card_exp_month: payment_method.expiration_month,
208
- card_exp_year: payment_method.expiration_year,
218
+ card_exp_year: payment_method.expiration_year
209
219
  }
210
220
 
211
221
  else
@@ -13,7 +13,7 @@ module Pay
13
13
  update(status: :canceled, ends_at: trial_ends_at)
14
14
  else
15
15
  gateway.subscription.update(subscription.id, {
16
- number_of_billing_cycles: subscription.current_billing_cycle,
16
+ number_of_billing_cycles: subscription.current_billing_cycle
17
17
  })
18
18
  update(status: :canceled, ends_at: subscription.billing_period_end_date.to_date)
19
19
  end
@@ -45,7 +45,7 @@ module Pay
45
45
 
46
46
  gateway.subscription.update(subscription.id, {
47
47
  never_expires: true,
48
- number_of_billing_cycles: nil,
48
+ number_of_billing_cycles: nil
49
49
  })
50
50
  end
51
51
 
@@ -80,8 +80,8 @@ module Pay
80
80
  never_expires: true,
81
81
  number_of_billing_cycles: nil,
82
82
  options: {
83
- prorate_charges: prorate?,
84
- },
83
+ prorate_charges: prorate?
84
+ }
85
85
  })
86
86
 
87
87
  if result.success?
@@ -159,10 +159,10 @@ module Pay
159
159
  {
160
160
  inherited_from_id: "plan-credit",
161
161
  amount: discount.amount,
162
- number_of_billing_cycles: discount.number_of_billing_cycles,
163
- },
164
- ],
165
- },
162
+ number_of_billing_cycles: discount.number_of_billing_cycles
163
+ }
164
+ ]
165
+ }
166
166
  }
167
167
  end
168
168
 
@@ -20,7 +20,7 @@ module Pay
20
20
  company: {
21
21
  name: Pay.business_name,
22
22
  address: Pay.business_address,
23
- email: Pay.support_email,
23
+ email: Pay.support_email
24
24
  },
25
25
  line_items: line_items
26
26
  )
@@ -32,7 +32,7 @@ module Pay
32
32
  ["Account Billed", "#{owner.name} (#{owner.email})"],
33
33
  ["Product", product],
34
34
  ["Amount", ActionController::Base.helpers.number_to_currency(amount / 100.0)],
35
- ["Charged to", charged_to],
35
+ ["Charged to", charged_to]
36
36
  ]
37
37
  line_items << ["Additional Info", owner.extra_billing_info] if owner.extra_billing_info?
38
38
  line_items
@@ -12,11 +12,11 @@ module Pay
12
12
 
13
13
  def setup
14
14
  ::Stripe.api_key = private_key
15
+ ::Stripe.api_version = '2020-08-27'
15
16
  ::StripeEvent.signing_secret = signing_secret
16
17
 
17
18
  Pay.charge_model.include Pay::Stripe::Charge
18
19
  Pay.subscription_model.include Pay::Stripe::Subscription
19
- Pay.user_model.include Pay::Stripe::Billable
20
20
  end
21
21
 
22
22
  def public_key
@@ -17,7 +17,7 @@ module Pay
17
17
  def create_setup_intent
18
18
  ::Stripe::SetupIntent.create(
19
19
  customer: processor_id,
20
- usage: :off_session,
20
+ usage: :off_session
21
21
  )
22
22
  end
23
23
 
@@ -32,7 +32,7 @@ module Pay
32
32
  confirmation_method: :automatic,
33
33
  currency: "usd",
34
34
  customer: customer.id,
35
- payment_method: customer.invoice_settings.default_payment_method,
35
+ payment_method: customer.invoice_settings.default_payment_method
36
36
  }.merge(options)
37
37
 
38
38
  payment_intent = ::Stripe::PaymentIntent.create(args)
@@ -48,17 +48,20 @@ module Pay
48
48
  #
49
49
  # Returns Pay::Subscription
50
50
  def create_stripe_subscription(name, plan, options = {})
51
+ quantity = options.delete(:quantity) || 1
51
52
  opts = {
52
53
  expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
53
- items: [plan: plan],
54
- off_session: true,
54
+ items: [plan: plan, quantity: quantity],
55
+ off_session: true
55
56
  }.merge(options)
56
57
 
57
58
  # Inherit trial from plan unless trial override was specified
58
59
  opts[:trial_from_plan] = true unless opts[:trial_period_days]
59
60
 
60
- stripe_sub = customer.subscriptions.create(opts)
61
- subscription = create_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status)
61
+ opts[:customer] = stripe_customer.id
62
+
63
+ stripe_sub = ::Stripe::Subscription.create(opts)
64
+ subscription = create_subscription(stripe_sub, "stripe", name, plan, status: stripe_sub.status, quantity: quantity)
62
65
 
63
66
  # No trial, card requires SCA
64
67
  if subscription.incomplete?
@@ -37,7 +37,7 @@ module Pay
37
37
  subscription = processor_subscription
38
38
  subscription.cancel_at_period_end = false
39
39
  subscription.plan = plan
40
- subscription.prorate = prorate
40
+ subscription.proration_behavior = (prorate ? 'create_prorations' : 'none')
41
41
  subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
42
42
  subscription.quantity = quantity if quantity?
43
43
  subscription.save
@@ -1,5 +1,5 @@
1
1
  require "stripe_event"
2
- Dir[File.join(__dir__, "webhooks", "**", "*.rb")].each { |file| require file }
2
+ Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
3
3
 
4
4
  StripeEvent.configure do |events|
5
5
  # Listen to the charge event to make sure we get non-subscription
@@ -4,23 +4,20 @@ module Pay
4
4
  class ChargeSucceeded
5
5
  def call(event)
6
6
  object = event.data.object
7
- user = Pay.user_model.find_by(
8
- processor: :stripe,
9
- processor_id: object.customer
10
- )
7
+ billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
11
8
 
12
- return unless user.present?
13
- return if user.charges.where(processor_id: object.id).any?
9
+ return unless billable.present?
10
+ return if billable.charges.where(processor_id: object.id).any?
14
11
 
15
- charge = create_charge(user, object)
16
- notify_user(user, charge)
12
+ charge = create_charge(billable, object)
13
+ notify_user(billable, charge)
17
14
  charge
18
15
  end
19
16
 
20
17
  def create_charge(user, object)
21
18
  charge = user.charges.find_or_initialize_by(
22
19
  processor: :stripe,
23
- processor_id: object.id,
20
+ processor_id: object.id
24
21
  )
25
22
 
26
23
  charge.update(
@@ -4,23 +4,23 @@ module Pay
4
4
  class CustomerDeleted
5
5
  def call(event)
6
6
  object = event.data.object
7
- user = Pay.user_model.find_by(processor: :stripe, processor_id: object.id)
7
+ billable = Pay.find_billable(processor: :stripe, processor_id: object.id)
8
8
 
9
9
  # Couldn't find user, we can skip
10
- return unless user.present?
10
+ return unless billable.present?
11
11
 
12
- user.update(
12
+ billable.update(
13
13
  processor_id: nil,
14
14
  trial_ends_at: nil,
15
15
  card_type: nil,
16
16
  card_last4: nil,
17
17
  card_exp_month: nil,
18
- card_exp_year: nil,
18
+ card_exp_year: nil
19
19
  )
20
20
 
21
- user.subscriptions.update_all(
21
+ billable.subscriptions.update_all(
22
22
  trial_ends_at: nil,
23
- ends_at: Time.zone.now,
23
+ ends_at: Time.zone.now
24
24
  )
25
25
  end
26
26
  end
@@ -4,12 +4,12 @@ module Pay
4
4
  class CustomerUpdated
5
5
  def call(event)
6
6
  object = event.data.object
7
- user = Pay.user_model.find_by(processor: :stripe, processor_id: object.id)
7
+ billable = Pay.find_billable(processor: :stripe, processor_id: object.id)
8
8
 
9
9
  # Couldn't find user, we can skip
10
- return unless user.present?
10
+ return unless billable.present?
11
11
 
12
- user.sync_card_from_stripe
12
+ billable.sync_card_from_stripe
13
13
  end
14
14
  end
15
15
  end
@@ -4,12 +4,12 @@ module Pay
4
4
  class PaymentMethodUpdated
5
5
  def call(event)
6
6
  object = event.data.object
7
- user = Pay.user_model.find_by(processor: :stripe, processor_id: object.customer)
7
+ billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
8
 
9
9
  # Couldn't find user, we can skip
10
- return unless user.present?
10
+ return unless billable.present?
11
11
 
12
- user.sync_card_from_stripe
12
+ billable.sync_card_from_stripe
13
13
  end
14
14
  end
15
15
  end
@@ -8,12 +8,15 @@ module Pay
8
8
  # We may already have the subscription in the database, so we can update that record
9
9
  subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
10
10
 
11
+ # Create the subscription in the database if we don't have it already
11
12
  if subscription.nil?
12
13
  # The customer should already be in the database
13
- owner = Pay.user_model.find_by(processor: :stripe, processor_id: object.customer)
14
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
14
15
 
15
- Rails.logger.error("[Pay] Unable to find #{Pay.user_model} with processor: :stripe and processor_id: '#{object.customer}'")
16
- return if owner.nil?
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
17
20
 
18
21
  subscription = Pay.subscription_model.new(owner: owner)
19
22
  end
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "2.0.3"
2
+ VERSION = "2.2.0"
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.0.3
4
+ version: 2.2.0
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: 2020-03-10 00:00:00.000000000 Z
12
+ date: 2020-09-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -73,20 +73,6 @@ dependencies:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: '2.3'
76
- - !ruby/object:Gem::Dependency
77
- name: bundler
78
- requirement: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- type: :development
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
76
  - !ruby/object:Gem::Dependency
91
77
  name: byebug
92
78
  requirement: !ruby/object:Gem::Requirement
@@ -233,10 +219,13 @@ files:
233
219
  - config/locales/en.yml
234
220
  - config/routes.rb
235
221
  - db/migrate/20170205020145_create_pay_subscriptions.rb
236
- - db/migrate/20170503131610_add_fields_to_billable.rb
237
222
  - db/migrate/20170727235816_create_pay_charges.rb
238
223
  - db/migrate/20190816015720_add_status_to_pay_subscriptions.rb
224
+ - lib/generators/active_record/pay_generator.rb
225
+ - lib/generators/active_record/templates/migration.rb
239
226
  - lib/generators/pay/email_views_generator.rb
227
+ - lib/generators/pay/orm_helpers.rb
228
+ - lib/generators/pay/pay_generator.rb
240
229
  - lib/generators/pay/views_generator.rb
241
230
  - lib/pay.rb
242
231
  - lib/pay/billable.rb
@@ -284,7 +273,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
284
273
  - !ruby/object:Gem::Version
285
274
  version: '0'
286
275
  requirements: []
287
- rubygems_version: 3.0.3
276
+ rubygems_version: 3.1.2
288
277
  signing_key:
289
278
  specification_version: 4
290
279
  summary: A Ruby on Rails subscription engine.
@@ -1,16 +0,0 @@
1
- class AddFieldsToBillable < ActiveRecord::Migration[4.2]
2
- def change
3
- unless ActiveRecord::Base.connection.table_exists?(Pay.billable_table)
4
- create_table Pay.billable_table.to_sym
5
- end
6
-
7
- add_column Pay.billable_table, :processor, :string
8
- add_column Pay.billable_table, :processor_id, :string
9
- add_column Pay.billable_table, :trial_ends_at, :datetime
10
- add_column Pay.billable_table, :card_type, :string
11
- add_column Pay.billable_table, :card_last4, :string
12
- add_column Pay.billable_table, :card_exp_month, :string
13
- add_column Pay.billable_table, :card_exp_year, :string
14
- add_column Pay.billable_table, :extra_billing_info, :text
15
- end
16
- end