pay 2.7.2 → 3.0.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 +4 -4
- data/README.md +34 -731
- data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
- data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
- data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
- data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
- data/app/models/pay/application_record.rb +0 -5
- data/app/models/pay/charge.rb +31 -18
- data/app/models/pay/customer.rb +87 -0
- data/app/models/pay/merchant.rb +19 -0
- data/app/models/pay/payment_method.rb +41 -0
- data/app/models/pay/subscription.rb +32 -30
- data/app/models/pay/webhook.rb +36 -0
- data/app/views/layouts/pay/application.html.erb +2 -3
- data/app/views/pay/payments/show.html.erb +109 -81
- data/app/views/pay/user_mailer/receipt.html.erb +2 -2
- data/app/views/pay/user_mailer/refund.html.erb +2 -2
- data/config/locales/en.yml +1 -1
- data/db/migrate/1_create_pay_tables.rb +72 -0
- data/lib/pay/attributes.rb +74 -0
- data/lib/pay/billable/sync_customer.rb +30 -0
- data/lib/pay/braintree/billable.rb +126 -108
- data/lib/pay/braintree/payment_method.rb +33 -0
- data/lib/pay/braintree/subscription.rb +7 -12
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
- data/lib/pay/braintree.rb +3 -2
- data/lib/pay/engine.rb +6 -1
- data/lib/pay/fake_processor/billable.rb +45 -21
- data/lib/pay/fake_processor/payment_method.rb +21 -0
- data/lib/pay/fake_processor/subscription.rb +11 -10
- data/lib/pay/fake_processor.rb +2 -1
- data/lib/pay/nano_id.rb +13 -0
- data/lib/pay/paddle/billable.rb +18 -48
- data/lib/pay/paddle/charge.rb +5 -5
- data/lib/pay/paddle/payment_method.rb +58 -0
- data/lib/pay/paddle/response.rb +0 -0
- data/lib/pay/paddle/subscription.rb +47 -8
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
- data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
- data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
- data/lib/pay/paddle.rb +7 -3
- data/lib/pay/payment.rb +1 -1
- data/lib/pay/receipts.rb +35 -7
- data/lib/pay/stripe/billable.rb +50 -64
- data/lib/pay/stripe/charge.rb +18 -15
- data/lib/pay/stripe/merchant.rb +10 -10
- data/lib/pay/stripe/payment_method.rb +61 -0
- data/lib/pay/stripe/subscription.rb +22 -17
- data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
- data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
- data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
- data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
- data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
- data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
- data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
- data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
- data/lib/pay/stripe/webhooks/payment_method_attached.rb +2 -4
- data/lib/pay/stripe/webhooks/payment_method_detached.rb +1 -6
- data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
- data/lib/pay/stripe.rb +6 -3
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks/delegator.rb +4 -0
- data/lib/pay/webhooks/process_job.rb +9 -0
- data/lib/pay/webhooks.rb +1 -0
- data/lib/pay.rb +7 -78
- metadata +20 -37
- data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
- data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
- data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
- data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
- data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
- data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
- data/db/migrate/20210714175351_add_uniqueness_to_pay_models.rb +0 -6
- data/lib/pay/billable/sync_email.rb +0 -40
- data/lib/pay/billable.rb +0 -172
- data/lib/pay/stripe/webhooks/payment_method_automatically_updated.rb +0 -17
@@ -0,0 +1,58 @@
|
|
1
|
+
module Pay
|
2
|
+
module Paddle
|
3
|
+
class PaymentMethod
|
4
|
+
attr_reader :pay_payment_method
|
5
|
+
|
6
|
+
delegate :customer, :processor_id, to: :pay_payment_method
|
7
|
+
|
8
|
+
# Paddle doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
|
9
|
+
def self.sync(pay_customer:, attributes: nil)
|
10
|
+
payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
|
11
|
+
payment_method.processor_id ||= NanoId.generate
|
12
|
+
|
13
|
+
# Lookup payment method from API unless passed in
|
14
|
+
attributes ||= payment_method_details_for(subscription_id: pay_customer.subscription.processor_id)
|
15
|
+
|
16
|
+
payment_method.update!(attributes)
|
17
|
+
payment_method
|
18
|
+
rescue ::PaddlePay::PaddlePayError => e
|
19
|
+
raise Pay::Paddle::Error, e
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.payment_method_details_for(subscription_id:)
|
23
|
+
subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
|
24
|
+
payment_information = subscription_user ? subscription_user[:payment_information] : {}
|
25
|
+
|
26
|
+
case payment_information[:payment_method]
|
27
|
+
when "card"
|
28
|
+
{
|
29
|
+
payment_method_type: :card,
|
30
|
+
brand: payment_information[:card_type],
|
31
|
+
last4: payment_information[:last_four_digits],
|
32
|
+
exp_month: payment_information[:expiry_date].split("/").first,
|
33
|
+
exp_year: payment_information[:expiry_date].split("/").last
|
34
|
+
}
|
35
|
+
when "paypal"
|
36
|
+
{
|
37
|
+
payment_method_type: :paypal,
|
38
|
+
brand: "PayPal"
|
39
|
+
}
|
40
|
+
else
|
41
|
+
{}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize(pay_payment_method)
|
46
|
+
@pay_payment_method = pay_payment_method
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets payment method as default
|
50
|
+
def make_default!
|
51
|
+
end
|
52
|
+
|
53
|
+
# Remove payment method
|
54
|
+
def detach
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
File without changes
|
@@ -20,6 +20,49 @@ module Pay
|
|
20
20
|
:trial_ends_at,
|
21
21
|
to: :pay_subscription
|
22
22
|
|
23
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
24
|
+
# Passthrough is not return from this API, so we can't use that
|
25
|
+
object ||= OpenStruct.new PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
|
26
|
+
|
27
|
+
pay_customer = Pay::Customer.find_by(processor: :paddle, processor_id: object.user_id)
|
28
|
+
|
29
|
+
# If passthrough exists (only on webhooks) we can use it to create the Pay::Customer
|
30
|
+
if pay_customer.nil? && object.passthrough
|
31
|
+
owner = Pay::Paddle.owner_from_passthrough(object.passthrough)
|
32
|
+
pay_customer = owner&.set_payment_processor(:paddle, processor_id: object.user_id)
|
33
|
+
end
|
34
|
+
|
35
|
+
return unless pay_customer
|
36
|
+
|
37
|
+
attributes = {
|
38
|
+
paddle_cancel_url: object.cancel_url,
|
39
|
+
paddle_update_url: object.update_url,
|
40
|
+
processor_plan: object.plan_id || object.subscription_plan_id,
|
41
|
+
quantity: object.quantity,
|
42
|
+
status: object.state || object.status
|
43
|
+
}
|
44
|
+
|
45
|
+
# If paused or delete while on trial, set ends_at to match
|
46
|
+
case attributes[:status]
|
47
|
+
when "trialing"
|
48
|
+
attributes[:trial_ends_at] = Time.zone.parse(object.next_bill_date)
|
49
|
+
attributes[:ends_at] = nil
|
50
|
+
when "paused", "deleted"
|
51
|
+
attributes[:trial_ends_at] = nil
|
52
|
+
attributes[:ends_at] = Time.zone.parse(object.next_bill_date)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Update or create the subscription
|
56
|
+
if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.subscription_id))
|
57
|
+
pay_subscription.with_lock do
|
58
|
+
pay_subscription.update!(attributes)
|
59
|
+
end
|
60
|
+
pay_subscription
|
61
|
+
else
|
62
|
+
pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.subscription_id))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
23
66
|
def initialize(pay_subscription)
|
24
67
|
@pay_subscription = pay_subscription
|
25
68
|
end
|
@@ -32,26 +75,22 @@ module Pay
|
|
32
75
|
end
|
33
76
|
|
34
77
|
def cancel
|
35
|
-
|
78
|
+
ends_at = on_trial? ? trial_ends_at : processor_subscription.next_payment[:date]
|
36
79
|
PaddlePay::Subscription::User.cancel(processor_id)
|
37
|
-
|
38
|
-
pay_subscription.update(status: :canceled, ends_at: trial_ends_at)
|
39
|
-
else
|
40
|
-
pay_subscription.update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
|
41
|
-
end
|
80
|
+
pay_subscription.update(status: :canceled, ends_at: ends_at)
|
42
81
|
rescue ::PaddlePay::PaddlePayError => e
|
43
82
|
raise Pay::Paddle::Error, e
|
44
83
|
end
|
45
84
|
|
46
85
|
def cancel_now!
|
47
86
|
PaddlePay::Subscription::User.cancel(processor_id)
|
48
|
-
pay_subscription.update(status: :canceled, ends_at: Time.
|
87
|
+
pay_subscription.update(status: :canceled, ends_at: Time.current)
|
49
88
|
rescue ::PaddlePay::PaddlePayError => e
|
50
89
|
raise Pay::Paddle::Error, e
|
51
90
|
end
|
52
91
|
|
53
92
|
def on_grace_period?
|
54
|
-
canceled? && Time.
|
93
|
+
canceled? && Time.current < ends_at || paused? && Time.current < paddle_paused_from
|
55
94
|
end
|
56
95
|
|
57
96
|
def paused?
|
@@ -3,14 +3,17 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCancelled
|
5
5
|
def call(event)
|
6
|
-
|
6
|
+
pay_subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event.subscription_id)
|
7
7
|
|
8
8
|
# We couldn't find the subscription for some reason, maybe it's from another service
|
9
|
-
return if
|
9
|
+
return if pay_subscription.nil?
|
10
10
|
|
11
11
|
# User canceled subscriptions have an ends_at
|
12
12
|
# Automatically canceled subscriptions need this value set
|
13
|
-
|
13
|
+
pay_subscription.update!(ends_at: Time.zone.parse(event.cancellation_effective_date)) if pay_subscription.ends_at.blank? && event.cancellation_effective_date.present?
|
14
|
+
|
15
|
+
# Paddle doesn't allow reusing customers, so we should remove their payment methods
|
16
|
+
pay_subscription.customer.payment_methods.destroy_all
|
14
17
|
end
|
15
18
|
end
|
16
19
|
end
|
@@ -3,46 +3,7 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCreated
|
5
5
|
def call(event)
|
6
|
-
|
7
|
-
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
8
|
-
|
9
|
-
# Create the subscription in the database if we don't have it already
|
10
|
-
if subscription.nil?
|
11
|
-
|
12
|
-
# The customer could already be in the database
|
13
|
-
owner = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
|
14
|
-
|
15
|
-
if owner.nil?
|
16
|
-
owner = Pay::Paddle.owner_from_passthrough(event["passthrough"])
|
17
|
-
owner&.update!(processor: "paddle", processor_id: event["user_id"])
|
18
|
-
end
|
19
|
-
|
20
|
-
if owner.nil?
|
21
|
-
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
|
22
|
-
return
|
23
|
-
end
|
24
|
-
|
25
|
-
subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: event["subscription_id"], status: :active)
|
26
|
-
end
|
27
|
-
|
28
|
-
subscription.quantity = event["quantity"]
|
29
|
-
subscription.processor_plan = event["subscription_plan_id"]
|
30
|
-
subscription.paddle_update_url = event["update_url"]
|
31
|
-
subscription.paddle_cancel_url = event["cancel_url"]
|
32
|
-
subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"]) if event["status"] == "trialing"
|
33
|
-
|
34
|
-
# If user was on trial, their subscription ends at the end of the trial
|
35
|
-
subscription.ends_at = if ["paused", "deleted"].include?(event["status"]) && subscription.on_trial?
|
36
|
-
subscription.trial_ends_at
|
37
|
-
|
38
|
-
# User wasn't on trial, so subscription ends at period end
|
39
|
-
elsif ["paused", "deleted"].include?(event["status"])
|
40
|
-
Time.zone.parse(event["next_bill_date"])
|
41
|
-
|
42
|
-
# Subscription isn't marked to cancel at period end
|
43
|
-
end
|
44
|
-
|
45
|
-
subscription.save!
|
6
|
+
Pay::Paddle::Subscription.sync(event.subscription_id, object: event)
|
46
7
|
end
|
47
8
|
end
|
48
9
|
end
|
@@ -3,11 +3,11 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionPaymentRefunded
|
5
5
|
def call(event)
|
6
|
-
charge = Pay.
|
6
|
+
charge = Pay::Charge.find_by_processor_and_id(:paddle, event.subscription_payment_id)
|
7
7
|
return unless charge.present?
|
8
8
|
|
9
|
-
charge.update(amount_refunded:
|
10
|
-
notify_user(charge.owner, charge)
|
9
|
+
charge.update(amount_refunded: (event.gross_refund.to_f * 100).to_i)
|
10
|
+
notify_user(charge.customer.owner, charge)
|
11
11
|
end
|
12
12
|
|
13
13
|
def notify_user(billable, charge)
|
@@ -3,43 +3,41 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionPaymentSucceeded
|
5
5
|
def call(event)
|
6
|
-
|
6
|
+
pay_customer = Pay::Customer.find_by(processor: :paddle, processor_id: event.user_id)
|
7
7
|
|
8
|
-
if
|
9
|
-
|
10
|
-
|
8
|
+
if pay_customer.nil?
|
9
|
+
owner = Pay::Paddle.owner_from_passthrough(event.passthrough)
|
10
|
+
pay_customer = owner&.set_payment_processor :paddle, processor_id: event.user_id
|
11
11
|
end
|
12
12
|
|
13
|
-
if
|
14
|
-
Rails.logger.error("[Pay] Unable to find Pay::
|
13
|
+
if pay_customer.nil?
|
14
|
+
Rails.logger.error("[Pay] Unable to find Pay::Customer with: '#{event.passthrough}'")
|
15
15
|
return
|
16
16
|
end
|
17
17
|
|
18
|
-
return if
|
18
|
+
return if pay_customer.charges.where(processor_id: event.subscription_payment_id).any?
|
19
19
|
|
20
|
-
charge = create_charge(
|
21
|
-
notify_user(
|
20
|
+
charge = create_charge(pay_customer, event)
|
21
|
+
notify_user(pay_customer.owner, charge)
|
22
22
|
end
|
23
23
|
|
24
|
-
def create_charge(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
charge.update(params.merge(payment_information))
|
42
|
-
user.update(payment_information)
|
24
|
+
def create_charge(pay_customer, event)
|
25
|
+
payment_method_details = Pay::Paddle::PaymentMethod.payment_method_details_for(subscription_id: event.subscription_id)
|
26
|
+
|
27
|
+
attributes = {
|
28
|
+
amount: (event.sale_gross.to_f * 100).to_i,
|
29
|
+
created_at: Time.zone.parse(event.event_time),
|
30
|
+
currency: event.currency,
|
31
|
+
paddle_receipt_url: event.receipt_url,
|
32
|
+
subscription: pay_customer.subscriptions.find_by(processor_id: event.subscription_id),
|
33
|
+
metadata: Pay::Paddle.parse_passthrough(event.passthrough).except("owner_sgid")
|
34
|
+
}.merge(payment_method_details)
|
35
|
+
|
36
|
+
charge = pay_customer.charges.find_or_initialize_by(processor_id: event.subscription_payment_id)
|
37
|
+
charge.update!(attributes)
|
38
|
+
|
39
|
+
# Update customer's payment method
|
40
|
+
Pay::Paddle::PaymentMethod.sync(pay_customer: pay_customer, attributes: payment_method_details)
|
43
41
|
|
44
42
|
charge
|
45
43
|
end
|
@@ -3,14 +3,14 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionUpdated
|
5
5
|
def call(event)
|
6
|
-
subscription = Pay.
|
6
|
+
subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event["subscription_id"])
|
7
7
|
|
8
8
|
return if subscription.nil?
|
9
9
|
|
10
10
|
case event["status"]
|
11
11
|
when "deleted"
|
12
12
|
subscription.status = "canceled"
|
13
|
-
subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.
|
13
|
+
subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.current if subscription.ends_at.blank?
|
14
14
|
when "trialing"
|
15
15
|
subscription.status = "trialing"
|
16
16
|
subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
|
data/lib/pay/paddle.rb
CHANGED
@@ -2,8 +2,9 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
autoload :Billable, "pay/paddle/billable"
|
4
4
|
autoload :Charge, "pay/paddle/charge"
|
5
|
-
autoload :Subscription, "pay/paddle/subscription"
|
6
5
|
autoload :Error, "pay/paddle/error"
|
6
|
+
autoload :PaymentMethod, "pay/paddle/payment_method"
|
7
|
+
autoload :Subscription, "pay/paddle/subscription"
|
7
8
|
|
8
9
|
module Webhooks
|
9
10
|
autoload :SignatureVerifier, "pay/paddle/webhooks/signature_verifier"
|
@@ -44,9 +45,12 @@ module Pay
|
|
44
45
|
options.merge(owner_sgid: owner.to_sgid.to_s).to_json
|
45
46
|
end
|
46
47
|
|
48
|
+
def self.parse_passthrough(passthrough)
|
49
|
+
JSON.parse(passthrough)
|
50
|
+
end
|
51
|
+
|
47
52
|
def self.owner_from_passthrough(passthrough)
|
48
|
-
|
49
|
-
GlobalID::Locator.locate_signed(passthrough_json["owner_sgid"])
|
53
|
+
GlobalID::Locator.locate_signed parse_passthrough(passthrough)["owner_sgid"]
|
50
54
|
rescue JSON::ParserError
|
51
55
|
nil
|
52
56
|
end
|
data/lib/pay/payment.rb
CHANGED
@@ -2,7 +2,7 @@ module Pay
|
|
2
2
|
class Payment
|
3
3
|
attr_reader :intent
|
4
4
|
|
5
|
-
delegate :id, :amount, :client_secret, :status, :confirm, to: :intent
|
5
|
+
delegate :id, :amount, :client_secret, :customer, :status, :confirm, to: :intent
|
6
6
|
|
7
7
|
def self.from_id(id)
|
8
8
|
intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id) : ::Stripe::PaymentIntent.retrieve(id)
|
data/lib/pay/receipts.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
module Pay
|
2
2
|
module Receipts
|
3
|
-
def filename
|
4
|
-
"receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
|
5
|
-
end
|
6
|
-
|
7
3
|
def product
|
8
4
|
Pay.application_name
|
9
5
|
end
|
10
6
|
|
11
|
-
|
7
|
+
def receipt_filename
|
8
|
+
"receipt-#{created_at.strftime("%Y-%m-%d")}.pdf"
|
9
|
+
end
|
10
|
+
alias_method :filename, :receipt_filename
|
11
|
+
|
12
12
|
def receipt
|
13
13
|
receipt_pdf.render
|
14
14
|
end
|
@@ -26,15 +26,43 @@ module Pay
|
|
26
26
|
)
|
27
27
|
end
|
28
28
|
|
29
|
+
def invoice_filename
|
30
|
+
"invoice-#{created_at.strftime("%Y-%m-%d")}.pdf"
|
31
|
+
end
|
32
|
+
|
33
|
+
def invoice
|
34
|
+
invoice_pdf.render
|
35
|
+
end
|
36
|
+
|
37
|
+
def invoice_pdf
|
38
|
+
::Receipts::Invoice.new(
|
39
|
+
id: id,
|
40
|
+
issue_date: created_at,
|
41
|
+
due_date: created_at,
|
42
|
+
status: "<b><color rgb='#5eba7d'>PAID</color></b>",
|
43
|
+
bill_to: [
|
44
|
+
customer.customer_name,
|
45
|
+
customer.email
|
46
|
+
].compact,
|
47
|
+
product: product,
|
48
|
+
company: {
|
49
|
+
name: Pay.business_name,
|
50
|
+
address: Pay.business_address,
|
51
|
+
email: Pay.support_email
|
52
|
+
},
|
53
|
+
line_items: line_items
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
29
57
|
def line_items
|
30
58
|
line_items = [
|
31
59
|
[I18n.t("receipt.date"), created_at.to_s],
|
32
|
-
[I18n.t("receipt.account_billed"), "#{
|
60
|
+
[I18n.t("receipt.account_billed"), "#{customer.customer_name} (#{customer.email})"],
|
33
61
|
[I18n.t("receipt.product"), product],
|
34
62
|
[I18n.t("receipt.amount"), ActionController::Base.helpers.number_to_currency(amount / 100.0)],
|
35
63
|
[I18n.t("receipt.charged_to"), charged_to]
|
36
64
|
]
|
37
|
-
line_items << [I18n.t("receipt.additional_info"), owner.extra_billing_info] if owner.extra_billing_info?
|
65
|
+
line_items << [I18n.t("receipt.additional_info"), customer.owner.extra_billing_info] if customer.owner.extra_billing_info?
|
38
66
|
line_items
|
39
67
|
end
|
40
68
|
end
|
data/lib/pay/stripe/billable.rb
CHANGED
@@ -3,43 +3,40 @@ module Pay
|
|
3
3
|
class Billable
|
4
4
|
include Rails.application.routes.url_helpers
|
5
5
|
|
6
|
-
attr_reader :
|
6
|
+
attr_reader :pay_customer
|
7
7
|
|
8
8
|
delegate :processor_id,
|
9
9
|
:processor_id?,
|
10
10
|
:email,
|
11
11
|
:customer_name,
|
12
|
-
:
|
12
|
+
:payment_method_token,
|
13
|
+
:payment_method_token?,
|
13
14
|
:stripe_account,
|
14
|
-
to: :
|
15
|
+
to: :pay_customer
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
Rails.application.config.action_mailer.default_url_options || {}
|
19
|
-
end
|
17
|
+
def self.default_url_options
|
18
|
+
Rails.application.config.action_mailer.default_url_options || {}
|
20
19
|
end
|
21
20
|
|
22
|
-
def initialize(
|
23
|
-
@
|
21
|
+
def initialize(pay_customer)
|
22
|
+
@pay_customer = pay_customer
|
24
23
|
end
|
25
24
|
|
26
|
-
# Handles Billable#customer
|
27
|
-
#
|
28
|
-
# Returns Stripe::Customer
|
29
25
|
def customer
|
30
26
|
stripe_customer = if processor_id?
|
31
|
-
::Stripe::Customer.retrieve(processor_id, stripe_options)
|
27
|
+
::Stripe::Customer.retrieve({id: processor_id}, stripe_options)
|
32
28
|
else
|
33
29
|
sc = ::Stripe::Customer.create({email: email, name: customer_name}, stripe_options)
|
34
|
-
|
30
|
+
pay_customer.update!(processor_id: sc.id, stripe_account: stripe_account)
|
35
31
|
sc
|
36
32
|
end
|
37
33
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
34
|
+
if payment_method_token?
|
35
|
+
payment_method = ::Stripe::PaymentMethod.attach(payment_method_token, {customer: stripe_customer.id}, stripe_options)
|
36
|
+
pay_payment_method = save_payment_method(payment_method, default: false)
|
37
|
+
pay_payment_method.make_default!
|
38
|
+
|
39
|
+
pay_customer.payment_method_token = nil
|
43
40
|
end
|
44
41
|
|
45
42
|
stripe_customer
|
@@ -47,37 +44,32 @@ module Pay
|
|
47
44
|
raise Pay::Stripe::Error, e
|
48
45
|
end
|
49
46
|
|
50
|
-
# Handles Billable#charge
|
51
|
-
#
|
52
|
-
# Returns Pay::Charge
|
53
47
|
def charge(amount, options = {})
|
54
|
-
|
48
|
+
add_payment_method(payment_method_token, default: true) if payment_method_token?
|
49
|
+
|
50
|
+
payment_method = pay_customer.default_payment_method
|
55
51
|
args = {
|
56
52
|
amount: amount,
|
57
53
|
confirm: true,
|
58
54
|
confirmation_method: :automatic,
|
59
55
|
currency: "usd",
|
60
|
-
customer:
|
61
|
-
payment_method:
|
56
|
+
customer: processor_id,
|
57
|
+
payment_method: payment_method&.processor_id
|
62
58
|
}.merge(options)
|
63
59
|
|
64
60
|
payment_intent = ::Stripe::PaymentIntent.create(args, stripe_options)
|
65
61
|
Pay::Payment.new(payment_intent).validate
|
66
62
|
|
67
|
-
# Create a new charge object
|
68
63
|
charge = payment_intent.charges.first
|
69
64
|
Pay::Stripe::Charge.sync(charge.id, object: charge)
|
70
65
|
rescue ::Stripe::StripeError => e
|
71
66
|
raise Pay::Stripe::Error, e
|
72
67
|
end
|
73
68
|
|
74
|
-
# Handles Billable#subscribe
|
75
|
-
#
|
76
|
-
# Returns Pay::Subscription
|
77
69
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
78
70
|
quantity = options.delete(:quantity) || 1
|
79
71
|
opts = {
|
80
|
-
expand: ["pending_setup_intent", "latest_invoice.payment_intent"],
|
72
|
+
expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"],
|
81
73
|
items: [plan: plan, quantity: quantity],
|
82
74
|
off_session: true
|
83
75
|
}.merge(options)
|
@@ -85,7 +77,7 @@ module Pay
|
|
85
77
|
# Inherit trial from plan unless trial override was specified
|
86
78
|
opts[:trial_from_plan] = true unless opts[:trial_period_days]
|
87
79
|
|
88
|
-
# Load the Stripe customer to verify it exists and update
|
80
|
+
# Load the Stripe customer to verify it exists and update payment method if needed
|
89
81
|
opts[:customer] = customer.id
|
90
82
|
|
91
83
|
# Create subscription on Stripe
|
@@ -94,7 +86,7 @@ module Pay
|
|
94
86
|
# Save Pay::Subscription
|
95
87
|
subscription = Pay::Stripe::Subscription.sync(stripe_sub.id, object: stripe_sub, name: name)
|
96
88
|
|
97
|
-
# No trial,
|
89
|
+
# No trial, payment method requires SCA
|
98
90
|
if subscription.incomplete?
|
99
91
|
Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
|
100
92
|
end
|
@@ -104,23 +96,38 @@ module Pay
|
|
104
96
|
raise Pay::Stripe::Error, e
|
105
97
|
end
|
106
98
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
def update_card(payment_method_id)
|
111
|
-
stripe_customer = customer
|
112
|
-
|
113
|
-
return true if payment_method_id == stripe_customer.invoice_settings.default_payment_method
|
99
|
+
def add_payment_method(payment_method_id, default: false)
|
100
|
+
customer unless processor_id?
|
101
|
+
payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: processor_id}, stripe_options)
|
114
102
|
|
115
|
-
|
116
|
-
|
103
|
+
if default
|
104
|
+
::Stripe::Customer.update(processor_id, {
|
105
|
+
invoice_settings: {
|
106
|
+
default_payment_method: payment_method.id
|
107
|
+
}
|
108
|
+
}, stripe_options)
|
109
|
+
end
|
117
110
|
|
118
|
-
|
119
|
-
true
|
111
|
+
save_payment_method(payment_method, default: default)
|
120
112
|
rescue ::Stripe::StripeError => e
|
121
113
|
raise Pay::Stripe::Error, e
|
122
114
|
end
|
123
115
|
|
116
|
+
# Save the Stripe::PaymentMethod to the database
|
117
|
+
def save_payment_method(payment_method, default:)
|
118
|
+
pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.id).first_or_initialize
|
119
|
+
|
120
|
+
attributes = Pay::Stripe::PaymentMethod.extract_attributes(payment_method).merge(default: default)
|
121
|
+
|
122
|
+
pay_customer.payment_methods.update_all(default: false) if default
|
123
|
+
pay_payment_method.update!(attributes)
|
124
|
+
|
125
|
+
# Reload the Rails association
|
126
|
+
pay_customer.reload_default_payment_method if default
|
127
|
+
|
128
|
+
pay_payment_method
|
129
|
+
end
|
130
|
+
|
124
131
|
def update_email!
|
125
132
|
::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
|
126
133
|
end
|
@@ -138,15 +145,6 @@ module Pay
|
|
138
145
|
::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
|
139
146
|
end
|
140
147
|
|
141
|
-
# Used by webhooks when the customer or source changes
|
142
|
-
def sync_card_from_stripe
|
143
|
-
if (payment_method_id = customer.invoice_settings.default_payment_method)
|
144
|
-
update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, stripe_options).card
|
145
|
-
else
|
146
|
-
billable.update(card_type: nil, card_last4: nil)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
148
|
def create_setup_intent
|
151
149
|
::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, stripe_options)
|
152
150
|
end
|
@@ -156,18 +154,6 @@ module Pay
|
|
156
154
|
stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
|
157
155
|
end
|
158
156
|
|
159
|
-
# Save the card to the database as the user's current card
|
160
|
-
def update_card_on_file(card)
|
161
|
-
billable.update!(
|
162
|
-
card_type: card.brand.capitalize,
|
163
|
-
card_last4: card.last4,
|
164
|
-
card_exp_month: card.exp_month,
|
165
|
-
card_exp_year: card.exp_year
|
166
|
-
)
|
167
|
-
|
168
|
-
billable.card_token = nil
|
169
|
-
end
|
170
|
-
|
171
157
|
# Syncs a customer's subscriptions from Stripe to the database
|
172
158
|
def sync_subscriptions
|
173
159
|
subscriptions = ::Stripe::Subscription.list({customer: customer}, stripe_options)
|