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
@@ -1,5 +1,18 @@
1
1
  module Pay
2
2
  class Engine < ::Rails::Engine
3
- isolate_namespace Pay
3
+ engine_name 'pay'
4
+
5
+ initializer 'pay.processors' do
6
+ # Include processor backends
7
+ require 'pay/stripe' if defined? ::Stripe
8
+ require 'pay/braintree' if defined? ::Braintree
9
+ end
10
+
11
+ config.to_prepare do
12
+ Pay::Stripe.setup if defined? ::Stripe
13
+ Pay::Braintree.setup if defined? ::Braintree
14
+
15
+ Pay.charge_model.include Pay::Receipts if defined? Receipts::Receipt
16
+ end
4
17
  end
5
18
  end
@@ -0,0 +1,37 @@
1
+ module Pay
2
+ module Receipts
3
+ def filename
4
+ "receipt-#{created_at.strftime('%Y-%m-%d')}.pdf"
5
+ end
6
+
7
+ # Must return a file object
8
+ def receipt
9
+ receipt_pdf.render
10
+ end
11
+
12
+ def receipt_pdf
13
+ Receipts::Receipt.new(
14
+ id: id,
15
+ product: Pay.config.application_name,
16
+ company: {
17
+ name: Pay.config.business_name,
18
+ address: Pay.config.business_address,
19
+ email: Pay.config.support_email,
20
+ },
21
+ line_items: line_items
22
+ )
23
+ end
24
+
25
+ def line_items
26
+ line_items = [
27
+ ["Date", created_at.to_s],
28
+ ["Account Billed", "#{owner.name} (#{owner.email})"],
29
+ ["Product", Pay.config.application_name],
30
+ ["Amount", ActionController::Base.helpers.number_to_currency(amount / 100.0)],
31
+ ["Charged to", charged_to],
32
+ ]
33
+ line_items << ["Additional Info", owner.extra_billing_info] if owner.extra_billing_info?
34
+ line_items
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,17 @@
1
+ require 'pay/stripe/api'
2
+ require 'pay/stripe/billable'
3
+ require 'pay/stripe/charge'
4
+ require 'pay/stripe/subscription'
5
+ require 'pay/stripe/webhooks'
6
+
7
+ module Pay
8
+ module Stripe
9
+ def self.setup
10
+ Pay::Stripe::Api.set_api_keys
11
+
12
+ Pay.charge_model.include Pay::Stripe::Charge
13
+ Pay.subscription_model.include Pay::Stripe::Subscription
14
+ Pay.user_model.include Pay::Stripe::Billable
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Stripe
3
+ module Api
4
+ def self.set_api_keys
5
+ env = Rails.env.to_sym
6
+ secrets = Rails.application.secrets
7
+ credentials = Rails.application.credentials
8
+
9
+ ::Stripe.api_key = ENV["STRIPE_PRIVATE_KEY"] || secrets.dig(env, :stripe, :private_key) || credentials.dig(env, :stripe, :private_key)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,143 @@
1
+ module Pay
2
+ module Stripe
3
+ module Billable
4
+ # Handles Billable#customer
5
+ #
6
+ # Returns Stripe::Customer
7
+ def stripe_customer
8
+ if processor_id?
9
+ ::Stripe::Customer.retrieve(processor_id)
10
+ else
11
+ create_stripe_customer
12
+ end
13
+ rescue ::Stripe::StripeError => e
14
+ raise Error, e.message
15
+ end
16
+
17
+ # Handles Billable#charge
18
+ #
19
+ # Returns Pay::Charge
20
+ def create_stripe_charge(amount, options={})
21
+ args = {
22
+ amount: amount,
23
+ currency: 'usd',
24
+ customer: customer.id,
25
+ description: customer_name,
26
+ }.merge(options)
27
+
28
+ stripe_charge = ::Stripe::Charge.create(args)
29
+
30
+ # Save the charge to the db, returns Charge
31
+ Pay::Stripe::Webhooks::ChargeSucceeded.new.create_charge(self, stripe_charge)
32
+ rescue ::Stripe::StripeError => e
33
+ raise Error, e.message
34
+ end
35
+
36
+ # Handles Billable#subscribe
37
+ #
38
+ # Returns Pay::Subscription
39
+ def create_stripe_subscription(name, plan, options={})
40
+ opts = { plan: plan, trial_from_plan: true }.merge(options)
41
+ stripe_sub = customer.subscriptions.create(opts)
42
+ subscription = create_subscription(stripe_sub, 'stripe', name, plan)
43
+ subscription
44
+ rescue ::Stripe::StripeError => e
45
+ raise Error, e.message
46
+ end
47
+
48
+ # Handles Billable#update_card
49
+ #
50
+ # Returns true if successful
51
+ def update_stripe_card(token)
52
+ customer = stripe_customer
53
+ token = ::Stripe::Token.retrieve(token)
54
+
55
+ return if token.card.id == customer.default_source
56
+
57
+ card = customer.sources.create(source: token.id)
58
+ customer.default_source = card.id
59
+ customer.save
60
+
61
+ update_stripe_card_on_file(card)
62
+ true
63
+ rescue ::Stripe::StripeError => e
64
+ raise Error, e.message
65
+ end
66
+
67
+ def update_stripe_email!
68
+ customer = stripe_customer
69
+ customer.email = email
70
+ customer.description = customer_name
71
+ customer.save
72
+ end
73
+
74
+ def stripe_subscription(subscription_id)
75
+ ::Stripe::Subscription.retrieve(subscription_id)
76
+ end
77
+
78
+ def stripe_invoice!
79
+ return unless processor_id?
80
+ ::Stripe::Invoice.create(customer: processor_id).pay
81
+ end
82
+
83
+ def stripe_upcoming_invoice
84
+ ::Stripe::Invoice.upcoming(customer: processor_id)
85
+ end
86
+
87
+ def stripe?
88
+ processor == "stripe"
89
+ end
90
+
91
+ # Used by webhooks when the customer or source changes
92
+ def sync_card_from_stripe
93
+ stripe_cust = stripe_customer
94
+ default_source_id = stripe_cust.default_source
95
+
96
+ if default_source_id.present?
97
+ card = stripe_customer.sources.data.find{ |s| s.id == default_source_id }
98
+ update(
99
+ card_type: card.brand,
100
+ card_last4: card.last4,
101
+ card_exp_month: card.exp_month,
102
+ card_exp_year: card.exp_year
103
+ )
104
+
105
+ # Customer has no default payment source
106
+ else
107
+ update(card_type: nil, card_last4: nil)
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def create_stripe_customer
114
+ customer = ::Stripe::Customer.create(email: email, source: card_token, description: customer_name)
115
+ update(processor: 'stripe', processor_id: customer.id)
116
+
117
+ # Update the user's card on file if a token was passed in
118
+ source = customer.sources.data.first
119
+ if source.present?
120
+ update_stripe_card_on_file customer.sources.retrieve(source.id)
121
+ end
122
+
123
+ customer
124
+ end
125
+
126
+ def stripe_trial_end_date(stripe_sub)
127
+ stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
128
+ end
129
+
130
+ # Save the card to the database as the user's current card
131
+ def update_stripe_card_on_file(card)
132
+ update!(
133
+ card_type: card.brand,
134
+ card_last4: card.last4,
135
+ card_exp_month: card.exp_month,
136
+ card_exp_year: card.exp_year
137
+ )
138
+
139
+ self.card_token = nil
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,30 @@
1
+ module Pay
2
+ module Stripe
3
+
4
+ module Charge
5
+ extend ActiveSupport::Concern
6
+
7
+ def stripe?
8
+ processor == "stripe"
9
+ end
10
+
11
+ def stripe_charge
12
+ Stripe::Charge.retrieve(processor_id)
13
+ rescue ::Stripe::StripeError => e
14
+ raise Error, e.message
15
+ end
16
+
17
+ def stripe_refund!(amount_to_refund)
18
+ Stripe::Refund.create(
19
+ charge: processor_id,
20
+ amount: amount_to_refund
21
+ )
22
+
23
+ update(amount_refunded: amount_to_refund)
24
+ rescue ::Stripe::StripeError => e
25
+ raise Error, e.message
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ module Pay
2
+ module Stripe
3
+ module Subscription
4
+ def stripe?
5
+ processor == "stripe"
6
+ end
7
+
8
+ def stripe_cancel
9
+ subscription = processor_subscription
10
+ subscription.cancel_at_period_end = true
11
+ subscription.save
12
+
13
+ new_ends_at = on_trial? ? trial_ends_at : Time.at(subscription.current_period_end)
14
+ update(ends_at: new_ends_at)
15
+ rescue ::Stripe::StripeError => e
16
+ raise Error, e.message
17
+ end
18
+
19
+ def stripe_cancel_now!
20
+ subscription = processor_subscription.delete
21
+ update(ends_at: Time.zone.now)
22
+ rescue ::Stripe::StripeError => e
23
+ raise Error, e.message
24
+ end
25
+
26
+ def stripe_resume
27
+ subscription = processor_subscription
28
+ subscription.plan = processor_plan
29
+ subscription.trial_end = on_trial? ? trial_ends_at.to_i : 'now'
30
+ subscription.cancel_at_period_end = false
31
+ subscription.save
32
+ rescue ::Stripe::StripeError => e
33
+ raise Error, e.message
34
+ end
35
+
36
+ def stripe_swap(plan)
37
+ subscription = processor_subscription
38
+ subscription.plan = plan
39
+ subscription.prorate = prorate
40
+ subscription.trial_end = on_trial? ? trial_ends_at.to_i : 'now'
41
+ subscription.quantity = quantity if quantity?
42
+ subscription.save
43
+ rescue ::Stripe::StripeError => e
44
+ raise Error, e.message
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ require 'stripe_event'
2
+ Dir[File.join(__dir__, 'webhooks', '**', '*.rb')].each { |file| require file }
3
+
4
+ env = Rails.env.to_sym
5
+ secrets = Rails.application.secrets
6
+ credentials = Rails.application.credentials
7
+
8
+ StripeEvent.signing_secret = ENV["STRIPE_SIGNING_SECRET"] || secrets.dig(env, :stripe, :signing_secret) || credentials.dig(env, :stripe, :signing_secret)
9
+
10
+ StripeEvent.configure do |events|
11
+ # Listen to the charge event to make sure we get non-subscription
12
+ # purchases as well. Invoice is only for subscriptions and manual creation
13
+ # so it does not include individual charges.
14
+ events.subscribe 'charge.succeeded', Pay::Stripe::Webhooks::ChargeSucceeded.new
15
+ events.subscribe 'charge.refunded', Pay::Stripe::Webhooks::ChargeRefunded.new
16
+
17
+ # Warn user of upcoming charges for their subscription. This is handy for
18
+ # notifying annual users their subscription will renew shortly.
19
+ # This probably should be ignored for monthly subscriptions.
20
+ events.subscribe 'invoice.upcoming', Pay::Stripe::Webhooks::SubscriptionRenewing.new
21
+
22
+ # If a subscription is manually created on Stripe, we want to sync
23
+ events.subscribe 'customer.subscription.created', Pay::Stripe::Webhooks::SubscriptionCreated.new
24
+
25
+ # If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
26
+ events.subscribe 'customer.subscription.updated', Pay::Stripe::Webhooks::SubscriptionUpdated.new
27
+
28
+ # When a customers subscription is canceled, we want to update our records
29
+ events.subscribe 'customer.subscription.deleted', Pay::Stripe::Webhooks::SubscriptionDeleted.new
30
+
31
+ # Monitor changes for customer's default card changing
32
+ events.subscribe 'customer.updated', Pay::Stripe::Webhooks::CustomerUpdated.new
33
+
34
+ # If a customer was deleted in Stripe, their subscriptions should be cancelled
35
+ events.subscribe 'customer.deleted', Pay::Stripe::Webhooks::CustomerDeleted.new
36
+
37
+ # If a customer's payment source was deleted in Stripe, we should update as well
38
+ events.subscribe 'customer.source.deleted', Pay::Stripe::Webhooks::SourceDeleted.new
39
+ end
@@ -0,0 +1,25 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+
5
+ class ChargeRefunded
6
+ def call(event)
7
+ object = event.data.object
8
+ charge = Pay.charge_model.find_by(processor: :stripe, processor_id: object.id)
9
+
10
+ return unless charge.present?
11
+
12
+ charge.update(amount_refunded: object.amount_refunded)
13
+ notify_user(charge.owner, charge)
14
+ end
15
+
16
+ def notify_user(user, charge)
17
+ if Pay.send_emails
18
+ Pay::UserMailer.refund(user, charge).deliver_later
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+
5
+ class ChargeSucceeded
6
+ def call(event)
7
+ object = event.data.object
8
+ user = Pay.user_model.find_by(
9
+ processor: :stripe,
10
+ processor_id: object.customer
11
+ )
12
+
13
+ return unless user.present?
14
+ return if user.charges.where(processor_id: object.id).any?
15
+
16
+ charge = create_charge(user, object)
17
+ notify_user(user, charge)
18
+ charge
19
+ end
20
+
21
+ def create_charge(user, object)
22
+ charge = user.charges.find_or_initialize_by(
23
+ processor: :stripe,
24
+ processor_id: object.id,
25
+ )
26
+
27
+ charge.update(
28
+ amount: object.amount,
29
+ card_last4: object.source.last4,
30
+ card_type: object.source.brand,
31
+ card_exp_month: object.source.exp_month,
32
+ card_exp_year: object.source.exp_year,
33
+ created_at: Time.zone.at(object.created)
34
+ )
35
+
36
+ charge
37
+ end
38
+
39
+ def notify_user(user, charge)
40
+ if Pay.send_emails && charge.respond_to?(:receipt)
41
+ Pay::UserMailer.receipt(user, charge).deliver_later
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+
5
+ class CustomerDeleted
6
+ def call(event)
7
+ object = event.data.object
8
+ user = Pay.user_model.find_by(processor: :stripe, processor_id: object.id)
9
+
10
+ # Couldn't find user, we can skip
11
+ return unless user.present?
12
+
13
+ user.update(
14
+ processor_id: nil,
15
+ trial_ends_at: nil,
16
+ card_type: nil,
17
+ card_last4: nil,
18
+ card_exp_month: nil,
19
+ card_exp_year: nil,
20
+ )
21
+
22
+ user.subscriptions.update_all(
23
+ trial_ends_at: nil,
24
+ ends_at: Time.zone.now,
25
+ )
26
+ end
27
+ end
28
+
29
+ end
30
+ end
31
+ end