pay 2.7.1 → 3.0.2
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 -715
- 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 +1 -5
- data/app/models/pay/charge.rb +54 -17
- 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 +42 -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 +108 -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/generators/active_record/templates/billable_migration.rb +1 -0
- data/lib/pay/attributes.rb +74 -0
- data/lib/pay/billable/sync_customer.rb +30 -0
- data/lib/pay/braintree/billable.rb +133 -110
- data/lib/pay/braintree/payment_method.rb +42 -0
- data/lib/pay/braintree/subscription.rb +9 -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 -8
- 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 +60 -0
- data/lib/pay/paddle/response.rb +0 -0
- data/lib/pay/paddle/subscription.rb +49 -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 +75 -76
- data/lib/pay/stripe/charge.rb +44 -17
- data/lib/pay/stripe/merchant.rb +10 -10
- data/lib/pay/stripe/payment_method.rb +61 -0
- data/lib/pay/stripe/subscription.rb +55 -22
- 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 +15 -0
- data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
- 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
- data/lib/tasks/pay.rake +20 -0
- metadata +23 -36
- data/app/models/pay.rb +0 -5
- 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/lib/pay/billable/sync_email.rb +0 -40
- data/lib/pay/billable.rb +0 -172
@@ -0,0 +1,60 @@
|
|
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
|
+
return unless pay_customer.subscription
|
11
|
+
|
12
|
+
payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
|
13
|
+
payment_method.processor_id ||= NanoId.generate
|
14
|
+
|
15
|
+
# Lookup payment method from API unless passed in
|
16
|
+
attributes ||= payment_method_details_for(subscription_id: pay_customer.subscription.processor_id)
|
17
|
+
|
18
|
+
payment_method.update!(attributes)
|
19
|
+
payment_method
|
20
|
+
rescue ::PaddlePay::PaddlePayError => e
|
21
|
+
raise Pay::Paddle::Error, e
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.payment_method_details_for(subscription_id:)
|
25
|
+
subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
|
26
|
+
payment_information = subscription_user ? subscription_user[:payment_information] : {}
|
27
|
+
|
28
|
+
case payment_information[:payment_method]
|
29
|
+
when "card"
|
30
|
+
{
|
31
|
+
payment_method_type: :card,
|
32
|
+
brand: payment_information[:card_type],
|
33
|
+
last4: payment_information[:last_four_digits],
|
34
|
+
exp_month: payment_information[:expiry_date].split("/").first,
|
35
|
+
exp_year: payment_information[:expiry_date].split("/").last
|
36
|
+
}
|
37
|
+
when "paypal"
|
38
|
+
{
|
39
|
+
payment_method_type: :paypal,
|
40
|
+
brand: "PayPal"
|
41
|
+
}
|
42
|
+
else
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(pay_payment_method)
|
48
|
+
@pay_payment_method = pay_payment_method
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sets payment method as default
|
52
|
+
def make_default!
|
53
|
+
end
|
54
|
+
|
55
|
+
# Remove payment method
|
56
|
+
def detach
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
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?
|
@@ -79,6 +118,8 @@ module Pay
|
|
79
118
|
end
|
80
119
|
|
81
120
|
def swap(plan)
|
121
|
+
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
122
|
+
|
82
123
|
attributes = {plan_id: plan, prorate: prorate}
|
83
124
|
attributes[:quantity] = quantity if quantity?
|
84
125
|
PaddlePay::Subscription::User.update(processor_id, attributes)
|
@@ -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(
|
27
|
+
::Stripe::Customer.retrieve({id: processor_id}, stripe_options)
|
32
28
|
else
|
33
|
-
sc = ::Stripe::Customer.create({email: email, name: customer_name},
|
34
|
-
|
29
|
+
sc = ::Stripe::Customer.create({email: email, name: customer_name}, stripe_options)
|
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
|
-
payment_intent = ::Stripe::PaymentIntent.create(args,
|
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,22 +77,18 @@ 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
|
92
|
-
stripe_sub = ::Stripe::Subscription.create(opts,
|
84
|
+
stripe_sub = ::Stripe::Subscription.create(opts, stripe_options)
|
93
85
|
|
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
|
-
|
101
|
-
# Trial, card requires SCA
|
102
|
-
elsif subscription.on_trial? && stripe_sub.pending_setup_intent
|
103
|
-
Pay::Payment.new(stripe_sub.pending_setup_intent).validate
|
104
92
|
end
|
105
93
|
|
106
94
|
subscription
|
@@ -108,51 +96,57 @@ module Pay
|
|
108
96
|
raise Pay::Stripe::Error, e
|
109
97
|
end
|
110
98
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
def update_card(payment_method_id)
|
115
|
-
stripe_customer = customer
|
116
|
-
|
117
|
-
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)
|
118
102
|
|
119
|
-
|
120
|
-
|
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
|
121
110
|
|
122
|
-
|
123
|
-
true
|
111
|
+
save_payment_method(payment_method, default: default)
|
124
112
|
rescue ::Stripe::StripeError => e
|
125
113
|
raise Pay::Stripe::Error, e
|
126
114
|
end
|
127
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
|
+
|
128
131
|
def update_email!
|
129
|
-
::Stripe::Customer.update(processor_id, {email: email, name: customer_name},
|
132
|
+
::Stripe::Customer.update(processor_id, {email: email, name: customer_name}, stripe_options)
|
130
133
|
end
|
131
134
|
|
132
135
|
def processor_subscription(subscription_id, options = {})
|
133
|
-
::Stripe::Subscription.retrieve(options.merge(id: subscription_id),
|
136
|
+
::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
|
134
137
|
end
|
135
138
|
|
136
139
|
def invoice!(options = {})
|
137
140
|
return unless processor_id?
|
138
|
-
::Stripe::Invoice.create(options.merge(customer: processor_id),
|
141
|
+
::Stripe::Invoice.create(options.merge(customer: processor_id), stripe_options).pay
|
139
142
|
end
|
140
143
|
|
141
144
|
def upcoming_invoice
|
142
|
-
::Stripe::Invoice.upcoming({customer: processor_id},
|
143
|
-
end
|
144
|
-
|
145
|
-
# Used by webhooks when the customer or source changes
|
146
|
-
def sync_card_from_stripe
|
147
|
-
if (payment_method_id = customer.invoice_settings.default_payment_method)
|
148
|
-
update_card_on_file ::Stripe::PaymentMethod.retrieve(payment_method_id, {stripe_account: stripe_account}).card
|
149
|
-
else
|
150
|
-
billable.update(card_type: nil, card_last4: nil)
|
151
|
-
end
|
145
|
+
::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
|
152
146
|
end
|
153
147
|
|
154
148
|
def create_setup_intent
|
155
|
-
::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session},
|
149
|
+
::Stripe::SetupIntent.create({customer: processor_id, usage: :off_session}, stripe_options)
|
156
150
|
end
|
157
151
|
|
158
152
|
def trial_end_date(stripe_sub)
|
@@ -160,16 +154,14 @@ module Pay
|
|
160
154
|
stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
|
161
155
|
end
|
162
156
|
|
163
|
-
#
|
164
|
-
def
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
billable.card_token = nil
|
157
|
+
# Syncs a customer's subscriptions from Stripe to the database
|
158
|
+
def sync_subscriptions
|
159
|
+
subscriptions = ::Stripe::Subscription.list({customer: customer}, stripe_options)
|
160
|
+
subscriptions.map do |subscription|
|
161
|
+
Pay::Stripe::Subscription.sync(subscription.id)
|
162
|
+
end
|
163
|
+
rescue ::Stripe::StripeError => e
|
164
|
+
raise Pay::Stripe::Error, e
|
173
165
|
end
|
174
166
|
|
175
167
|
# https://stripe.com/docs/api/checkout/sessions/create
|
@@ -203,7 +195,7 @@ module Pay
|
|
203
195
|
}
|
204
196
|
end
|
205
197
|
|
206
|
-
::Stripe::Checkout::Session.create(args.merge(options),
|
198
|
+
::Stripe::Checkout::Session.create(args.merge(options), stripe_options)
|
207
199
|
end
|
208
200
|
|
209
201
|
# https://stripe.com/docs/api/checkout/sessions/create
|
@@ -230,7 +222,14 @@ module Pay
|
|
230
222
|
customer: processor_id,
|
231
223
|
return_url: options.delete(:return_url) || root_url
|
232
224
|
}
|
233
|
-
::Stripe::BillingPortal::Session.create(args.merge(options),
|
225
|
+
::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
|
226
|
+
end
|
227
|
+
|
228
|
+
private
|
229
|
+
|
230
|
+
# Options for Stripe requests
|
231
|
+
def stripe_options
|
232
|
+
{stripe_account: stripe_account}.compact
|
234
233
|
end
|
235
234
|
end
|
236
235
|
end
|