pay 3.0.22 → 4.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 +1 -1
- data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
- data/app/controllers/pay/webhooks/paddle_controller.rb +1 -1
- data/app/controllers/pay/webhooks/stripe_controller.rb +1 -1
- data/app/jobs/pay/customer_sync_job.rb +1 -3
- data/app/mailers/pay/application_mailer.rb +1 -1
- data/app/mailers/pay/user_mailer.rb +2 -2
- data/app/models/pay/charge.rb +23 -0
- data/app/models/pay/customer.rb +2 -6
- data/app/models/pay/merchant.rb +6 -0
- data/app/models/pay/subscription.rb +35 -8
- data/app/views/pay/user_mailer/receipt.html.erb +6 -6
- data/app/views/pay/user_mailer/refund.html.erb +6 -6
- data/config/locales/en.yml +31 -24
- data/config/routes.rb +3 -3
- data/lib/pay/attributes.rb +28 -2
- data/lib/pay/billable/sync_customer.rb +3 -3
- data/lib/pay/braintree/billable.rb +61 -48
- data/lib/pay/braintree/subscription.rb +8 -3
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +6 -1
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +2 -2
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -1
- data/lib/pay/braintree.rb +6 -2
- data/lib/pay/currency.rb +8 -2
- data/lib/pay/engine.rb +22 -4
- data/lib/pay/fake_processor/billable.rb +11 -8
- data/lib/pay/fake_processor/subscription.rb +11 -3
- data/lib/pay/paddle/billable.rb +0 -4
- data/lib/pay/paddle/subscription.rb +2 -2
- data/lib/pay/paddle/webhooks/signature_verifier.rb +45 -41
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +7 -2
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +7 -7
- data/lib/pay/paddle/webhooks/subscription_updated.rb +15 -15
- data/lib/pay/paddle.rb +14 -2
- data/lib/pay/receipts.rb +106 -32
- data/lib/pay/stripe/billable.rb +67 -23
- data/lib/pay/stripe/charge.rb +93 -11
- data/lib/pay/stripe/merchant.rb +1 -1
- data/lib/pay/stripe/subscription.rb +132 -30
- data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
- data/lib/pay/stripe/webhooks/payment_action_required.rb +5 -5
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +5 -9
- data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -1
- data/lib/pay/stripe.rb +10 -1
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +23 -4
- data/lib/tasks/pay.rake +1 -1
- metadata +3 -4
- data/lib/pay/merchant.rb +0 -37
@@ -11,7 +11,7 @@ module Pay
|
|
11
11
|
pay_subscription = Pay::Subscription.find_by_processor_and_id(:braintree, subscription.id)
|
12
12
|
return unless pay_subscription.present?
|
13
13
|
|
14
|
-
pay_subscription.update(trial_ends_at: Time.current)
|
14
|
+
pay_subscription.update!(trial_ends_at: Time.current)
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
data/lib/pay/braintree.rb
CHANGED
@@ -19,6 +19,12 @@ module Pay
|
|
19
19
|
|
20
20
|
extend Env
|
21
21
|
|
22
|
+
def self.enabled?
|
23
|
+
return false unless Pay.enabled_processors.include?(:braintree) && defined?(::Braintree)
|
24
|
+
|
25
|
+
Pay::Engine.version_matches?(required: "~> 4", current: ::Braintree::Version::String) || (raise "[Pay] braintree gem must be version ~> 4")
|
26
|
+
end
|
27
|
+
|
22
28
|
def self.setup
|
23
29
|
Pay.braintree_gateway = ::Braintree::Gateway.new(
|
24
30
|
environment: environment.to_sym,
|
@@ -26,8 +32,6 @@ module Pay
|
|
26
32
|
public_key: public_key,
|
27
33
|
private_key: private_key
|
28
34
|
)
|
29
|
-
|
30
|
-
configure_webhooks
|
31
35
|
end
|
32
36
|
|
33
37
|
def self.public_key
|
data/lib/pay/currency.rb
CHANGED
@@ -23,9 +23,9 @@ module Pay
|
|
23
23
|
|
24
24
|
def format_amount(amount, **options)
|
25
25
|
number_to_currency(
|
26
|
-
amount.
|
26
|
+
amount.to_f / subunit_to_unit.to_f,
|
27
27
|
{
|
28
|
-
precision: precision,
|
28
|
+
precision: precision + additional_precision(amount),
|
29
29
|
unit: unit,
|
30
30
|
separator: separator,
|
31
31
|
delimiter: delimiter,
|
@@ -43,6 +43,12 @@ module Pay
|
|
43
43
|
subunit_to_unit.digits.count - 1
|
44
44
|
end
|
45
45
|
|
46
|
+
# If amount is 0.8, we want to display $0.008
|
47
|
+
def additional_precision(amount)
|
48
|
+
_, decimals = amount.to_s.split(".")
|
49
|
+
decimals&.length || 0
|
50
|
+
end
|
51
|
+
|
46
52
|
def unit
|
47
53
|
attributes["unit"]
|
48
54
|
end
|
data/lib/pay/engine.rb
CHANGED
@@ -17,12 +17,30 @@ module Pay
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
initializer "pay.webhooks" do
|
21
|
+
Pay::Stripe.configure_webhooks if Pay::Stripe.enabled?
|
22
|
+
Pay::Braintree.configure_webhooks if Pay::Braintree.enabled?
|
23
|
+
Pay::Paddle.configure_webhooks if Pay::Paddle.enabled?
|
24
|
+
end
|
25
|
+
|
20
26
|
config.to_prepare do
|
21
|
-
Pay::Stripe.setup if
|
22
|
-
Pay::Braintree.setup if
|
23
|
-
Pay::Paddle.setup if
|
27
|
+
Pay::Stripe.setup if Pay::Stripe.enabled?
|
28
|
+
Pay::Braintree.setup if Pay::Braintree.enabled?
|
29
|
+
Pay::Paddle.setup if Pay::Paddle.enabled?
|
30
|
+
|
31
|
+
if defined?(::Receipts::VERSION)
|
32
|
+
if Pay::Engine.version_matches?(required: "~> 2", current: ::Receipts::VERSION)
|
33
|
+
Pay::Charge.include Pay::Receipts
|
34
|
+
else
|
35
|
+
raise "[Pay] receipts gem must be version ~> 2"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
24
39
|
|
25
|
-
|
40
|
+
# Determines if a gem version matches requirements
|
41
|
+
# Used for verifying that dependencies are correct
|
42
|
+
def version_matches?(current:, required:)
|
43
|
+
Gem::Dependency.new("gem", required).match? "gem", current
|
26
44
|
end
|
27
45
|
end
|
28
46
|
end
|
@@ -31,8 +31,8 @@ module Pay
|
|
31
31
|
processor_id: NanoId.generate,
|
32
32
|
amount: amount,
|
33
33
|
data: {
|
34
|
-
|
35
|
-
|
34
|
+
payment_method_type: :card,
|
35
|
+
brand: "Fake",
|
36
36
|
last4: 1234,
|
37
37
|
exp_month: Date.today.month,
|
38
38
|
exp_year: Date.today.year
|
@@ -44,7 +44,6 @@ module Pay
|
|
44
44
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
45
45
|
# Make to generate a processor_id
|
46
46
|
customer
|
47
|
-
|
48
47
|
attributes = options.merge(
|
49
48
|
processor_id: NanoId.generate,
|
50
49
|
name: name,
|
@@ -52,6 +51,11 @@ module Pay
|
|
52
51
|
status: :active,
|
53
52
|
quantity: options.fetch(:quantity, 1)
|
54
53
|
)
|
54
|
+
|
55
|
+
if (trial_period_days = attributes.delete(:trial_period_days))
|
56
|
+
attributes[:trial_ends_at] = trial_period_days.to_i.days.from_now
|
57
|
+
end
|
58
|
+
|
55
59
|
pay_customer.subscriptions.create!(attributes)
|
56
60
|
end
|
57
61
|
|
@@ -59,10 +63,10 @@ module Pay
|
|
59
63
|
# Make to generate a processor_id
|
60
64
|
customer
|
61
65
|
|
62
|
-
pay_customer.payment_methods.create!(
|
66
|
+
pay_payment_method = pay_customer.payment_methods.create!(
|
63
67
|
processor_id: NanoId.generate,
|
64
68
|
default: default,
|
65
|
-
type: :
|
69
|
+
type: :card,
|
66
70
|
data: {
|
67
71
|
brand: "Fake",
|
68
72
|
last4: 1234,
|
@@ -70,10 +74,9 @@ module Pay
|
|
70
74
|
exp_year: Date.today.year
|
71
75
|
}
|
72
76
|
)
|
73
|
-
end
|
74
77
|
|
75
|
-
|
76
|
-
|
78
|
+
pay_customer.reload_default_payment_method if default
|
79
|
+
pay_payment_method
|
77
80
|
end
|
78
81
|
|
79
82
|
def processor_subscription(subscription_id, options = {})
|
@@ -25,7 +25,7 @@ module Pay
|
|
25
25
|
|
26
26
|
# With trial, sets end to trial end (mimicing Stripe)
|
27
27
|
# Without trial, sets can ends_at to end of month
|
28
|
-
def cancel
|
28
|
+
def cancel(**options)
|
29
29
|
if pay_subscription.on_trial?
|
30
30
|
pay_subscription.update(ends_at: pay_subscription.trial_ends_at)
|
31
31
|
else
|
@@ -33,8 +33,13 @@ module Pay
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def cancel_now!
|
37
|
-
|
36
|
+
def cancel_now!(**options)
|
37
|
+
ends_at = Time.current
|
38
|
+
pay_subscription.update(
|
39
|
+
status: :canceled,
|
40
|
+
trial_ends_at: (ends_at if pay_subscription.trial_ends_at?),
|
41
|
+
ends_at: ends_at
|
42
|
+
)
|
38
43
|
end
|
39
44
|
|
40
45
|
def on_grace_period?
|
@@ -57,6 +62,9 @@ module Pay
|
|
57
62
|
|
58
63
|
def swap(plan)
|
59
64
|
end
|
65
|
+
|
66
|
+
def change_quantity(quantity)
|
67
|
+
end
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|
data/lib/pay/paddle/billable.rb
CHANGED
@@ -74,7 +74,7 @@ module Pay
|
|
74
74
|
raise Pay::Paddle::Error, e
|
75
75
|
end
|
76
76
|
|
77
|
-
def cancel
|
77
|
+
def cancel(**options)
|
78
78
|
ends_at = if on_trial?
|
79
79
|
trial_ends_at
|
80
80
|
elsif paused?
|
@@ -92,7 +92,7 @@ module Pay
|
|
92
92
|
raise Pay::Paddle::Error, e
|
93
93
|
end
|
94
94
|
|
95
|
-
def cancel_now!
|
95
|
+
def cancel_now!(**options)
|
96
96
|
PaddlePay::Subscription::User.cancel(processor_id)
|
97
97
|
pay_subscription.update(status: :canceled, ends_at: Time.current)
|
98
98
|
|
@@ -8,11 +8,15 @@ module Pay
|
|
8
8
|
class SignatureVerifier
|
9
9
|
def initialize(data)
|
10
10
|
@data = data
|
11
|
+
@public_key_file = Pay::Paddle.public_key_file
|
12
|
+
@public_key = Pay::Paddle.public_key
|
11
13
|
@public_key_base64 = Pay::Paddle.public_key_base64
|
12
14
|
end
|
13
15
|
|
14
16
|
def verify
|
15
17
|
data = @data
|
18
|
+
public_key = @public_key if @public_key
|
19
|
+
public_key = File.read(@public_key_file) if @public_key_file
|
16
20
|
public_key = Base64.decode64(@public_key_base64) if @public_key_base64
|
17
21
|
return false unless data && data["p_signature"] && public_key
|
18
22
|
|
@@ -59,53 +63,53 @@ module Pay
|
|
59
63
|
def serialize(var, assoc = false)
|
60
64
|
s = ""
|
61
65
|
case var
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
var.each do |k, v|
|
66
|
-
s << serialize(k, assoc) << serialize(v, assoc)
|
67
|
-
end
|
68
|
-
else
|
69
|
-
var.each_with_index do |v, i|
|
70
|
-
s << "i:#{i};#{serialize(v, assoc)}"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
s << "}"
|
74
|
-
when Hash
|
75
|
-
s << "a:#{var.size}:{"
|
66
|
+
when Array
|
67
|
+
s << "a:#{var.size}:{"
|
68
|
+
if assoc && var.first.is_a?(Array) && (var.first.size == 2)
|
76
69
|
var.each do |k, v|
|
77
|
-
s <<
|
70
|
+
s << serialize(k, assoc) << serialize(v, assoc)
|
78
71
|
end
|
79
|
-
|
80
|
-
|
72
|
+
else
|
73
|
+
var.each_with_index do |v, i|
|
74
|
+
s << "i:#{i};#{serialize(v, assoc)}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
s << "}"
|
78
|
+
when Hash
|
79
|
+
s << "a:#{var.size}:{"
|
80
|
+
var.each do |k, v|
|
81
|
+
s << "#{serialize(k, assoc)}#{serialize(v, assoc)}"
|
82
|
+
end
|
83
|
+
s << "}"
|
84
|
+
when Struct
|
85
|
+
# encode as Object with same name
|
86
|
+
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{var.members.length}:{"
|
87
|
+
var.members.each do |member|
|
88
|
+
s << "#{serialize(member, assoc)}#{serialize(var[member], assoc)}"
|
89
|
+
end
|
90
|
+
s << "}"
|
91
|
+
when String, Symbol
|
92
|
+
s << "s:#{var.to_s.bytesize}:\"#{var}\";"
|
93
|
+
when Integer
|
94
|
+
s << "i:#{var};"
|
95
|
+
when Float
|
96
|
+
s << "d:#{var};"
|
97
|
+
when NilClass
|
98
|
+
s << "N;"
|
99
|
+
when FalseClass, TrueClass
|
100
|
+
s << "b:#{var ? 1 : 0};"
|
101
|
+
else
|
102
|
+
if var.respond_to?(:to_assoc)
|
103
|
+
v = var.to_assoc
|
81
104
|
# encode as Object with same name
|
82
|
-
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{
|
83
|
-
|
84
|
-
s << "#{serialize(
|
105
|
+
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{"
|
106
|
+
v.each do |k, v|
|
107
|
+
s << "#{serialize(k.to_s, assoc)}#{serialize(v, assoc)}"
|
85
108
|
end
|
86
109
|
s << "}"
|
87
|
-
when String, Symbol
|
88
|
-
s << "s:#{var.to_s.bytesize}:\"#{var}\";"
|
89
|
-
when Integer
|
90
|
-
s << "i:#{var};"
|
91
|
-
when Float
|
92
|
-
s << "d:#{var};"
|
93
|
-
when NilClass
|
94
|
-
s << "N;"
|
95
|
-
when FalseClass, TrueClass
|
96
|
-
s << "b:#{var ? 1 : 0};"
|
97
110
|
else
|
98
|
-
|
99
|
-
|
100
|
-
# encode as Object with same name
|
101
|
-
s << "O:#{var.class.to_s.bytesize}:\"#{var.class.to_s.downcase}\":#{v.length}:{"
|
102
|
-
v.each do |k, v|
|
103
|
-
s << "#{serialize(k.to_s, assoc)}#{serialize(v, assoc)}"
|
104
|
-
end
|
105
|
-
s << "}"
|
106
|
-
else
|
107
|
-
raise TypeError, "Unable to serialize type #{var.class}"
|
108
|
-
end
|
111
|
+
raise TypeError, "Unable to serialize type #{var.class}"
|
112
|
+
end
|
109
113
|
end
|
110
114
|
s
|
111
115
|
end
|
@@ -9,8 +9,13 @@ module Pay
|
|
9
9
|
return if pay_subscription.nil?
|
10
10
|
|
11
11
|
# User canceled subscriptions have an ends_at
|
12
|
-
# Automatically
|
13
|
-
|
12
|
+
# Automatically cancelled subscriptions need this value set
|
13
|
+
ends_at = Time.zone.parse(event.cancellation_effective_date)
|
14
|
+
pay_subscription.update!(
|
15
|
+
status: :canceled,
|
16
|
+
trial_ends_at: (ends_at if pay_subscription.trial_ends_at?),
|
17
|
+
ends_at: ends_at
|
18
|
+
)
|
14
19
|
|
15
20
|
# Paddle doesn't allow reusing customers, so we should remove their payment methods
|
16
21
|
Pay::PaymentMethod.where(customer_id: pay_subscription.customer_id).destroy_all
|
@@ -6,10 +6,10 @@ module Pay
|
|
6
6
|
pay_charge = Pay::Charge.find_by_processor_and_id(:paddle, event.subscription_payment_id)
|
7
7
|
return unless pay_charge.present?
|
8
8
|
|
9
|
-
pay_charge.update(amount_refunded: (event.gross_refund.to_f * 100).to_i)
|
9
|
+
pay_charge.update!(amount_refunded: (event.gross_refund.to_f * 100).to_i)
|
10
10
|
|
11
|
-
if Pay.
|
12
|
-
Pay::UserMailer.with(pay_customer: pay_charge.customer,
|
11
|
+
if Pay.send_email?(:refund, pay_charge)
|
12
|
+
Pay::UserMailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).refund.deliver_later
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -17,8 +17,8 @@ module Pay
|
|
17
17
|
|
18
18
|
return if pay_customer.charges.where(processor_id: event.subscription_payment_id).any?
|
19
19
|
|
20
|
-
|
21
|
-
notify_user(
|
20
|
+
pay_charge = create_charge(pay_customer, event)
|
21
|
+
notify_user(pay_charge)
|
22
22
|
end
|
23
23
|
|
24
24
|
def create_charge(pay_customer, event)
|
@@ -33,18 +33,18 @@ module Pay
|
|
33
33
|
metadata: Pay::Paddle.parse_passthrough(event.passthrough).except("owner_sgid")
|
34
34
|
}.merge(payment_method_details)
|
35
35
|
|
36
|
-
|
37
|
-
|
36
|
+
pay_charge = pay_customer.charges.find_or_initialize_by(processor_id: event.subscription_payment_id)
|
37
|
+
pay_charge.update!(attributes)
|
38
38
|
|
39
39
|
# Update customer's payment method
|
40
40
|
Pay::Paddle::PaymentMethod.sync(pay_customer: pay_customer, attributes: payment_method_details)
|
41
41
|
|
42
|
-
|
42
|
+
pay_charge
|
43
43
|
end
|
44
44
|
|
45
45
|
def notify_user(pay_charge)
|
46
|
-
if Pay.
|
47
|
-
Pay::UserMailer.with(pay_customer: pay_charge.customer,
|
46
|
+
if Pay.send_email?(:receipt, pay_charge)
|
47
|
+
Pay::UserMailer.with(pay_customer: pay_charge.customer, pay_charge: pay_charge).receipt.deliver_later
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
@@ -3,33 +3,33 @@ module Pay
|
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionUpdated
|
5
5
|
def call(event)
|
6
|
-
|
6
|
+
pay_subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event["subscription_id"])
|
7
7
|
|
8
|
-
return if
|
8
|
+
return if pay_subscription.nil?
|
9
9
|
|
10
10
|
case event["status"]
|
11
11
|
when "deleted"
|
12
|
-
|
13
|
-
|
12
|
+
pay_subscription.status = "canceled"
|
13
|
+
pay_subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.current if pay_subscription.ends_at.blank?
|
14
14
|
when "trialing"
|
15
|
-
|
16
|
-
|
15
|
+
pay_subscription.status = "trialing"
|
16
|
+
pay_subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
|
17
17
|
when "active"
|
18
|
-
|
19
|
-
|
18
|
+
pay_subscription.status = "active"
|
19
|
+
pay_subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
|
20
20
|
else
|
21
|
-
|
21
|
+
pay_subscription.status = event["status"]
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
pay_subscription.quantity = event["new_quantity"]
|
25
|
+
pay_subscription.processor_plan = event["subscription_plan_id"]
|
26
|
+
pay_subscription.paddle_update_url = event["update_url"]
|
27
|
+
pay_subscription.paddle_cancel_url = event["cancel_url"]
|
28
28
|
|
29
29
|
# If user was on trial, their subscription ends at the end of the trial
|
30
|
-
|
30
|
+
pay_subscription.ends_at = pay_subscription.trial_ends_at if pay_subscription.on_trial?
|
31
31
|
|
32
|
-
|
32
|
+
pay_subscription.save!
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
data/lib/pay/paddle.rb
CHANGED
@@ -17,12 +17,16 @@ module Pay
|
|
17
17
|
|
18
18
|
extend Env
|
19
19
|
|
20
|
+
def self.enabled?
|
21
|
+
return false unless Pay.enabled_processors.include?(:paddle) && defined?(::PaddlePay)
|
22
|
+
|
23
|
+
Pay::Engine.version_matches?(required: "~> 0.2", current: ::PaddlePay::VERSION) || (raise "[Pay] paddle gem must be version ~> 0.2")
|
24
|
+
end
|
25
|
+
|
20
26
|
def self.setup
|
21
27
|
::PaddlePay.config.vendor_id = vendor_id
|
22
28
|
::PaddlePay.config.vendor_auth_code = vendor_auth_code
|
23
29
|
::PaddlePay.config.environment = environment
|
24
|
-
|
25
|
-
configure_webhooks
|
26
30
|
end
|
27
31
|
|
28
32
|
def self.vendor_id
|
@@ -37,6 +41,14 @@ module Pay
|
|
37
41
|
find_value_by_name(:paddle, :environment) || "production"
|
38
42
|
end
|
39
43
|
|
44
|
+
def self.public_key
|
45
|
+
find_value_by_name(:paddle, :public_key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.public_key_file
|
49
|
+
find_value_by_name(:paddle, :public_key_file)
|
50
|
+
end
|
51
|
+
|
40
52
|
def self.public_key_base64
|
41
53
|
find_value_by_name(:paddle, :public_key_base64)
|
42
54
|
end
|
data/lib/pay/receipts.rb
CHANGED
@@ -13,27 +13,102 @@ module Pay
|
|
13
13
|
receipt_pdf.render
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
16
|
+
def receipt_details
|
17
|
+
[
|
18
|
+
[I18n.t("pay.receipt.number"), receipt_number],
|
18
19
|
[I18n.t("pay.receipt.date"), I18n.l(created_at, format: :long)],
|
19
|
-
[I18n.t("pay.receipt.
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
[I18n.t("pay.receipt.payment_method"), charged_to]
|
21
|
+
]
|
22
|
+
end
|
23
|
+
|
24
|
+
def pdf_line_items
|
25
|
+
items = [
|
26
|
+
[
|
27
|
+
"<b>#{I18n.t("pay.line_items.description")}</b>",
|
28
|
+
"<b>#{I18n.t("pay.line_items.quantity")}</b>",
|
29
|
+
"<b>#{I18n.t("pay.line_items.unit_price")}</b>",
|
30
|
+
"<b>#{I18n.t("pay.line_items.amount")}</b>"
|
31
|
+
]
|
23
32
|
]
|
24
|
-
|
25
|
-
|
33
|
+
|
34
|
+
# Unit price is stored with the line item
|
35
|
+
# Negative amounts shouldn't display quantity
|
36
|
+
# Sort by line_items by period_end? oldest to newest
|
37
|
+
if line_items.any?
|
38
|
+
line_items.each do |li|
|
39
|
+
items << [li["description"], li["quantity"], Pay::Currency.format(li["unit_amount"], currency: currency), Pay::Currency.format(li["amount"], currency: currency)]
|
40
|
+
|
41
|
+
Array.wrap(li["discounts"]).each do |discount_id|
|
42
|
+
if (discount = total_discount_amounts.find { |d| d.dig("discount", "id") == discount_id })
|
43
|
+
items << [discount_description(discount), nil, nil, Pay::Currency.format(-discount["amount"], currency: currency)]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
else
|
48
|
+
items << [product, 1, Pay::Currency.format(amount, currency: currency), Pay::Currency.format(amount, currency: currency)]
|
49
|
+
end
|
50
|
+
|
51
|
+
# If no subtotal, we will display the total
|
52
|
+
items << [nil, nil, I18n.t("pay.line_items.subtotal"), Pay::Currency.format(subtotal || amount, currency: currency)]
|
53
|
+
|
54
|
+
# Discounts on the invoice
|
55
|
+
Array.wrap(discounts).each do |discount_id|
|
56
|
+
if (discount = total_discount_amounts.find { |d| d.dig("discount", "id") == discount_id })
|
57
|
+
items << [nil, nil, discount_description(discount), Pay::Currency.format(-discount["amount"], currency: currency)]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Tax rates
|
62
|
+
Array.wrap(total_tax_amounts).each do |tax_amount|
|
63
|
+
items << [nil, nil, tax_description(tax_amount), Pay::Currency.format(tax, currency: currency)]
|
64
|
+
end
|
65
|
+
|
66
|
+
items << [nil, nil, I18n.t("pay.line_items.total"), Pay::Currency.format(amount, currency: currency)]
|
67
|
+
items
|
68
|
+
end
|
69
|
+
|
70
|
+
def discount_description(discount)
|
71
|
+
coupon = discount.dig("discount", "coupon")
|
72
|
+
name = coupon.dig("name")
|
73
|
+
|
74
|
+
if (percent = coupon["percent_off"])
|
75
|
+
I18n.t("pay.line_items.percent_discount", name: name, percent: ActiveSupport::NumberHelper.number_to_rounded(percent, strip_insignificant_zeros: true))
|
76
|
+
else
|
77
|
+
I18n.t("pay.line_items.amount_discount", name: name, amount: Pay::Currency.format(coupon["amount_off"], currency: coupon["currency"]))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def tax_description(tax_amount)
|
82
|
+
tax_rate = tax_amount["tax_rate"]
|
83
|
+
percent = "#{ActiveSupport::NumberHelper.number_to_rounded(tax_rate["percentage"], strip_insignificant_zeros: true)}%"
|
84
|
+
percent += " inclusive" if tax_rate["inclusive"]
|
85
|
+
"#{tax_rate["display_name"]} - #{tax_rate["jurisdiction"]} (#{percent})"
|
86
|
+
end
|
87
|
+
|
88
|
+
def receipt_pdf(**options)
|
89
|
+
receipt_line_items = pdf_line_items
|
90
|
+
|
91
|
+
# Include total paid
|
92
|
+
receipt_line_items << [nil, nil, I18n.t("pay.receipt.amount_paid"), Pay::Currency.format(amount, currency: currency)]
|
93
|
+
|
94
|
+
if refunded?
|
95
|
+
receipt_line_items << [nil, nil, I18n.t("pay.receipt.refunded_on"), Pay::Currency.format(amount_refunded, currency: currency)]
|
96
|
+
end
|
26
97
|
|
27
98
|
defaults = {
|
28
|
-
|
29
|
-
|
99
|
+
details: receipt_details,
|
100
|
+
recipient: [
|
101
|
+
customer.customer_name,
|
102
|
+
customer.email,
|
103
|
+
customer.owner.try(:extra_billing_info)
|
104
|
+
],
|
30
105
|
company: {
|
31
106
|
name: Pay.business_name,
|
32
107
|
address: Pay.business_address,
|
33
108
|
email: Pay.support_email,
|
34
109
|
logo: Pay.business_logo
|
35
110
|
},
|
36
|
-
line_items:
|
111
|
+
line_items: receipt_line_items
|
37
112
|
}
|
38
113
|
|
39
114
|
::Receipts::Receipt.new(defaults.deep_merge(options))
|
@@ -47,41 +122,40 @@ module Pay
|
|
47
122
|
invoice_pdf.render
|
48
123
|
end
|
49
124
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
total = Pay::Currency.format(amount, currency: currency)
|
56
|
-
|
57
|
-
line_items = [
|
58
|
-
["<b>#{I18n.t("pay.invoice.product")}</b>", nil, "<b>#{I18n.t("pay.invoice.amount")}</b>"],
|
59
|
-
[product, nil, total],
|
60
|
-
[nil, I18n.t("pay.invoice.subtotal"), total],
|
61
|
-
[nil, I18n.t("pay.invoice.total"), total]
|
125
|
+
def invoice_details
|
126
|
+
[
|
127
|
+
[I18n.t("pay.invoice.number"), invoice_number],
|
128
|
+
[I18n.t("pay.invoice.date"), I18n.l(created_at, format: :long)],
|
129
|
+
[I18n.t("pay.invoice.payment_method"), charged_to]
|
62
130
|
]
|
131
|
+
end
|
63
132
|
|
133
|
+
def invoice_pdf(**options)
|
64
134
|
defaults = {
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
135
|
+
details: invoice_details,
|
136
|
+
recipient: [
|
137
|
+
customer.customer_name,
|
138
|
+
customer.email,
|
139
|
+
customer.owner.try(:extra_billing_info)
|
140
|
+
],
|
71
141
|
company: {
|
72
142
|
name: Pay.business_name,
|
73
143
|
address: Pay.business_address,
|
74
144
|
email: Pay.support_email,
|
75
145
|
logo: Pay.business_logo
|
76
146
|
},
|
77
|
-
line_items:
|
147
|
+
line_items: pdf_line_items
|
78
148
|
}
|
79
149
|
|
80
150
|
::Receipts::Invoice.new(defaults.deep_merge(options))
|
81
151
|
end
|
82
152
|
|
83
|
-
def
|
84
|
-
|
153
|
+
def invoice_number
|
154
|
+
id
|
155
|
+
end
|
156
|
+
|
157
|
+
def receipt_number
|
158
|
+
invoice_number
|
85
159
|
end
|
86
160
|
end
|
87
161
|
end
|