pay 11.7.2 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/controllers/pay/payments_controller.rb +5 -12
  4. data/app/controllers/pay/webhooks/base_controller.rb +50 -0
  5. data/app/controllers/pay/webhooks/braintree_controller.rb +11 -18
  6. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +8 -26
  7. data/app/controllers/pay/webhooks/paddle_billing_controller.rb +13 -35
  8. data/app/controllers/pay/webhooks/paddle_classic_controller.rb +5 -20
  9. data/app/controllers/pay/webhooks/stripe_controller.rb +13 -29
  10. data/app/models/pay/braintree/charge.rb +6 -12
  11. data/app/models/pay/braintree/payment_method.rb +7 -5
  12. data/app/models/pay/braintree/subscription.rb +39 -44
  13. data/app/models/pay/charge.rb +1 -1
  14. data/app/models/pay/customer.rb +1 -9
  15. data/app/models/pay/lemon_squeezy/charge.rb +63 -61
  16. data/app/models/pay/lemon_squeezy/customer.rb +1 -1
  17. data/app/models/pay/lemon_squeezy/subscription.rb +43 -46
  18. data/app/models/pay/merchant.rb +1 -1
  19. data/app/models/pay/paddle_billing/charge.rb +43 -53
  20. data/app/models/pay/paddle_billing/customer.rb +1 -1
  21. data/app/models/pay/paddle_billing/payment_method.rb +19 -23
  22. data/app/models/pay/paddle_billing/subscription.rb +56 -67
  23. data/app/models/pay/paddle_classic/customer.rb +1 -1
  24. data/app/models/pay/paddle_classic/subscription.rb +42 -44
  25. data/app/models/pay/payment_method.rb +1 -5
  26. data/app/models/pay/stripe/charge.rb +54 -59
  27. data/app/models/pay/stripe/customer.rb +45 -23
  28. data/app/models/pay/stripe/merchant.rb +1 -1
  29. data/app/models/pay/stripe/payment_method.rb +27 -29
  30. data/app/models/pay/stripe/subscription.rb +128 -119
  31. data/app/models/pay/subscription.rb +2 -12
  32. data/app/views/layouts/pay/application.html.erb +0 -2
  33. data/app/views/pay/payments/show.html.erb +19 -8
  34. data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
  35. data/app/views/pay/user_mailer/payment_action_required.text.erb +1 -1
  36. data/lib/pay/braintree/webhooks/subscription.rb +15 -0
  37. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -7
  38. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -7
  39. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -7
  40. data/lib/pay/braintree/webhooks/subscription_went_active.rb +2 -8
  41. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +2 -8
  42. data/lib/pay/braintree.rb +1 -2
  43. data/lib/pay/currency.rb +0 -4
  44. data/lib/pay/engine.rb +4 -0
  45. data/lib/pay/errors.rb +4 -0
  46. data/lib/pay/lemon_squeezy.rb +4 -7
  47. data/lib/pay/paddle_billing.rb +4 -1
  48. data/lib/pay/paddle_classic.rb +4 -1
  49. data/lib/pay/payment.rb +11 -17
  50. data/lib/pay/receipts.rb +0 -1
  51. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +8 -4
  52. data/lib/pay/stripe/webhooks/invoice_updated.rb +4 -1
  53. data/lib/pay/stripe/webhooks/payment_action_required.rb +3 -2
  54. data/lib/pay/stripe/webhooks/subscription_created.rb +2 -4
  55. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -5
  56. data/lib/pay/stripe.rb +3 -3
  57. data/lib/pay/sync.rb +29 -0
  58. data/lib/pay/version.rb +1 -1
  59. data/lib/pay.rb +7 -5
  60. metadata +5 -3
  61. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +0 -24
@@ -1,80 +1,82 @@
1
1
  module Pay
2
2
  module LemonSqueezy
3
3
  class Charge < Pay::Charge
4
+ extend Pay::Sync
5
+
4
6
  # LemonSqueezy uses Order for one-time payments and Order + Subscription + SubscriptionInvoice for subscriptions
7
+ # Charges are stored with a "order:123" or "subscription_invoice:123" processor_id so both can be synced by ID
5
8
 
6
- def self.sync_order(order_id, object: nil, try: 0, retries: 1)
7
- object ||= ::LemonSqueezy::Order.retrieve(id: order_id)
9
+ def self.sync(processor_id, object: nil)
10
+ type, id = processor_id.split(":", 2)
11
+ case type
12
+ when "order"
13
+ sync_order(id, object: object)
14
+ when "subscription_invoice"
15
+ sync_subscription_invoice(id, object: object)
16
+ end
17
+ end
8
18
 
9
- pay_customer = Pay::LemonSqueezy::Customer.find_by(processor_id: object.customer_id)
10
- return unless pay_customer
19
+ def self.sync_order(order_id, object: nil)
20
+ sync_with_retries do
21
+ order = object || ::LemonSqueezy::Order.retrieve(id: order_id)
22
+ return unless (pay_customer = find_pay_customer(order.customer_id))
11
23
 
12
- processor_id = "order:#{object.id}"
13
- attributes = {
14
- processor_id: processor_id,
15
- currency: object.currency,
16
- subtotal: object.subtotal,
17
- tax: object.tax,
18
- amount: object.total,
19
- amount_refunded: object.refunded_amount,
20
- created_at: (object.created_at ? Time.parse(object.created_at) : nil),
21
- updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
22
- }
24
+ processor_id = "order:#{order.id}"
25
+ attributes = {
26
+ processor_id: processor_id,
27
+ currency: order.currency,
28
+ subtotal: order.subtotal,
29
+ tax: order.tax,
30
+ amount: order.total,
31
+ amount_refunded: order.refunded_amount,
32
+ created_at: (order.created_at ? Time.parse(order.created_at) : nil),
33
+ updated_at: (order.updated_at ? Time.parse(order.updated_at) : nil)
34
+ }
23
35
 
24
- # Update or create the charge
25
- if (pay_charge = find_by(customer: pay_customer, processor_id: processor_id))
26
- pay_charge.with_lock { pay_charge.update!(attributes) }
27
- pay_charge
28
- else
29
- create!(attributes.merge(customer: pay_customer, processor_id: processor_id))
36
+ # Update or create the charge
37
+ if (pay_charge = find_by(customer: pay_customer, processor_id: processor_id))
38
+ pay_charge.with_lock { pay_charge.update!(attributes) }
39
+ pay_charge
40
+ else
41
+ create!(attributes.merge(customer: pay_customer, processor_id: processor_id))
42
+ end
30
43
  end
31
44
  end
32
45
 
33
46
  def self.sync_subscription_invoice(subscription_invoice_id, object: nil)
34
- # Skip loading the latest subscription invoice details from the API if we already have it
35
- object ||= ::LemonSqueezy::SubscriptionInvoice.retrieve(id: subscription_invoice_id)
36
-
37
- pay_customer = Pay::LemonSqueezy::Customer.find_by(processor_id: object.customer_id)
38
- return unless pay_customer
47
+ sync_with_retries do
48
+ invoice = object || ::LemonSqueezy::SubscriptionInvoice.retrieve(id: subscription_invoice_id)
49
+ return unless (pay_customer = find_pay_customer(invoice.customer_id))
39
50
 
40
- processor_id = "subscription_invoice:#{object.id}"
41
- subscription = Pay::LemonSqueezy::Subscription.find_by(processor_id: object.subscription_id)
42
- attributes = {
43
- processor_id: processor_id,
44
- currency: object.currency,
45
- amount: object.total,
46
- amount_refunded: object.refunded_amount,
47
- subtotal: object.subtotal,
48
- tax: object.tax,
49
- subscription: subscription,
50
- payment_method_type: ("card" if object.card_brand.present?),
51
- brand: object.card_brand,
52
- last4: object.card_last_four,
53
- created_at: (object.created_at ? Time.parse(object.created_at) : nil),
54
- updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
55
- }
51
+ processor_id = "subscription_invoice:#{invoice.id}"
52
+ subscription = Pay::LemonSqueezy::Subscription.find_by(processor_id: invoice.subscription_id)
53
+ attributes = {
54
+ processor_id: processor_id,
55
+ currency: invoice.currency,
56
+ amount: invoice.total,
57
+ amount_refunded: invoice.refunded_amount,
58
+ subtotal: invoice.subtotal,
59
+ tax: invoice.tax,
60
+ subscription: subscription,
61
+ payment_method_type: ("card" if invoice.card_brand.present?),
62
+ brand: invoice.card_brand,
63
+ last4: invoice.card_last_four,
64
+ created_at: (invoice.created_at ? Time.parse(invoice.created_at) : nil),
65
+ updated_at: (invoice.updated_at ? Time.parse(invoice.updated_at) : nil)
66
+ }
56
67
 
57
- # Update customer's payment method
58
- Pay::LemonSqueezy::PaymentMethod.sync(pay_customer: pay_customer, attributes: object)
68
+ # Update customer's payment method
69
+ Pay::LemonSqueezy::PaymentMethod.sync(pay_customer: pay_customer, attributes: invoice)
59
70
 
60
- # Update or create the charge
61
- if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
62
- pay_charge.with_lock do
63
- pay_charge.update!(attributes)
71
+ # Update or create the charge
72
+ if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
73
+ pay_charge.with_lock do
74
+ pay_charge.update!(attributes)
75
+ end
76
+ pay_charge
77
+ else
78
+ pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
64
79
  end
65
- pay_charge
66
- else
67
- pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
68
- end
69
- end
70
-
71
- def save
72
- ls_type, ls_id = processor_id.split(":", 2)
73
- case ls_type
74
- when "order"
75
- self.class.sync_order(ls_id)
76
- when "subscription_invoice"
77
- self.class.sync_subscription_invoice(ls_id)
78
80
  end
79
81
  end
80
82
 
@@ -41,7 +41,7 @@ module Pay
41
41
  end
42
42
 
43
43
  def charge(amount, options = {})
44
- raise Pay::Error, "LemonSqueezy does not support one-off charges"
44
+ raise Pay::NotSupportedError, "Lemon Squeezy does not support one-off charges"
45
45
  end
46
46
 
47
47
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
@@ -1,43 +1,46 @@
1
1
  module Pay
2
2
  module LemonSqueezy
3
3
  class Subscription < Pay::Subscription
4
- def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
5
- object ||= ::LemonSqueezy::Subscription.retrieve(id: subscription_id)
6
-
7
- pay_customer = Pay::Customer.find_by(processor: :lemon_squeezy, processor_id: object.customer_id)
8
- return unless pay_customer
9
-
10
- attributes = {
11
- current_period_end: object.renews_at,
12
- ends_at: (object.ends_at ? Time.parse(object.ends_at) : nil),
13
- pause_starts_at: (object.pause&.resumes_at ? Time.parse(object.pause.resumes_at) : nil),
14
- status: object.status,
15
- processor_plan: object.first_subscription_item.price_id,
16
- quantity: object.first_subscription_item.quantity,
17
- created_at: (object.created_at ? Time.parse(object.created_at) : nil),
18
- updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
19
- }
20
-
21
- case attributes[:status]
22
- when "cancelled"
23
- # Remove payment methods since customer cannot be reused after cancelling
24
- Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
25
- when "on_trial"
26
- attributes[:trial_ends_at] = Time.parse(object.trial_ends_at)
27
- when "paused"
28
- # attributes[:pause_starts_at] = Time.parse(object.paused_at)
29
- when "active", "past_due"
30
- attributes[:trial_ends_at] = nil
31
- attributes[:pause_starts_at] = nil
32
- attributes[:ends_at] = nil
33
- end
4
+ extend Pay::Sync
34
5
 
35
- # Update or create the subscription
36
- if (pay_subscription = find_by(customer: pay_customer, processor_id: object.id))
37
- pay_subscription.with_lock { pay_subscription.update!(attributes) }
38
- pay_subscription
39
- else
40
- create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
6
+ # Lemon Squeezy statuses that Pay spells differently
7
+ STATUSES = {"on_trial" => "trialing", "cancelled" => "canceled"}.freeze
8
+
9
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
10
+ sync_with_retries do
11
+ subscription = object || ::LemonSqueezy::Subscription.retrieve(id: subscription_id)
12
+ return unless (pay_customer = find_pay_customer(subscription.customer_id))
13
+
14
+ attributes = {
15
+ current_period_end: subscription.renews_at,
16
+ ends_at: (subscription.ends_at ? Time.parse(subscription.ends_at) : nil),
17
+ pause_resumes_at: (subscription.pause&.resumes_at ? Time.parse(subscription.pause.resumes_at) : nil),
18
+ status: STATUSES.fetch(subscription.status, subscription.status),
19
+ processor_plan: subscription.first_subscription_item.price_id,
20
+ quantity: subscription.first_subscription_item.quantity,
21
+ created_at: (subscription.created_at ? Time.parse(subscription.created_at) : nil),
22
+ updated_at: (subscription.updated_at ? Time.parse(subscription.updated_at) : nil)
23
+ }
24
+
25
+ case attributes[:status]
26
+ when "canceled"
27
+ # Remove payment methods since customer cannot be reused after cancelling
28
+ pay_customer.payment_methods.destroy_all
29
+ when "trialing"
30
+ attributes[:trial_ends_at] = Time.parse(subscription.trial_ends_at)
31
+ when "active", "past_due"
32
+ attributes[:trial_ends_at] = nil
33
+ attributes[:pause_resumes_at] = nil
34
+ attributes[:ends_at] = nil
35
+ end
36
+
37
+ # Update or create the subscription
38
+ if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription.id))
39
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
40
+ pay_subscription
41
+ else
42
+ create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription.id))
43
+ end
41
44
  end
42
45
  end
43
46
 
@@ -64,7 +67,7 @@ module Pay
64
67
  end
65
68
 
66
69
  def cancel_now!(**options)
67
- raise Pay::Error, "Lemon Squeezy does not support cancelling immediately through the API."
70
+ raise Pay::NotSupportedError, "Lemon Squeezy does not support cancelling immediately through the API"
68
71
  end
69
72
 
70
73
  def change_quantity(quantity, **options)
@@ -75,19 +78,13 @@ module Pay
75
78
  raise Pay::LemonSqueezy::Error, e
76
79
  end
77
80
 
78
- # A subscription could be set to cancel or pause in the future
79
- # It is considered on grace period until the cancel or pause time begins
80
- def on_grace_period?
81
- (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
82
- end
83
-
84
81
  def paused?
85
82
  status == "paused"
86
83
  end
87
84
 
88
85
  def pause(**options)
89
86
  response = ::LemonSqueezy::Subscription.pause(id: processor_id, **options)
90
- update!(status: :paused, pause_starts_at: response.pause&.resumes_at)
87
+ update!(status: :paused, pause_resumes_at: response.pause&.resumes_at)
91
88
  rescue ::LemonSqueezy::Error => e
92
89
  raise Pay::LemonSqueezy::Error, e
93
90
  end
@@ -101,13 +98,13 @@ module Pay
101
98
  raise Error, "You can only resume paused or cancelled subscriptions"
102
99
  end
103
100
 
104
- if paused? && pause_starts_at? && Time.current < pause_starts_at
101
+ if paused?
105
102
  ::LemonSqueezy::Subscription.unpause(id: processor_id)
106
103
  else
107
104
  ::LemonSqueezy::Subscription.uncancel(id: processor_id)
108
105
  end
109
106
 
110
- update(ends_at: nil, status: :active, pause_starts_at: nil)
107
+ update(ends_at: nil, status: :active, pause_resumes_at: nil)
111
108
  rescue ::LemonSqueezy::Error => e
112
109
  raise Pay::LemonSqueezy::Error, e
113
110
  end
@@ -7,7 +7,7 @@ module Pay
7
7
  store_accessor :data, :onboarding_complete
8
8
 
9
9
  def onboarding_complete?
10
- ActiveModel::Type::Boolean.new.cast(data&.fetch("onboarding_complete")) || false
10
+ ActiveModel::Type::Boolean.new.cast(onboarding_complete) || false
11
11
  end
12
12
  end
13
13
  end
@@ -1,66 +1,56 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
3
  class Charge < Pay::Charge
4
- store_accessor :data, :paddle_receipt_url
5
-
6
- def self.sync(charge_id, object: nil, try: 0, retries: 1)
7
- # Skip loading the latest charge details from the API if we already have it
8
- object ||= ::Paddle::Transaction.retrieve(id: charge_id)
9
-
10
- # Ignore transactions that aren't completed
11
- return unless object.status == "completed"
4
+ extend Pay::Sync
12
5
 
13
- # Ignore charges without a Customer
14
- return if object.customer_id.blank?
15
-
16
- pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
17
- return unless pay_customer
6
+ store_accessor :data, :paddle_receipt_url
18
7
 
19
- # Ignore transactions that are payment method changes
20
- # But update the customer's payment method
21
- if object.origin == "subscription_payment_method_change"
22
- Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
23
- return
24
- end
8
+ def self.sync(charge_id, object: nil)
9
+ sync_with_retries do
10
+ transaction = object || ::Paddle::Transaction.retrieve(id: charge_id)
25
11
 
26
- attrs = {
27
- amount: object.details.totals.grand_total,
28
- created_at: object.created_at,
29
- currency: object.currency_code,
30
- metadata: object.details.line_items&.first&.id,
31
- subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
32
- }
12
+ # Ignore transactions that aren't completed
13
+ return unless transaction.status == "completed"
14
+ return unless (pay_customer = find_pay_customer(transaction.customer_id))
33
15
 
34
- if (details = Array.wrap(object.payments).first&.method_details)
35
- case details.type.downcase
36
- when "card"
37
- attrs[:payment_method_type] = "card"
38
- attrs[:brand] = details.card.type
39
- attrs[:exp_month] = details.card.expiry_month
40
- attrs[:exp_year] = details.card.expiry_year
41
- attrs[:last4] = details.card.last4
42
- when "paypal"
43
- attrs[:payment_method_type] = "paypal"
16
+ # Ignore transactions that are payment method changes
17
+ # But update the customer's payment method
18
+ if transaction.origin == "subscription_payment_method_change"
19
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: transaction.payments.first)
20
+ return
44
21
  end
45
22
 
46
- # Update customer's payment method
47
- Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
48
- end
23
+ attrs = {
24
+ amount: transaction.details.totals.grand_total,
25
+ created_at: transaction.created_at,
26
+ currency: transaction.currency_code,
27
+ metadata: transaction.details.line_items&.first&.id,
28
+ subscription: pay_customer.subscriptions.find_by(processor_id: transaction.subscription_id)
29
+ }
30
+
31
+ if (details = Array.wrap(transaction.payments).first&.method_details)
32
+ case details.type.downcase
33
+ when "card"
34
+ attrs[:payment_method_type] = "card"
35
+ attrs[:brand] = details.card.type
36
+ attrs[:exp_month] = details.card.expiry_month
37
+ attrs[:exp_year] = details.card.expiry_year
38
+ attrs[:last4] = details.card.last4
39
+ when "paypal"
40
+ attrs[:payment_method_type] = "paypal"
41
+ end
42
+
43
+ # Update customer's payment method
44
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: transaction.payments.first)
45
+ end
49
46
 
50
- # Update or create the charge
51
- if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
52
- pay_charge.with_lock { pay_charge.update!(attrs) }
53
- pay_charge
54
- else
55
- create!(attrs.merge(customer: pay_customer, processor_id: object.id))
56
- end
57
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
58
- try += 1
59
- if try <= retries
60
- sleep 0.1
61
- retry
62
- else
63
- raise
47
+ # Update or create the charge
48
+ if (pay_charge = find_by(customer: pay_customer, processor_id: transaction.id))
49
+ pay_charge.with_lock { pay_charge.update!(attrs) }
50
+ pay_charge
51
+ else
52
+ create!(attrs.merge(customer: pay_customer, processor_id: transaction.id))
53
+ end
64
54
  end
65
55
  end
66
56
  end
@@ -60,7 +60,7 @@ module Pay
60
60
  end
61
61
 
62
62
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
63
- # pass
63
+ raise Pay::NotSupportedError, "Paddle Billing subscriptions are created with Paddle Checkout and synced from webhooks"
64
64
  end
65
65
 
66
66
  # Paddle does not use payment method tokens. The method signature has it here
@@ -1,6 +1,8 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
3
  class PaymentMethod < Pay::PaymentMethod
4
+ extend Pay::Sync
5
+
4
6
  def self.sync_from_transaction(pay_customer:, transaction:)
5
7
  transaction = ::Paddle::Transaction.retrieve(id: transaction)
6
8
  return unless transaction.status == "completed"
@@ -8,30 +10,24 @@ module Pay
8
10
  sync(pay_customer: pay_customer, attributes: transaction.payments.first)
9
11
  end
10
12
 
11
- def self.sync(pay_customer:, attributes:, try: 0, retries: 1)
12
- details = attributes.method_details
13
- attrs = {
14
- payment_method_type: details.type.downcase
15
- }
16
-
17
- case details.type.downcase
18
- when "card"
19
- attrs[:brand] = details.card.type
20
- attrs[:last4] = details.card.last4
21
- attrs[:exp_month] = details.card.expiry_month
22
- attrs[:exp_year] = details.card.expiry_year
23
- end
13
+ def self.sync(pay_customer:, attributes:)
14
+ sync_with_retries do
15
+ details = attributes.method_details
16
+ attrs = {
17
+ payment_method_type: details.type.downcase
18
+ }
19
+
20
+ case details.type.downcase
21
+ when "card"
22
+ attrs[:brand] = details.card.type
23
+ attrs[:last4] = details.card.last4
24
+ attrs[:exp_month] = details.card.expiry_month
25
+ attrs[:exp_year] = details.card.expiry_year
26
+ end
24
27
 
25
- payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
26
- payment_method.update!(attrs)
27
- payment_method
28
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
29
- try += 1
30
- if try <= retries
31
- sleep 0.1
32
- retry
33
- else
34
- raise
28
+ payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
29
+ payment_method.update!(attrs)
30
+ payment_method
35
31
  end
36
32
  end
37
33
 
@@ -1,75 +1,68 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
3
  class Subscription < Pay::Subscription
4
+ extend Pay::Sync
5
+
4
6
  store_accessor :data, :paddle_update_url
5
7
  store_accessor :data, :paddle_cancel_url
6
8
 
7
9
  def self.sync_from_transaction(transaction_id)
8
- transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
9
- sync(transaction.subscription_id) if transaction.subscription_id
10
+ Pay.deprecator.warn "Pay::PaddleBilling::Subscription.sync_from_transaction is deprecated, use Pay::PaddleBilling.sync_transaction instead"
11
+ Pay::PaddleBilling.sync_transaction(transaction_id)
10
12
  end
11
13
 
12
- def self.sync(subscription_id, object: nil, name: Pay.default_product_name, try: 0, retries: 1)
13
- # Passthrough is not return from this API, so we can't use that
14
- object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
15
-
16
- pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
17
- return unless pay_customer
18
-
19
- attributes = {
20
- current_period_end: object.current_billing_period&.ends_at,
21
- current_period_start: object.current_billing_period&.starts_at,
22
- ends_at: (object.canceled_at ? Time.parse(object.canceled_at) : nil),
23
- metadata: object.custom_data,
24
- paddle_cancel_url: object.management_urls&.cancel,
25
- paddle_update_url: object.management_urls&.update_payment_method,
26
- pause_starts_at: (object.paused_at ? Time.parse(object.paused_at) : nil),
27
- status: object.status
28
- }
29
-
30
- if object.items&.first
31
- item = object.items.first
32
- attributes[:processor_plan] = item.price.id
33
- attributes[:quantity] = item.quantity
34
- end
35
-
36
- case attributes[:status]
37
- when "canceled"
38
- # Remove payment methods since customer cannot be reused after cancelling
39
- Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
40
- when "trialing"
41
- attributes[:trial_ends_at] = Time.parse(object.next_billed_at) if object.next_billed_at
42
- when "paused"
43
- attributes[:pause_starts_at] = Time.parse(object.paused_at) if object.paused_at
44
- when "active", "past_due"
45
- attributes[:trial_ends_at] = nil
46
- attributes[:pause_starts_at] = nil
47
- attributes[:ends_at] = nil
48
- end
49
-
50
- case object.scheduled_change&.action
51
- when "cancel"
52
- attributes[:ends_at] = Time.parse(object.scheduled_change.effective_at)
53
- when "pause"
54
- attributes[:pause_starts_at] = Time.parse(object.scheduled_change.effective_at)
55
- when "resume"
56
- attributes[:pause_resumes_at] = Time.parse(object.scheduled_change.effective_at)
57
- end
58
-
59
- # Update or create the subscription
60
- if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription_id))
61
- pay_subscription.with_lock { pay_subscription.update!(attributes) }
62
- pay_subscription
63
- else
64
- create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription_id))
65
- end
66
- rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
67
- try += 1
68
- if try <= retries
69
- sleep 0.1
70
- retry
71
- else
72
- raise
14
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
15
+ sync_with_retries do
16
+ subscription = object || ::Paddle::Subscription.retrieve(id: subscription_id)
17
+ return unless (pay_customer = find_pay_customer(subscription.customer_id))
18
+
19
+ attributes = {
20
+ current_period_end: subscription.current_billing_period&.ends_at,
21
+ current_period_start: subscription.current_billing_period&.starts_at,
22
+ ends_at: (subscription.canceled_at ? Time.parse(subscription.canceled_at) : nil),
23
+ metadata: subscription.custom_data,
24
+ paddle_cancel_url: subscription.management_urls&.cancel,
25
+ paddle_update_url: subscription.management_urls&.update_payment_method,
26
+ pause_starts_at: (subscription.paused_at ? Time.parse(subscription.paused_at) : nil),
27
+ status: subscription.status
28
+ }
29
+
30
+ if subscription.items&.first
31
+ item = subscription.items.first
32
+ attributes[:processor_plan] = item.price.id
33
+ attributes[:quantity] = item.quantity
34
+ end
35
+
36
+ case attributes[:status]
37
+ when "canceled"
38
+ # Remove payment methods since customer cannot be reused after cancelling
39
+ pay_customer.payment_methods.destroy_all
40
+ when "trialing"
41
+ attributes[:trial_ends_at] = Time.parse(subscription.next_billed_at) if subscription.next_billed_at
42
+ when "paused"
43
+ attributes[:pause_starts_at] = Time.parse(subscription.paused_at) if subscription.paused_at
44
+ when "active", "past_due"
45
+ attributes[:trial_ends_at] = nil
46
+ attributes[:pause_starts_at] = nil
47
+ attributes[:ends_at] = nil
48
+ end
49
+
50
+ case subscription.scheduled_change&.action
51
+ when "cancel"
52
+ attributes[:ends_at] = Time.parse(subscription.scheduled_change.effective_at)
53
+ when "pause"
54
+ attributes[:pause_starts_at] = Time.parse(subscription.scheduled_change.effective_at)
55
+ when "resume"
56
+ attributes[:pause_resumes_at] = Time.parse(subscription.scheduled_change.effective_at)
57
+ end
58
+
59
+ # Update or create the subscription
60
+ if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription_id))
61
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
62
+ pay_subscription
63
+ else
64
+ create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription_id))
65
+ end
73
66
  end
74
67
  end
75
68
 
@@ -175,10 +168,6 @@ module Pay
175
168
  )
176
169
  update(processor_plan: plan, ends_at: nil, status: :active)
177
170
  end
178
-
179
- # Retries the latest invoice for a Past Due subscription
180
- def retry_failed_payment
181
- end
182
171
  end
183
172
  end
184
173
  end
@@ -35,7 +35,7 @@ module Pay
35
35
  end
36
36
 
37
37
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
38
- # pass
38
+ raise Pay::NotSupportedError, "Paddle Classic subscriptions are created with Paddle Checkout and synced from webhooks"
39
39
  end
40
40
 
41
41
  # Paddle does not use payment method tokens. The method signature has it here