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.
- checksums.yaml +5 -5
- data/MIT-LICENSE +1 -1
- data/README.md +256 -29
- data/Rakefile +1 -6
- data/app/controllers/pay/webhooks/braintree_controller.rb +56 -0
- data/app/jobs/pay/email_sync_job.rb +12 -0
- data/app/mailers/pay/user_mailer.rb +42 -0
- data/app/models/pay/charge.rb +31 -0
- data/app/models/pay/subscription.rb +77 -0
- data/app/views/pay/user_mailer/receipt.html.erb +20 -0
- data/app/views/pay/user_mailer/refund.html.erb +21 -0
- data/app/views/pay/user_mailer/subscription_renewing.html.erb +6 -0
- data/config/routes.rb +3 -1
- data/db/migrate/20170205020145_create_subscriptions.rb +1 -1
- data/db/migrate/20170503131610_add_fields_to_users.rb +3 -2
- data/db/migrate/20170727235816_create_charges.rb +17 -0
- data/lib/generators/pay/email_views_generator.rb +13 -0
- data/lib/pay.rb +65 -1
- data/lib/pay/billable.rb +54 -24
- data/lib/pay/billable/sync_email.rb +41 -0
- data/lib/pay/braintree.rb +16 -0
- data/lib/pay/braintree/api.rb +30 -0
- data/lib/pay/braintree/billable.rb +219 -0
- data/lib/pay/braintree/charge.rb +27 -0
- data/lib/pay/braintree/subscription.rb +173 -0
- data/lib/pay/engine.rb +14 -1
- data/lib/pay/receipts.rb +37 -0
- data/lib/pay/stripe.rb +17 -0
- data/lib/pay/stripe/api.rb +13 -0
- data/lib/pay/stripe/billable.rb +143 -0
- data/lib/pay/stripe/charge.rb +30 -0
- data/lib/pay/stripe/subscription.rb +48 -0
- data/lib/pay/stripe/webhooks.rb +39 -0
- data/lib/pay/stripe/webhooks/charge_refunded.rb +25 -0
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +47 -0
- data/lib/pay/stripe/webhooks/customer_deleted.rb +31 -0
- data/lib/pay/stripe/webhooks/customer_updated.rb +19 -0
- data/lib/pay/stripe/webhooks/source_deleted.rb +19 -0
- data/lib/pay/stripe/webhooks/subscription_created.rb +46 -0
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +21 -0
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +25 -0
- data/lib/pay/stripe/webhooks/subscription_updated.rb +35 -0
- data/lib/pay/version.rb +1 -1
- metadata +124 -30
- data/app/models/subscription.rb +0 -59
- data/config/initializers/pay.rb +0 -3
- data/config/initializers/stripe.rb +0 -1
- data/db/development.sqlite3 +0 -0
- data/lib/pay/billable/braintree.rb +0 -57
- data/lib/pay/billable/stripe.rb +0 -47
- data/lib/tasks/pay_tasks.rake +0 -4
data/lib/pay/engine.rb
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
module Pay
|
2
2
|
class Engine < ::Rails::Engine
|
3
|
-
|
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
|
data/lib/pay/receipts.rb
ADDED
@@ -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
|
data/lib/pay/stripe.rb
ADDED
@@ -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
|