pay 0.0.2 → 1.0.0.beta2

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 (51) hide show
  1. checksums.yaml +5 -5
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +256 -29
  4. data/Rakefile +1 -6
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +56 -0
  6. data/app/jobs/pay/email_sync_job.rb +12 -0
  7. data/app/mailers/pay/user_mailer.rb +42 -0
  8. data/app/models/pay/charge.rb +31 -0
  9. data/app/models/pay/subscription.rb +77 -0
  10. data/app/views/pay/user_mailer/receipt.html.erb +20 -0
  11. data/app/views/pay/user_mailer/refund.html.erb +21 -0
  12. data/app/views/pay/user_mailer/subscription_renewing.html.erb +6 -0
  13. data/config/routes.rb +3 -1
  14. data/db/migrate/20170205020145_create_subscriptions.rb +1 -1
  15. data/db/migrate/20170503131610_add_fields_to_users.rb +3 -2
  16. data/db/migrate/20170727235816_create_charges.rb +17 -0
  17. data/lib/generators/pay/email_views_generator.rb +13 -0
  18. data/lib/pay.rb +65 -1
  19. data/lib/pay/billable.rb +54 -24
  20. data/lib/pay/billable/sync_email.rb +41 -0
  21. data/lib/pay/braintree.rb +16 -0
  22. data/lib/pay/braintree/api.rb +30 -0
  23. data/lib/pay/braintree/billable.rb +219 -0
  24. data/lib/pay/braintree/charge.rb +27 -0
  25. data/lib/pay/braintree/subscription.rb +173 -0
  26. data/lib/pay/engine.rb +14 -1
  27. data/lib/pay/receipts.rb +37 -0
  28. data/lib/pay/stripe.rb +17 -0
  29. data/lib/pay/stripe/api.rb +13 -0
  30. data/lib/pay/stripe/billable.rb +143 -0
  31. data/lib/pay/stripe/charge.rb +30 -0
  32. data/lib/pay/stripe/subscription.rb +48 -0
  33. data/lib/pay/stripe/webhooks.rb +39 -0
  34. data/lib/pay/stripe/webhooks/charge_refunded.rb +25 -0
  35. data/lib/pay/stripe/webhooks/charge_succeeded.rb +47 -0
  36. data/lib/pay/stripe/webhooks/customer_deleted.rb +31 -0
  37. data/lib/pay/stripe/webhooks/customer_updated.rb +19 -0
  38. data/lib/pay/stripe/webhooks/source_deleted.rb +19 -0
  39. data/lib/pay/stripe/webhooks/subscription_created.rb +46 -0
  40. data/lib/pay/stripe/webhooks/subscription_deleted.rb +21 -0
  41. data/lib/pay/stripe/webhooks/subscription_renewing.rb +25 -0
  42. data/lib/pay/stripe/webhooks/subscription_updated.rb +35 -0
  43. data/lib/pay/version.rb +1 -1
  44. metadata +124 -30
  45. data/app/models/subscription.rb +0 -59
  46. data/config/initializers/pay.rb +0 -3
  47. data/config/initializers/stripe.rb +0 -1
  48. data/db/development.sqlite3 +0 -0
  49. data/lib/pay/billable/braintree.rb +0 -57
  50. data/lib/pay/billable/stripe.rb +0 -47
  51. data/lib/tasks/pay_tasks.rake +0 -4
data/Rakefile CHANGED
@@ -15,12 +15,11 @@ RDoc::Task.new(:rdoc) do |rdoc|
15
15
  end
16
16
 
17
17
  APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
18
- load 'rails/tasks/engine.rake'
19
18
 
19
+ load 'rails/tasks/engine.rake'
20
20
  load 'rails/tasks/statistics.rake'
21
21
 
22
22
  require 'bundler/gem_tasks'
23
-
24
23
  require 'rake/testtask'
25
24
 
26
25
  Rake::TestTask.new(:test) do |t|
@@ -39,7 +38,3 @@ task :console do
39
38
  ARGV.clear
40
39
  IRB.start
41
40
  end
42
-
43
- require 'rubocop/rake_task'
44
-
45
- RuboCop::RakeTask.new
@@ -0,0 +1,56 @@
1
+ module Pay
2
+ module Webhooks
3
+ class BraintreeController < Pay::ApplicationController
4
+ if Rails.application.config.action_controller.default_protect_from_forgery
5
+ skip_before_action :verify_authenticity_token
6
+ end
7
+
8
+ def create
9
+ case verified_event.kind
10
+ when "subscription_charged_successfully"
11
+ subscription_charged_successfully(verified_event)
12
+ when "subscription_canceled"
13
+ subscription_canceled(verified_event)
14
+ end
15
+
16
+ render json: {success: true}
17
+ rescue Braintree::InvalidSignature => e
18
+ head :ok
19
+ end
20
+
21
+ private
22
+
23
+ def subscription_charged_successfully(event)
24
+ subscription = event.subscription
25
+ return if subscription.nil?
26
+
27
+ user = Pay.user_model.find_by(processor: :braintree, processor_id: subscription.id)
28
+ return unless user.present?
29
+
30
+ charge = user.save_braintree_transaction(subscription.transactions.first)
31
+
32
+ if Pay.send_emails
33
+ Pay::UserMailer.receipt(user, charge).deliver_later
34
+ end
35
+ end
36
+
37
+ def subscription_canceled(event)
38
+ subscription = event.subscription
39
+ return if subscription.nil?
40
+
41
+ user = Pay.user_model.find_by(processor: :braintree, processor_id: subscription.id)
42
+ return unless user.present?
43
+
44
+ # User canceled or failed to make payments
45
+ user.update(braintree_subscription_id: nil)
46
+ end
47
+
48
+ def verified_webhook
49
+ @webhook_notification ||= Braintree::WebhookNotification.parse(
50
+ params[:bt_signature],
51
+ params[:bt_payload]
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ module Pay
2
+ class EmailSyncJob < ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform(id)
6
+ billable = Pay.user_model.find(id)
7
+ billable.sync_email_with_processor
8
+ rescue ActiveRecord::RecordNotFound
9
+ Rails.logger.info "Couldn't find a #{Pay.billable_class} with ID = #{id}"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ module Pay
2
+ class UserMailer < ApplicationMailer
3
+ def receipt(user, charge)
4
+ @user, @charge = user, charge
5
+
6
+ if charge.respond_to? :receipt
7
+ attachments[charge.filename] = charge.receipt
8
+ end
9
+
10
+ mail(
11
+ to: to(user),
12
+ subject: Pay.email_receipt_subject,
13
+ )
14
+ end
15
+
16
+ def refund(user, charge)
17
+ @user, @charge = user, charge
18
+
19
+ mail(
20
+ to: to(user),
21
+ subject: Pay.email_refund_subject,
22
+ )
23
+ end
24
+
25
+ def subscription_renewing(user, subscription)
26
+ mail(
27
+ to: to(user),
28
+ subject: Pay.email_renewing_subject,
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ def to(user)
35
+ if user.respond_to?(:name)
36
+ "#{user.name} <#{user.email}>"
37
+ else
38
+ user.email
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ module Pay
2
+ class Charge < ApplicationRecord
3
+ self.table_name = Pay.chargeable_table
4
+
5
+ # Associations
6
+ belongs_to :owner, class_name: Pay.billable_class, foreign_key: :owner_id
7
+
8
+ # Scopes
9
+ scope :sorted, -> { order(created_at: :desc) }
10
+ default_scope -> { sorted }
11
+
12
+ # Validations
13
+ validates :amount, presence: true
14
+ validates :processor, presence: true
15
+ validates :processor_id, presence: true
16
+ validates :card_type, presence: true
17
+
18
+ def processor_charge
19
+ send("#{processor}_charge")
20
+ end
21
+
22
+ def refund!(refund_amount = nil)
23
+ refund_amount ||= amount
24
+ send("#{processor}_refund!", refund_amount)
25
+ end
26
+
27
+ def charged_to
28
+ "#{card_type} (**** **** **** #{card_last4})"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,77 @@
1
+ module Pay
2
+ class Subscription < ApplicationRecord
3
+ self.table_name = Pay.subscription_table
4
+
5
+ # Associations
6
+ belongs_to :owner, class_name: Pay.billable_class, foreign_key: :owner_id
7
+
8
+ # Validations
9
+ validates :name, presence: true
10
+ validates :processor, presence: true
11
+ validates :processor_id, presence: true
12
+ validates :processor_plan, presence: true
13
+ validates :quantity, presence: true
14
+
15
+ # Scopes
16
+ scope :for_name, ->(name) { where(name: name) }
17
+ scope :on_trial, ->{ where.not(trial_ends_at: nil).where("? < trial_ends_at", Time.zone.now) }
18
+ scope :cancelled, ->{ where.not(ends_at: nil) }
19
+ scope :on_grace_period, ->{ cancelled.where("? < ends_at", Time.zone.now) }
20
+ scope :active, ->{ where(ends_at: nil).or(on_grace_period).or(on_trial) }
21
+
22
+ attribute :prorate, :boolean, default: true
23
+
24
+ def no_prorate
25
+ self.prorate = false
26
+ end
27
+
28
+ def skip_trial
29
+ self.trial_ends_at = nil
30
+ end
31
+
32
+ def on_trial?
33
+ trial_ends_at? && Time.zone.now < trial_ends_at
34
+ end
35
+
36
+ def cancelled?
37
+ ends_at?
38
+ end
39
+
40
+ def on_grace_period?
41
+ cancelled? && Time.zone.now < ends_at
42
+ end
43
+
44
+ def active?
45
+ ends_at.nil? || on_grace_period? || on_trial?
46
+ end
47
+
48
+ def cancel
49
+ send("#{processor}_cancel")
50
+ end
51
+
52
+ def cancel_now!
53
+ send("#{processor}_cancel_now!")
54
+ end
55
+
56
+ def resume
57
+ unless on_grace_period?
58
+ raise StandardError,
59
+ 'You can only resume subscriptions within their grace period.'
60
+ end
61
+
62
+ send("#{processor}_resume")
63
+
64
+ update(ends_at: nil)
65
+ self
66
+ end
67
+
68
+ def swap(plan)
69
+ send("#{processor}_swap", plan)
70
+ update(processor_plan: plan, ends_at: nil)
71
+ end
72
+
73
+ def processor_subscription
74
+ owner.processor_subscription(processor_id)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,20 @@
1
+ We received payment for your subscription. Thanks for your business!<br/>
2
+ <br/>
3
+ Questions? Please reply to this email.<br/>
4
+ <br/>
5
+ ------------------------------------<br/>
6
+ RECEIPT - SUBSCRIPTION<br/>
7
+ <br/>
8
+ Amount: USD <%= ActionController::Base.helpers.number_to_currency(@charge.amount / 100.0) %><br/>
9
+ <br/>
10
+ Charged to: <%= @charge.charged_to %><br/>
11
+ Transaction ID: <%= @charge.id %><br/>
12
+ Date: <%= @charge.created_at %><br/>
13
+ <% if @charge.owner.extra_billing_info? %>
14
+ <%= @charge.owner.extra_billing_info %><br/>
15
+ <% end %>
16
+ <br/>
17
+ <br/>
18
+ <%= Pay.business_name %><br/>
19
+ <%= simple_format Pay.business_address %>
20
+ ------------------------------------<br/>
@@ -0,0 +1,21 @@
1
+ We have processed your refund.<br/>
2
+ Please allow up to 7 business days for your refund to appear in your account<br/>
3
+ <br/>
4
+ Questions? Please reply to this email.<br/>
5
+ <br/>
6
+ ------------------------------------<br/>
7
+ RECEIPT - REFUND<br/>
8
+ <br/>
9
+ Amount: USD <%= ActionController::Base.helpers.number_to_currency(@charge.amount / 100.0) %><br/>
10
+ <br/>
11
+ Refunded to: <%= @charge.charged_to %><br/>
12
+ Transaction ID: <%= @charge.id %><br/>
13
+ Date: <%= @charge.created_at %><br/>
14
+ <% if @charge.owner.extra_billing_info? %>
15
+ <%= @charge.owner.extra_billing_info %><br/>
16
+ <% end %>
17
+ <br/>
18
+ <br/>
19
+ <%= Pay.business_name %><br/>
20
+ <%= simple_format Pay.business_address %>
21
+ ------------------------------------<br/>
@@ -0,0 +1,6 @@
1
+ <h3>Your upcoming subscription renewal</h3>
2
+ <p>This is just a friendly reminder that your <%= Pay.business_name %> subscription will renew automatically on <%= @subscription %>.</p>
3
+
4
+ <p>You may manage your subscription via your account. If you have any questions, please hit reply and let us know.</p>
5
+
6
+ <p>- The <%= Pay.business_name %> Team</p>
@@ -1,2 +1,4 @@
1
- Pay::Engine.routes.draw do
1
+ Rails.application.routes.draw do
2
+ post '/webhooks/stripe', to: 'stripe_event/webhook#event'
3
+ post '/webhooks/braintree', to: 'pay/webhooks/braintree#create'
2
4
  end
@@ -1,6 +1,6 @@
1
1
  class CreateSubscriptions < ActiveRecord::Migration[4.2]
2
2
  def change
3
- create_table :subscriptions do |t|
3
+ create_table :pay_subscriptions do |t|
4
4
  t.references :owner
5
5
  t.string :name, null: false
6
6
  t.string :processor, null: false
@@ -6,10 +6,11 @@ class AddFieldsToUsers < ActiveRecord::Migration[4.2]
6
6
 
7
7
  add_column Pay.billable_table, :processor, :string
8
8
  add_column Pay.billable_table, :processor_id, :string
9
- add_column Pay.billable_table, :card_token, :string
10
- add_column Pay.billable_table, :card_brand, :string
9
+ add_column Pay.billable_table, :trial_ends_at, :datetime
10
+ add_column Pay.billable_table, :card_type, :string
11
11
  add_column Pay.billable_table, :card_last4, :string
12
12
  add_column Pay.billable_table, :card_exp_month, :string
13
13
  add_column Pay.billable_table, :card_exp_year, :string
14
+ add_column Pay.billable_table, :extra_billing_info, :text
14
15
  end
15
16
  end
@@ -0,0 +1,17 @@
1
+ class CreateCharges < ActiveRecord::Migration[5.1]
2
+ def change
3
+ create_table :pay_charges do |t|
4
+ t.references :owner
5
+ t.string :processor, null: false
6
+ t.string :processor_id, null: false
7
+ t.integer :amount, null: false
8
+ t.integer :amount_refunded
9
+ t.string :card_type
10
+ t.string :card_last4
11
+ t.string :card_exp_month
12
+ t.string :card_exp_year
13
+
14
+ t.timestamps
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module Pay
4
+ module Generators
5
+ class EmailViewsGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("../../../..", __FILE__)
7
+
8
+ def copy_views
9
+ directory 'app/views/pay/user_mailer', 'app/views/pay/user_mailer'
10
+ end
11
+ end
12
+ end
13
+ end
data/lib/pay.rb CHANGED
@@ -1,15 +1,79 @@
1
- require 'stripe'
2
1
  require 'pay/engine'
3
2
  require 'pay/billable'
3
+ require 'pay/receipts'
4
4
 
5
5
  module Pay
6
6
  # Define who owns the subscription
7
7
  mattr_accessor :billable_class
8
8
  mattr_accessor :billable_table
9
+ mattr_accessor :braintree_gateway
10
+
9
11
  @@billable_class = 'User'
10
12
  @@billable_table = @@billable_class.tableize
11
13
 
14
+ mattr_accessor :chargeable_class
15
+ mattr_accessor :chargeable_table
16
+ @@chargeable_class = 'Pay::Charge'
17
+ @@chargeable_table = 'pay_charges'
18
+
19
+ mattr_accessor :subscription_class
20
+ mattr_accessor :subscription_table
21
+ @@subscription_class = 'Pay::Subscription'
22
+ @@subscription_table = 'pay_subscriptions'
23
+
24
+ # Business details for receipts
25
+ mattr_accessor :application_name
26
+ mattr_accessor :business_address
27
+ mattr_accessor :business_name
28
+ mattr_accessor :support_email
29
+
30
+ # Email configuration
31
+ mattr_accessor :send_emails
32
+ @@send_emails = true
33
+
34
+ mattr_accessor :email_receipt_subject
35
+ @@email_receipt_subject = 'Payment receipt'
36
+ mattr_accessor :email_refund_subject
37
+ @@email_refund_subject = 'Payment refunded'
38
+ mattr_accessor :email_renewing_subject
39
+ @@email_renewing_subject = 'Your upcoming subscription renewal'
40
+
12
41
  def self.setup
13
42
  yield self
14
43
  end
44
+
45
+ def self.user_model
46
+ if Rails.application.config.cache_classes
47
+ @@user_model ||= billable_class.constantize
48
+ else
49
+ billable_class.constantize
50
+ end
51
+ end
52
+
53
+ def self.charge_model
54
+ if Rails.application.config.cache_classes
55
+ @@charge_model ||= chargeable_class.constantize
56
+ else
57
+ chargeable_class.constantize
58
+ end
59
+ end
60
+
61
+ def self.subscription_model
62
+ if Rails.application.config.cache_classes
63
+ @@subscription_model ||= subscription_class.constantize
64
+ else
65
+ subscription_class.constantize
66
+ end
67
+ end
68
+
69
+ def self.receipts_supported?
70
+ charge_model.respond_to?(:receipt) &&
71
+ application_name.present? &&
72
+ business_name &&
73
+ business_address &&
74
+ support_email
75
+ end
76
+
77
+ class Error < StandardError
78
+ end
15
79
  end
@@ -1,58 +1,92 @@
1
- require 'pay/billable/stripe'
2
- require 'pay/billable/braintree'
1
+ require 'pay/billable/sync_email'
3
2
 
4
3
  module Pay
5
4
  module Billable
6
5
  extend ActiveSupport::Concern
7
6
 
8
7
  included do
9
- include Pay::Billable::Stripe
10
- include Pay::Billable::Braintree
8
+ include Pay::Billable::SyncEmail
11
9
 
12
- has_many :subscriptions, foreign_key: :owner_id
10
+ has_many :charges, class_name: Pay.chargeable_class, foreign_key: :owner_id, inverse_of: :owner
11
+ has_many :subscriptions, class_name: Pay.subscription_class, foreign_key: :owner_id, inverse_of: :owner
13
12
 
14
13
  attribute :plan, :string
15
14
  attribute :quantity, :integer
16
15
  attribute :card_token, :string
17
16
  end
18
17
 
19
- def customer(token = nil)
18
+ def customer
20
19
  check_for_processor
21
- send("#{processor}_customer", token)
20
+ raise Pay::Error, "Email is required to create a customer" if email.nil?
21
+
22
+ customer = send("#{processor}_customer")
23
+ update_card(card_token) if card_token.present?
24
+ customer
22
25
  end
23
26
 
24
- def subscribe(name = 'default', plan = 'default', processor = 'stripe')
25
- self.processor = processor
26
- send("create_#{processor}_subscription", name, plan)
27
+ def customer_name
28
+ [try(:first_name), try(:last_name)].compact.join(" ")
29
+ end
30
+
31
+ def charge(amount_in_cents, options = {})
32
+ check_for_processor
33
+ send("create_#{processor}_charge", amount_in_cents, options)
34
+ end
35
+
36
+ def subscribe(name: 'default', plan: 'default', **options)
37
+ check_for_processor
38
+ send("create_#{processor}_subscription", name, plan, options)
27
39
  end
28
40
 
29
41
  def update_card(token)
30
42
  check_for_processor
43
+ customer if processor_id.nil?
31
44
  send("update_#{processor}_card", token)
32
45
  end
33
46
 
47
+ def on_trial?(name: 'default', plan: nil)
48
+ return true if default_generic_trial?(name, plan)
49
+
50
+ sub = subscription(name: name)
51
+ return sub && sub.on_trial? if plan.nil?
52
+
53
+ sub && sub.on_trial? && sub.processor_plan == plan
54
+ end
55
+
56
+ def on_generic_trial?
57
+ trial_ends_at? && trial_ends_at > Time.zone.now
58
+ end
59
+
34
60
  def processor_subscription(subscription_id)
35
61
  check_for_processor
36
62
  send("#{processor}_subscription", subscription_id)
37
63
  end
38
64
 
39
- def subscribed?(name = 'default', plan = nil)
40
- subscription = subscription(name)
65
+ def subscribed?(name: 'default', processor_plan: nil)
66
+ subscription = subscription(name: name)
41
67
 
42
68
  return false if subscription.nil?
43
- return subscription.active? if plan.nil?
69
+ return subscription.active? if processor_plan.nil?
44
70
 
45
- subscription.active? && subscription.plan == plan
71
+ subscription.active? && subscription.processor_plan == processor_plan
46
72
  end
47
73
 
48
- def subscription(name = 'default')
74
+ def subscription(name: 'default')
49
75
  subscriptions.for_name(name).last
50
76
  end
51
77
 
78
+ def invoice!
79
+ send("#{processor}_invoice!")
80
+ end
81
+
82
+ def upcoming_invoice
83
+ send("#{processor}_upcoming_invoice")
84
+ end
85
+
52
86
  private
53
87
 
54
88
  def check_for_processor
55
- raise StandardError, 'No processor selected' unless processor
89
+ raise StandardError, "No payment processor selected. Make sure to set the #{Pay.billable_class}'s `processor` attribute to either 'stripe' or 'braintree'." unless processor
56
90
  end
57
91
 
58
92
  def create_subscription(subscription, processor, name, plan, qty = 1)
@@ -61,19 +95,15 @@ module Pay
61
95
  processor: processor,
62
96
  processor_id: subscription.id,
63
97
  processor_plan: plan,
64
- trial_ends_at: trial_end_date(subscription),
98
+ trial_ends_at: send("#{processor}_trial_end_date", subscription),
65
99
  quantity: qty,
66
100
  ends_at: nil
67
101
  )
68
102
  end
69
103
 
70
- def update_card_on_file(card)
71
- update!(
72
- card_brand: card.brand,
73
- card_last4: card.last4,
74
- card_exp_month: card.exp_month,
75
- card_exp_year: card.exp_year
76
- )
104
+ def default_generic_trial?(name, plan)
105
+ # Generic trials don't have plans or custom names
106
+ plan.nil? && name == 'default' && on_generic_trial?
77
107
  end
78
108
  end
79
109
  end