pay 3.0.24 → 4.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.

Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +1 -1
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +1 -1
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +1 -1
  6. data/app/jobs/pay/customer_sync_job.rb +0 -2
  7. data/app/mailers/pay/application_mailer.rb +1 -1
  8. data/app/mailers/pay/user_mailer.rb +3 -3
  9. data/app/models/pay/charge.rb +23 -0
  10. data/app/models/pay/customer.rb +2 -6
  11. data/app/models/pay/merchant.rb +6 -0
  12. data/app/models/pay/subscription.rb +35 -8
  13. data/app/views/pay/user_mailer/receipt.html.erb +6 -6
  14. data/app/views/pay/user_mailer/refund.html.erb +6 -6
  15. data/config/locales/en.yml +31 -24
  16. data/config/routes.rb +3 -3
  17. data/lib/pay/attributes.rb +28 -2
  18. data/lib/pay/billable/sync_customer.rb +3 -3
  19. data/lib/pay/braintree/billable.rb +61 -48
  20. data/lib/pay/braintree/subscription.rb +8 -3
  21. data/lib/pay/braintree/webhooks/subscription_canceled.rb +6 -1
  22. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +2 -2
  23. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  24. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -1
  25. data/lib/pay/braintree.rb +6 -2
  26. data/lib/pay/currency.rb +8 -2
  27. data/lib/pay/engine.rb +22 -4
  28. data/lib/pay/fake_processor/billable.rb +0 -4
  29. data/lib/pay/fake_processor/subscription.rb +8 -3
  30. data/lib/pay/paddle/billable.rb +0 -4
  31. data/lib/pay/paddle/subscription.rb +2 -2
  32. data/lib/pay/paddle/webhooks/signature_verifier.rb +45 -41
  33. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +7 -2
  34. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  35. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +7 -7
  36. data/lib/pay/paddle/webhooks/subscription_updated.rb +15 -15
  37. data/lib/pay/paddle.rb +14 -2
  38. data/lib/pay/receipts.rb +106 -32
  39. data/lib/pay/stripe/billable.rb +50 -18
  40. data/lib/pay/stripe/charge.rb +93 -11
  41. data/lib/pay/stripe/merchant.rb +1 -1
  42. data/lib/pay/stripe/subscription.rb +132 -30
  43. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
  44. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
  45. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +1 -8
  46. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +21 -2
  47. data/lib/pay/stripe/webhooks/payment_action_required.rb +6 -6
  48. data/lib/pay/stripe/webhooks/subscription_renewing.rb +6 -10
  49. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -1
  50. data/lib/pay/stripe.rb +10 -1
  51. data/lib/pay/version.rb +1 -1
  52. data/lib/pay.rb +39 -4
  53. data/lib/tasks/pay.rake +1 -1
  54. metadata +3 -4
  55. data/lib/pay/merchant.rb +0 -37
@@ -15,7 +15,7 @@ module Pay
15
15
  # pay_charge = Pay::Braintree::Billable.new(pay_customer).save_transaction(subscription.transactions.first)
16
16
 
17
17
  # if Pay.send_emails
18
- # Pay::UserMailer.with(pay_customer: pay_charge.customer, charge: pay_charge).receipt.deliver_later
18
+ # Pay.mailer.with(pay_customer: pay_charge.customer, charge: pay_charge).receipt.deliver_later
19
19
  # end
20
20
  end
21
21
  end
@@ -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.to_i / subunit_to_unit.to_f,
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 defined? ::Stripe
22
- Pay::Braintree.setup if defined? ::Braintree
23
- Pay::Paddle.setup if defined? ::PaddlePay
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
- Pay::Charge.include Pay::Receipts if defined? ::Receipts::Receipt
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
@@ -79,10 +79,6 @@ module Pay
79
79
  pay_payment_method
80
80
  end
81
81
 
82
- def update_email!
83
- # pass
84
- end
85
-
86
82
  def processor_subscription(subscription_id, options = {})
87
83
  pay_customer.subscriptions.find_by(processor_id: subscription_id)
88
84
  end
@@ -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
- pay_subscription.update(ends_at: Time.current, status: :canceled)
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?
@@ -53,10 +53,6 @@ module Pay
53
53
  Pay::Paddle::PaymentMethod.sync(self)
54
54
  end
55
55
 
56
- def update_email!
57
- # pass
58
- end
59
-
60
56
  def trial_end_date(subscription)
61
57
  return unless subscription.state == "trialing"
62
58
  Time.zone.parse(subscription.next_payment[:date]).end_of_day
@@ -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
- when Array
63
- s << "a:#{var.size}:{"
64
- if assoc && var.first.is_a?(Array) && (var.first.size == 2)
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 << "#{serialize(k, assoc)}#{serialize(v, assoc)}"
70
+ s << serialize(k, assoc) << serialize(v, assoc)
78
71
  end
79
- s << "}"
80
- when Struct
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}\":#{var.members.length}:{"
83
- var.members.each do |member|
84
- s << "#{serialize(member, assoc)}#{serialize(var[member], assoc)}"
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
- if var.respond_to?(:to_assoc)
99
- v = var.to_assoc
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 canceled subscriptions need this value set
13
- pay_subscription.update!(ends_at: Time.zone.parse(event.cancellation_effective_date)) if pay_subscription.ends_at.blank? && event.cancellation_effective_date.present?
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.send_emails
12
- Pay::UserMailer.with(pay_customer: pay_charge.customer, charge: pay_charge).refund.deliver_later
11
+ if Pay.send_email?(:refund, pay_charge)
12
+ Pay.mailer.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
- charge = create_charge(pay_customer, event)
21
- notify_user(charge)
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
- charge = pay_customer.charges.find_or_initialize_by(processor_id: event.subscription_payment_id)
37
- charge.update!(attributes)
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
- charge
42
+ pay_charge
43
43
  end
44
44
 
45
45
  def notify_user(pay_charge)
46
- if Pay.send_emails
47
- Pay::UserMailer.with(pay_customer: pay_charge.customer, charge: pay_charge).receipt.deliver_later
46
+ if Pay.send_email?(:receipt, pay_charge)
47
+ Pay.mailer.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
- subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event["subscription_id"])
6
+ pay_subscription = Pay::Subscription.find_by_processor_and_id(:paddle, event["subscription_id"])
7
7
 
8
- return if subscription.nil?
8
+ return if pay_subscription.nil?
9
9
 
10
10
  case event["status"]
11
11
  when "deleted"
12
- subscription.status = "canceled"
13
- subscription.ends_at = Time.zone.parse(event["next_bill_date"]) || Time.current if subscription.ends_at.blank?
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
- subscription.status = "trialing"
16
- subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
15
+ pay_subscription.status = "trialing"
16
+ pay_subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
17
17
  when "active"
18
- subscription.status = "active"
19
- subscription.paddle_paused_from = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
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
- subscription.status = event["status"]
21
+ pay_subscription.status = event["status"]
22
22
  end
23
23
 
24
- subscription.quantity = event["new_quantity"]
25
- subscription.processor_plan = event["subscription_plan_id"]
26
- subscription.paddle_update_url = event["update_url"]
27
- subscription.paddle_cancel_url = event["cancel_url"]
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
- subscription.ends_at = subscription.trial_ends_at if subscription.on_trial?
30
+ pay_subscription.ends_at = pay_subscription.trial_ends_at if pay_subscription.on_trial?
31
31
 
32
- subscription.save!
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 receipt_pdf(**options)
17
- line_items = [
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.account_billed"), "#{customer.customer_name} (#{customer.email})"],
20
- [I18n.t("pay.receipt.product"), product],
21
- [I18n.t("pay.receipt.amount"), Pay::Currency.format(amount, currency: currency)],
22
- [I18n.t("pay.receipt.charged_to"), charged_to]
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
- line_items << [I18n.t("pay.receipt.additional_info"), customer.owner.extra_billing_info] if customer.owner.try(:extra_billing_info?)
25
- line_items << [I18n.t("pay.receipt.refunded"), Pay::Currency.format(amount_refunded, currency: currency)] if refunded?
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
- id: id,
29
- product: product,
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: 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 invoice_pdf(**options)
51
- bill_to = [customer.owner.name]
52
- bill_to += [customer.owner.extra_billing_info] if customer.owner.try(:extra_billing_info?)
53
- bill_to += [nil, customer.owner.email]
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
- id: id,
66
- issue_date: I18n.l(created_at, format: :long),
67
- due_date: I18n.l(created_at, format: :long),
68
- status: "<b><color rgb='#5eba7d'>#{I18n.t("pay.receipt.paid").upcase}</color></b>",
69
- bill_to: bill_to,
70
- product: product,
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: 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 line_items
84
- line_items
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