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
@@ -0,0 +1,41 @@
1
+ module Pay
2
+ module Billable
3
+ module SyncEmail
4
+ # Sync email address changes from the model to the processor.
5
+ # This way they're kept in sync and email notifications are
6
+ # always sent to the correct email address after an update.
7
+ #
8
+ # Processor classes simply need to implement a method named:
9
+ #
10
+ # update_PROCESSOR_email!
11
+ #
12
+ # This method should take the email address on the billable
13
+ # object and update the associated API record.
14
+
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+ after_update :enqeue_sync_email_job,
19
+ if: :should_sync_email_with_processor?
20
+ end
21
+
22
+ def should_sync_email_with_processor?
23
+ respond_to? :saved_change_to_email?
24
+ end
25
+
26
+ def sync_email_with_processor
27
+ send("update_#{processor}_email!")
28
+ end
29
+
30
+ private
31
+
32
+ def enqeue_sync_email_job
33
+ # Only update if the processor id is the same
34
+ # This prevents duplicate API hits if this is their first time
35
+ if processor_id? && !processor_id_changed? && saved_change_to_email?
36
+ EmailSyncJob.perform_later(id)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,16 @@
1
+ require 'pay/braintree/api'
2
+ require 'pay/braintree/billable'
3
+ require 'pay/braintree/charge'
4
+ require 'pay/braintree/subscription'
5
+
6
+ module Pay
7
+ module Braintree
8
+ def self.setup
9
+ Pay::Braintree::Api.set_api_keys
10
+
11
+ Pay.charge_model.include Pay::Braintree::Charge
12
+ Pay.subscription_model.include Pay::Braintree::Subscription
13
+ Pay.user_model.include Pay::Braintree::Billable
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ module Pay
2
+ module Braintree
3
+ module Api
4
+ def self.set_api_keys
5
+ environment = get_key_for(:environment, 'sandbox')
6
+ merchant_id = get_key_for(:merchant_id)
7
+ public_key = get_key_for(:public_key)
8
+ private_key = get_key_for(:private_key)
9
+
10
+ Pay.braintree_gateway = ::Braintree::Gateway.new(
11
+ environment: environment.to_sym,
12
+ merchant_id: merchant_id,
13
+ public_key: public_key,
14
+ private_key: private_key
15
+ )
16
+ end
17
+
18
+ def self.get_key_for(name, default = '')
19
+ env = Rails.env.to_sym
20
+ secrets = Rails.application.secrets
21
+ credentials = Rails.application.credentials
22
+
23
+ ENV["BRAINTREE_#{name.upcase}"] ||
24
+ secrets.dig(env, :braintree, name) ||
25
+ credentials.dig(env, :braintree, name) ||
26
+ default
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,219 @@
1
+ module Pay
2
+ module Braintree
3
+ module Billable
4
+ # Handles Billable#customer
5
+ #
6
+ # Returns Braintree::Customer
7
+ def braintree_customer
8
+ if processor_id?
9
+ gateway.customer.find(processor_id)
10
+ else
11
+ result = gateway.customer.create(
12
+ email: email,
13
+ first_name: try(:first_name),
14
+ last_name: try(:last_name),
15
+ payment_method_nonce: card_token,
16
+ )
17
+ raise Pay::Error.new(result.message) unless result.success?
18
+
19
+ update(processor: 'braintree', processor_id: result.customer.id)
20
+
21
+ if card_token.present?
22
+ update_braintree_card_on_file result.customer.payment_methods.last
23
+ end
24
+
25
+ result.customer
26
+ end
27
+ rescue ::Braintree::BraintreeError => e
28
+ raise Error, e.message
29
+ end
30
+
31
+ # Handles Billable#charge
32
+ #
33
+ # Returns a Pay::Charge
34
+ def create_braintree_charge(amount, options={})
35
+ args = {
36
+ amount: amount / 100.0,
37
+ customer_id: customer.id,
38
+ options: { submit_for_settlement: true }
39
+ }.merge(options)
40
+
41
+ result = gateway.transaction.sale(args)
42
+ save_braintree_transaction(result.transaction) if result.success?
43
+ rescue ::BraintreeError => e
44
+ raise Error, e.message
45
+ end
46
+
47
+ # Handles Billable#subscribe
48
+ #
49
+ # Returns Pay::Subscription
50
+ def create_braintree_subscription(name, plan, options={})
51
+ token = customer.payment_methods.find(&:default?).try(:token)
52
+ raise Pay::Error, "Customer has no default payment method" if token.nil?
53
+
54
+ subscription_options = options.merge(
55
+ payment_method_token: token,
56
+ plan_id: plan
57
+ )
58
+
59
+ result = gateway.subscription.create(subscription_options)
60
+ raise Pay::Error.new(result.message) unless result.success?
61
+
62
+ create_subscription(result.subscription, 'braintree', name, plan)
63
+ rescue ::Braintree::BraintreeError => e
64
+ raise Error, e.message
65
+ end
66
+
67
+ # Handles Billable#update_card
68
+ #
69
+ # Returns true if successful
70
+ def update_braintree_card(token)
71
+ result = gateway.payment_method.create(
72
+ customer_id: processor_id,
73
+ payment_method_nonce: token,
74
+ options: {
75
+ make_default: true,
76
+ verify_card: true
77
+ }
78
+ )
79
+ raise Pay::Error.new(result.message) unless result.success?
80
+
81
+ update_braintree_card_on_file result.payment_method
82
+ update_subscriptions_to_payment_method(result.payment_method.token)
83
+ true
84
+ rescue ::Braintree::BraintreeError => e
85
+ raise Error, e.message
86
+ end
87
+
88
+ def update_braintree_email!
89
+ braintree_customer.update(
90
+ email: email,
91
+ first_name: try(:first_name),
92
+ last_name: try(:last_name),
93
+ )
94
+ end
95
+
96
+ def braintree_trial_end_date(subscription)
97
+ return unless subscription.trial_period
98
+ Time.zone.parse(subscription.first_billing_date)
99
+ end
100
+
101
+ def update_subscriptions_to_payment_method(token)
102
+ subscriptions.each do |subscription|
103
+ if subscription.active?
104
+ gateway.subscription.update(subscription.processor_id, { payment_method_token: token })
105
+ end
106
+ end
107
+ end
108
+
109
+ def braintree_subscription(subscription_id)
110
+ gateway.subscription.find(subscription_id)
111
+ end
112
+
113
+ def braintree_invoice!
114
+ # pass
115
+ end
116
+
117
+ def braintree_upcoming_invoice
118
+ # pass
119
+ end
120
+
121
+ def braintree?
122
+ processor == "braintree"
123
+ end
124
+
125
+ def paypal?
126
+ braintree? && card_type == "PayPal"
127
+ end
128
+
129
+ def save_braintree_transaction(transaction)
130
+ attrs = card_details_for_braintree_transaction(transaction)
131
+ attrs.merge!(amount: transaction.amount.to_f * 100)
132
+
133
+ charge = charges.find_or_initialize_by(
134
+ processor: :braintree,
135
+ processor_id: transaction.id
136
+ )
137
+ charge.update(attrs)
138
+ charge
139
+ end
140
+
141
+ private
142
+
143
+ def gateway
144
+ Pay.braintree_gateway
145
+ end
146
+
147
+ def update_braintree_card_on_file(payment_method)
148
+ case payment_method
149
+ when ::Braintree::CreditCard
150
+ update!(
151
+ card_type: payment_method.card_type,
152
+ card_last4: payment_method.last_4,
153
+ card_exp_month: payment_method.expiration_month,
154
+ card_exp_year: payment_method.expiration_year
155
+ )
156
+
157
+ when ::Braintree::PayPalAccount
158
+ update!(
159
+ card_type: "PayPal",
160
+ card_last4: payment_method.email
161
+ )
162
+ end
163
+
164
+ # Clear the card token so we don't accidentally update twice
165
+ self.card_token = nil
166
+ end
167
+
168
+ def card_details_for_braintree_transaction(transaction)
169
+ case transaction.payment_instrument_type
170
+ when "credit_card", "samsung_pay_card", "masterpass_card", "samsung_pay_card", "visa_checkout_card"
171
+ payment_method = transaction.send("#{transaction.payment_instrument_type}_details")
172
+ {
173
+ card_type: payment_method.card_type,
174
+ card_last4: payment_method.last_4,
175
+ card_exp_month: payment_method.expiration_month,
176
+ card_exp_year: payment_method.expiration_year,
177
+ }
178
+
179
+ when "paypal_account"
180
+ {
181
+ card_type: "PayPal",
182
+ card_last4: transaction.paypal_details.payer_email,
183
+ card_exp_month: nil,
184
+ card_exp_year: nil,
185
+ }
186
+
187
+ when "android_pay_card"
188
+ payment_method = transaction.android_pay_details
189
+ {
190
+ card_type: payment_method.source_card_type,
191
+ card_last4: payment_method.source_card_last_4,
192
+ card_exp_month: payment_method.expiration_month,
193
+ card_exp_year: payment_method.expiration_year,
194
+ }
195
+
196
+ when "venmo_account"
197
+ {
198
+ card_type: "Venmo",
199
+ card_last4: transaction.venmo_account_details.username,
200
+ card_exp_month: nil,
201
+ card_exp_year: nil,
202
+ }
203
+
204
+ when "apple_pay_card"
205
+ payment_method = transaction.apple_pay_details
206
+ {
207
+ card_type: payment_method.card_type,
208
+ card_last4: payment_method.last_4,
209
+ card_exp_month: payment_method.expiration_month,
210
+ card_exp_year: payment_method.expiration_year,
211
+ }
212
+
213
+ else
214
+ {}
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,27 @@
1
+ module Pay
2
+ module Braintree
3
+
4
+ module Charge
5
+ extend ActiveSupport::Concern
6
+
7
+ def braintree?
8
+ processor == "braintree"
9
+ end
10
+
11
+ def braintree_charge
12
+ Pay.braintree_gateway.transaction.find(processor_id)
13
+ rescue ::Braintree::BraintreeError => e
14
+ raise Error, e.message
15
+ end
16
+
17
+ def braintree_refund!(amount_to_refund)
18
+ Pay.braintree_gateway.transaction.refund(processor_id, amount_to_refund / 100.0)
19
+
20
+ update(amount_refunded: amount_to_refund)
21
+ rescue ::Braintree::BraintreeError => e
22
+ raise Error, e.message
23
+ end
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,173 @@
1
+ module Pay
2
+ module Braintree
3
+ module Subscription
4
+ def braintree?
5
+ processor == "braintree"
6
+ end
7
+
8
+ def braintree_cancel
9
+ subscription = processor_subscription
10
+
11
+ if on_trial?
12
+ gateway.subscription.cancel(processor_subscription.id)
13
+ update(ends_at: trial_ends_at)
14
+ else
15
+ gateway.subscription.update(subscription.id, {
16
+ number_of_billing_cycles: subscription.current_billing_cycle
17
+ })
18
+ update(ends_at: subscription.billing_period_end_date)
19
+ end
20
+ rescue ::Braintree::BraintreeError => e
21
+ raise Error, e.message
22
+ end
23
+
24
+ def braintree_cancel_now!
25
+ gateway.subscription.cancel(processor_subscription.id)
26
+ update(ends_at: Time.zone.now)
27
+ rescue ::Braintree::BraintreeError => e
28
+ raise Error, e.message
29
+ end
30
+
31
+ def braintree_resume
32
+ if cancelled? && on_trial?
33
+ duration = trial_ends_at.to_date - Date.today
34
+
35
+ owner.subscribe(
36
+ name: name,
37
+ plan: processor_plan,
38
+ trial_period: true,
39
+ trial_duration: duration.to_i,
40
+ trial_duration_unit: :day
41
+ )
42
+
43
+ else
44
+ subscription = processor_subscription
45
+
46
+ gateway.subscription.update(subscription.id, {
47
+ never_expires: true,
48
+ number_of_billing_cycles: nil
49
+ })
50
+ end
51
+ rescue ::Braintree::BraintreeError => e
52
+ raise Error, e.message
53
+ end
54
+
55
+ def braintree_swap(plan)
56
+ if on_grace_period? && processor_plan == plan
57
+ resume
58
+ return
59
+ end
60
+
61
+ if !active?
62
+ owner.subscribe(name, plan, processor, trial_period: false)
63
+ return
64
+ end
65
+
66
+ braintree_plan = find_braintree_plan(plan)
67
+
68
+ if would_change_billing_frequency?(braintree_plan) && prorate?
69
+ swap_across_frequencies(braintree_plan)
70
+ return
71
+ end
72
+
73
+ subscription = processor_subscription
74
+
75
+ result = gateway.subscription.update(subscription.id, {
76
+ plan_id: braintree_plan.id,
77
+ price: braintree_plan.price,
78
+ never_expires: true,
79
+ number_of_billing_cycles: nil,
80
+ options: {
81
+ prorate_charges: prorate?,
82
+ }
83
+ })
84
+
85
+ if result.success?
86
+ update(processor_plan: braintree_plan.id, ends_at: nil)
87
+ else
88
+ raise Error, "Braintree failed to swap plans: #{result.message}"
89
+ end
90
+ rescue ::Braintree::BraintreeError => e
91
+ raise Error, e.message
92
+ end
93
+
94
+ private
95
+
96
+ def gateway
97
+ Pay.braintree_gateway
98
+ end
99
+
100
+ def would_change_billing_frequency?(plan)
101
+ plan.billing_frequency != find_braintree_plan(processor_plan).billing_frequency
102
+ end
103
+
104
+ def find_braintree_plan(id)
105
+ @braintree_plans ||= gateway.plan.all
106
+ @braintree_plans.find{ |p| p.id == id }
107
+ end
108
+
109
+ # Helper methods for swapping plans
110
+ def switching_to_monthly_plan?(current_plan, plan)
111
+ current_plan.billing_frequency == 12 && plan.billing_frequency == 1
112
+ end
113
+
114
+ def discount_for_switching_to_monthly(current_plan, plan)
115
+ cycles = (money_remaining_on_yearly_plan(current_plan) / plan.price).floor
116
+ OpenStruct.new(
117
+ amount: plan.price,
118
+ number_of_billing_cycles: cycles
119
+ )
120
+ end
121
+
122
+ def money_remaining_on_yearly_plan(current_plan)
123
+ end_date = processor_subscription.billing_period_end_date.to_date
124
+ (current_plan.price / 365) * (end_date - Date.today)
125
+ end
126
+
127
+ def discount_for_switching_to_yearly
128
+ amount = 0
129
+
130
+ processor_subscription.discounts.each do |discount|
131
+ if discount.id == 'plan-credit'
132
+ amount += discount.amount * discount.number_of_billing_cycles
133
+ end
134
+ end
135
+
136
+ OpenStruct.new(
137
+ amount: amount,
138
+ number_of_billing_cycles: 1
139
+ )
140
+ end
141
+
142
+ def swap_across_frequencies(plan)
143
+ current_plan = find_braintree_plan(processor_plan)
144
+
145
+ discount = if switching_to_monthly_plan?(current_plan, plan)
146
+ discount_for_switching_to_monthly(current_plan, plan)
147
+ else
148
+ discount_for_switching_to_yearly
149
+ end
150
+
151
+ options = {}
152
+
153
+ if discount.amount > 0 && discount.number_of_billing_cycles > 0
154
+ options = {
155
+ discounts: {
156
+ add: [
157
+ {
158
+ inherited_from_id: 'plan-credit',
159
+ amount: discount.amount,
160
+ number_of_billing_cycles: discount.number_of_billing_cycles
161
+ }
162
+ ]
163
+ }
164
+ }
165
+ end
166
+
167
+ cancel_now!
168
+
169
+ owner.subscribe(options.merge(name: name, plan: plan.id))
170
+ end
171
+ end
172
+ end
173
+ end