pay 7.1.1 → 7.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5976a641a8e7a7eafa74d343fa1bdb2ab4c1b07d234d44e1efafba5b9b34b34d
4
- data.tar.gz: 57f00237ae29f1a611f714f473a05de859277cdc5bb515741cefa5f75426b393
3
+ metadata.gz: 690bb7ad1078274199d3a9c3c32b5ace7fb405bf0c37f0bcabeffeb8af734902
4
+ data.tar.gz: 1e0df19aea4d907f3878450fcbb320421a3abb7da1fe0187ba7bde1878267d60
5
5
  SHA512:
6
- metadata.gz: '0396532d1af2f80400facc5f6294d7b2f6bcbeab373d004a241e05eb8213a4c91c09ae3fd28655501fec298cfb75520c8506ce697629f41e21cfc961cb76b7bc'
7
- data.tar.gz: 6e0a26efaf1eb11347ab34c2eac50a25ed59d4f6b9b8eaa11707eb6f3b0d72d4708c3dcb399cbf915999343fd35625397c00c800f90853634db8da00c24643a7
6
+ metadata.gz: a0767157a5d6ecc7a710e71047080b4e5d6edbdf4a27fb7f2fcfbf585558b68b43af5f483d87815668dcc4d85128f3fe6c098a5fc2899195529142a1ba3682fa
7
+ data.tar.gz: 400de12e3525fc3395ba4bf4dd8b2df158de62cb18db60a298d51acec1fe1a16fa0c32f0f3750f9f8a01f0337de9f71d61b33628e0c39848508dc818c85f5356
data/README.md CHANGED
@@ -44,7 +44,7 @@ Want to add a new payment provider? Contributions are welcome.
44
44
  * **Payment Processors**
45
45
  * [Stripe](docs/stripe/1_overview.md)
46
46
  * [Braintree](docs/braintree/1_overview.md)
47
- * [Paddle](docs/paddle/1_overview.md)
47
+ * [Paddle](docs/paddle_billing/1_overview.md)
48
48
  * [Fake Processor](docs/fake_processor/1_overview.md)
49
49
  * **Marketplaces**
50
50
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
@@ -55,6 +55,10 @@ Want to add a new payment provider? Contributions are welcome.
55
55
 
56
56
  If you have an issue you'd like to submit, please do so using the issue tracker in GitHub. In order for us to help you in the best way possible, please be as detailed as you can.
57
57
 
58
+ For those using devcontainers, if you want to test the application with different databases:
59
+ 1. Uncomment the `DATABASE_URL` corresponding to the database type you wish to use in the `.devcontainer/devcontainer.json` file.
60
+ 2. Rebuild the devcontainer, which will configure the application to use the selected database for your development environment.
61
+
58
62
  If you'd like to open a PR please make sure the following things pass:
59
63
 
60
64
  ```ruby
@@ -9,11 +9,11 @@ module Pay
9
9
 
10
10
  # Scopes
11
11
  scope :for_name, ->(name) { where(name: name) }
12
- scope :on_trial, -> { where("trial_ends_at > ?", Time.current) }
12
+ scope :on_trial, -> { where(status: ["trialing", "active"]).where("trial_ends_at > ?", Time.current) }
13
13
  scope :canceled, -> { where.not(ends_at: nil) }
14
14
  scope :cancelled, -> { canceled }
15
15
  scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
16
- scope :active, -> { where(status: ["trialing", "active"]).pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).where("trial_ends_at IS NULL OR trial_ends_at > ?", Time.current) }
16
+ scope :active, -> { where(status: "active").pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).or(on_trial) }
17
17
  scope :paused, -> { where(status: "paused").or(where("pause_starts_at <= ?", Time.current)) }
18
18
  scope :pause_not_started, -> { where("pause_starts_at IS NULL OR pause_starts_at > ?", Time.current) }
19
19
  scope :active_or_paused, -> { active.or(paused) }
@@ -28,7 +28,8 @@ module Pay
28
28
  # Make to generate a processor_id
29
29
  customer
30
30
 
31
- attributes = options.merge(
31
+ valid_attributes = options.slice(*Pay::Charge.attribute_names.map(&:to_sym))
32
+ attributes = {
32
33
  processor_id: NanoId.generate,
33
34
  amount: amount,
34
35
  data: {
@@ -38,7 +39,7 @@ module Pay
38
39
  exp_month: Date.today.month,
39
40
  exp_year: Date.today.year
40
41
  }
41
- )
42
+ }.deep_merge(valid_attributes)
42
43
  pay_customer.charges.create!(attributes)
43
44
  end
44
45
 
@@ -0,0 +1,90 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class Billable
4
+ attr_reader :pay_customer
5
+
6
+ delegate :processor_id,
7
+ :processor_id?,
8
+ :email,
9
+ :customer_name,
10
+ :card_token,
11
+ to: :pay_customer
12
+
13
+ def initialize(pay_customer)
14
+ @pay_customer = pay_customer
15
+ end
16
+
17
+ def customer_attributes
18
+ {email: email, name: customer_name}
19
+ end
20
+
21
+ # Retrieves a Paddle::Customer object
22
+ #
23
+ # Finds an existing Paddle::Customer if processor_id exists
24
+ # Creates a new Paddle::Customer using `email` and `customer_name` if empty processor_id
25
+ #
26
+ # Returns a Paddle::Customer object
27
+ def customer
28
+ if processor_id?
29
+ ::Paddle::Customer.retrieve(id: processor_id)
30
+ else
31
+ sc = ::Paddle::Customer.create(email: email, name: customer_name)
32
+ pay_customer.update!(processor_id: sc.id)
33
+ sc
34
+ end
35
+ rescue ::Paddle::Error => e
36
+ raise Pay::PaddleBilling::Error, e
37
+ end
38
+
39
+ # Syncs name and email to Paddle::Customer
40
+ # You can also pass in other attributes that will be merged into the default attributes
41
+ def update_customer!(**attributes)
42
+ customer unless processor_id?
43
+ attrs = customer_attributes.merge(attributes)
44
+ ::Paddle::Customer.update(id: processor_id, **attrs)
45
+ end
46
+
47
+ def charge(amount, options = {})
48
+ return Pay::Error unless options
49
+
50
+ items = options[:items]
51
+ opts = options.except(:items).merge(customer_id: processor_id)
52
+ transaction = ::Paddle::Transaction.create(items: items, **opts)
53
+
54
+ attrs = {
55
+ amount: transaction.details.totals.grand_total,
56
+ created_at: transaction.created_at,
57
+ currency: transaction.currency_code,
58
+ metadata: transaction.details.line_items&.first&.id
59
+ }
60
+
61
+ charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
62
+ charge.update(attrs)
63
+ charge
64
+ rescue ::Paddle::Error => e
65
+ raise Pay::PaddleBilling::Error, e
66
+ end
67
+
68
+ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
69
+ # pass
70
+ end
71
+
72
+ # Paddle does not use payment method tokens. The method signature has it here
73
+ # to have a uniform API with the other payment processors.
74
+ def add_payment_method(token = nil, default: true)
75
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer)
76
+ end
77
+
78
+ def trial_end_date(subscription)
79
+ return unless subscription.state == "trialing"
80
+ Time.zone.parse(subscription.next_payment[:date]).end_of_day
81
+ end
82
+
83
+ def processor_subscription(subscription_id, options = {})
84
+ ::Paddle::Subscription.retrieve(id: subscription_id, **options)
85
+ rescue ::Paddle::Error => e
86
+ raise Pay::PaddleBilling::Error, e
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,68 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class Charge
4
+ attr_reader :pay_charge
5
+
6
+ delegate :processor_id, :customer, to: :pay_charge
7
+
8
+ def initialize(pay_charge)
9
+ @pay_charge = pay_charge
10
+ end
11
+
12
+ def self.sync(charge_id, object: nil, try: 0, retries: 1)
13
+ # Skip loading the latest charge details from the API if we already have it
14
+ object ||= ::Paddle::Transaction.retrieve(id: charge_id)
15
+
16
+ # Ignore transactions that aren't completed
17
+ return unless object.status == "completed"
18
+
19
+ # Ignore charges without a Customer
20
+ return if object.customer_id.blank?
21
+
22
+ pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
23
+ return unless pay_customer
24
+
25
+ # Ignore transactions that are payment method changes
26
+ # But update the customer's payment method
27
+ if object.origin == "subscription_payment_method_change"
28
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
29
+ return
30
+ end
31
+
32
+ attrs = {
33
+ amount: object.details.totals.grand_total,
34
+ created_at: object.created_at,
35
+ currency: object.currency_code,
36
+ metadata: object.details.line_items&.first&.id,
37
+ subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
38
+ }
39
+
40
+ if object.payment
41
+ case object.payment.method_details.type.downcase
42
+ when "card"
43
+ attrs[:payment_method_type] = "card"
44
+ attrs[:brand] = details.card.type
45
+ attrs[:exp_month] = details.card.expiry_month
46
+ attrs[:exp_year] = details.card.expiry_year
47
+ attrs[:last4] = details.card.last4
48
+ when "paypal"
49
+ attrs[:payment_method_type] = "paypal"
50
+ end
51
+
52
+ # Update customer's payment method
53
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
54
+ end
55
+
56
+ # Update or create the charge
57
+ if (pay_charge = pay_customer.charges.find_by(processor_id: object.id))
58
+ pay_charge.with_lock do
59
+ pay_charge.update!(attrs)
60
+ end
61
+ pay_charge
62
+ else
63
+ pay_customer.charges.create!(attrs.merge(processor_id: object.id))
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,7 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class Error < Pay::Error
4
+ delegate :message, to: :cause
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class PaymentMethod
4
+ attr_reader :pay_payment_method
5
+
6
+ delegate :customer, :processor_id, to: :pay_payment_method
7
+
8
+ def self.sync(pay_customer:, attributes:)
9
+ details = attributes.method_details
10
+ attrs = {
11
+ type: details.type.downcase
12
+ }
13
+
14
+ case details.type.downcase
15
+ when "card"
16
+ attrs[:brand] = details.card.type
17
+ attrs[:last4] = details.card.last4
18
+ attrs[:exp_month] = details.card.expiry_month
19
+ attrs[:exp_year] = details.card.expiry_year
20
+ end
21
+
22
+ payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.stored_payment_method_id)
23
+ payment_method.update!(attrs)
24
+ payment_method
25
+ end
26
+
27
+ def initialize(pay_payment_method)
28
+ @pay_payment_method = pay_payment_method
29
+ end
30
+
31
+ # Sets payment method as default
32
+ def make_default!
33
+ end
34
+
35
+ # Remove payment method
36
+ def detach
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,185 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class Subscription
4
+ attr_reader :pay_subscription
5
+
6
+ delegate :active?,
7
+ :canceled?,
8
+ :on_grace_period?,
9
+ :on_trial?,
10
+ :ends_at,
11
+ :name,
12
+ :owner,
13
+ :pause_starts_at,
14
+ :pause_starts_at?,
15
+ :processor_id,
16
+ :processor_plan,
17
+ :processor_subscription,
18
+ :prorate,
19
+ :prorate?,
20
+ :quantity,
21
+ :quantity?,
22
+ :trial_ends_at,
23
+ to: :pay_subscription
24
+
25
+ def self.sync_from_transaction(transaction_id)
26
+ transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
27
+ sync(transaction.subscription_id) if transaction.subscription_id
28
+ end
29
+
30
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
31
+ # Passthrough is not return from this API, so we can't use that
32
+ object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
33
+
34
+ pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
35
+ return unless pay_customer
36
+
37
+ attributes = {
38
+ current_period_end: object.current_billing_period&.ends_at,
39
+ current_period_start: object.current_billing_period&.starts_at,
40
+ ends_at: (object.canceled_at ? Time.parse(object.canceled_at) : nil),
41
+ metadata: object.custom_data,
42
+ paddle_cancel_url: object.management_urls&.cancel,
43
+ paddle_update_url: object.management_urls&.update_payment_method,
44
+ pause_starts_at: (object.paused_at ? Time.parse(object.paused_at) : nil),
45
+ status: object.status
46
+ }
47
+
48
+ if object.items&.first
49
+ item = object.items.first
50
+ attributes[:processor_plan] = item.price.id
51
+ attributes[:quantity] = item.quantity
52
+ end
53
+
54
+ case attributes[:status]
55
+ when "canceled"
56
+ # Remove payment methods since customer cannot be reused after cancelling
57
+ Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
58
+ when "trialing"
59
+ attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
60
+ when "paused"
61
+ attributes[:pause_starts_at] = Time.parse(object.paused_at)
62
+ end
63
+
64
+ case object.scheduled_change&.action
65
+ when "cancel"
66
+ attributes[:ends_at] = Time.parse(object.scheduled_change.effective_at)
67
+ when "pause"
68
+ attributes[:pause_starts_at] = Time.parse(object.scheduled_change.effective_at)
69
+ when "resume"
70
+ attributes[:pause_resumes_at] = Time.parse(object.scheduled_change.effective_at)
71
+ end
72
+
73
+ # Update or create the subscription
74
+ if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: subscription_id))
75
+ pay_subscription.with_lock do
76
+ pay_subscription.update!(attributes)
77
+ end
78
+ pay_subscription
79
+ else
80
+ pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: subscription_id))
81
+ end
82
+ end
83
+
84
+ def initialize(pay_subscription)
85
+ @pay_subscription = pay_subscription
86
+ end
87
+
88
+ def subscription(**options)
89
+ @paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
90
+ end
91
+
92
+ # Get a transaction to update payment method
93
+ def payment_method_transaction
94
+ ::Paddle::Subscription.get_transaction(id: processor_id)
95
+ end
96
+
97
+ # If a subscription is paused, cancel immediately
98
+ # Otherwise, cancel at period end
99
+ def cancel(**options)
100
+ return if canceled?
101
+
102
+ response = ::Paddle::Subscription.cancel(
103
+ id: processor_id,
104
+ effective_from: options.fetch(:effective_from, (paused? ? "immediately" : "next_billing_period"))
105
+ )
106
+ pay_subscription.update(
107
+ status: response.status,
108
+ ends_at: response.scheduled_change.effective_at
109
+ )
110
+ rescue ::Paddle::Error => e
111
+ raise Pay::PaddleBilling::Error, e
112
+ end
113
+
114
+ def cancel_now!(**options)
115
+ cancel(options.merge(effective_from: "immediately"))
116
+ rescue ::Paddle::Error => e
117
+ raise Pay::PaddleBilling::Error, e
118
+ end
119
+
120
+ def change_quantity(quantity, **options)
121
+ items = [{
122
+ price_id: processor_plan,
123
+ quantity: quantity
124
+ }]
125
+
126
+ ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
127
+ rescue ::Paddle::Error => e
128
+ raise Pay::PaddleBilling::Error, e
129
+ end
130
+
131
+ # A subscription could be set to cancel or pause in the future
132
+ # It is considered on grace period until the cancel or pause time begins
133
+ def on_grace_period?
134
+ (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
135
+ end
136
+
137
+ def paused?
138
+ pay_subscription.status == "paused"
139
+ end
140
+
141
+ def pause
142
+ response = ::Paddle::Subscription.pause(id: processor_id)
143
+ pay_subscription.update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
144
+ rescue ::Paddle::Error => e
145
+ raise Pay::PaddleBilling::Error, e
146
+ end
147
+
148
+ def resumable?
149
+ paused?
150
+ end
151
+
152
+ def resume
153
+ unless resumable?
154
+ raise StandardError, "You can only resume paused subscriptions."
155
+ end
156
+
157
+ # Paddle Billing API only allows "resuming" subscriptions when they are paused
158
+ # So cancel the scheduled change if it is in the future
159
+ if paused? && pause_starts_at? && Time.current < pause_starts_at
160
+ ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
161
+ else
162
+ ::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
163
+ end
164
+
165
+ pay_subscription.update(status: :active, pause_starts_at: nil)
166
+ rescue ::Paddle::Error => e
167
+ raise Pay::PaddleBilling::Error, e
168
+ end
169
+
170
+ def swap(plan, **options)
171
+ items = [{
172
+ price_id: plan,
173
+ quantity: quantity || 1
174
+ }]
175
+
176
+ ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
177
+ pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
178
+ end
179
+
180
+ # Retries the latest invoice for a Past Due subscription
181
+ def retry_failed_payment
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,11 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ module Webhooks
4
+ class Subscription
5
+ def call(event)
6
+ Pay::PaddleBilling::Subscription.sync(event.id, object: event)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ module Webhooks
4
+ class TransactionCompleted
5
+ def call(event)
6
+ Pay::PaddleBilling::Charge.sync(event.id)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,138 @@
1
+ module Pay
2
+ module LemonSqueezy
3
+ autoload :Billable, "pay/stripe/billable"
4
+ autoload :Charge, "pay/stripe/charge"
5
+ autoload :Error, "pay/stripe/error"
6
+ autoload :Merchant, "pay/stripe/merchant"
7
+ autoload :PaymentMethod, "pay/stripe/payment_method"
8
+ autoload :Subscription, "pay/stripe/subscription"
9
+
10
+ module Webhooks
11
+ autoload :AccountUpdated, "pay/stripe/webhooks/account_updated"
12
+ autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
13
+ autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
14
+ autoload :CheckoutSessionCompleted, "pay/stripe/webhooks/checkout_session_completed"
15
+ autoload :CheckoutSessionAsyncPaymentSucceeded, "pay/stripe/webhooks/checkout_session_async_payment_succeeded"
16
+ autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
17
+ autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
18
+ autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
19
+ autoload :PaymentFailed, "pay/stripe/webhooks/payment_failed"
20
+ autoload :PaymentIntentSucceeded, "pay/stripe/webhooks/payment_intent_succeeded"
21
+ autoload :PaymentMethodAttached, "pay/stripe/webhooks/payment_method_attached"
22
+ autoload :PaymentMethodDetached, "pay/stripe/webhooks/payment_method_detached"
23
+ autoload :PaymentMethodUpdated, "pay/stripe/webhooks/payment_method_updated"
24
+ autoload :SubscriptionCreated, "pay/stripe/webhooks/subscription_created"
25
+ autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
26
+ autoload :SubscriptionRenewing, "pay/stripe/webhooks/subscription_renewing"
27
+ autoload :SubscriptionUpdated, "pay/stripe/webhooks/subscription_updated"
28
+ autoload :SubscriptionTrialWillEnd, "pay/stripe/webhooks/subscription_trial_will_end"
29
+ end
30
+
31
+ extend Env
32
+
33
+ REQUIRED_VERSION = "~> 1"
34
+
35
+ def self.enabled?
36
+ return false unless Pay.enabled_processors.include?(:lemonsqueezy) && defined?(::Lemonzsqueezy)
37
+
38
+ Pay::Engine.version_matches?(required: REQUIRED_VERSION, current: ::Lemonsqueezy::VERSION) || (raise "[Pay] lemonsqueezy gem must be version #{REQUIRED_VERSION}")
39
+ end
40
+
41
+ def self.setup
42
+ ::Stripe.api_key = private_key
43
+
44
+ # Used by Stripe to identify Pay for support
45
+ ::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
46
+
47
+ # Automatically retry requests that fail
48
+ # This automatically includes idempotency keys in the request to guarantee that retires are safe
49
+ # https://github.com/stripe/stripe-ruby#configuring-automatic-retries
50
+ ::Stripe.max_network_retries = 2
51
+ end
52
+
53
+ def self.public_key
54
+ find_value_by_name(:stripe, :public_key)
55
+ end
56
+
57
+ def self.private_key
58
+ find_value_by_name(:stripe, :private_key)
59
+ end
60
+
61
+ def self.signing_secret
62
+ find_value_by_name(:stripe, :signing_secret)
63
+ end
64
+
65
+ def self.configure_webhooks
66
+ Pay::Webhooks.configure do |events|
67
+ # Listen to the charge event to make sure we get non-subscription
68
+ # purchases as well. Invoice is only for subscriptions and manual creation
69
+ # so it does not include individual charges.
70
+ events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
71
+ events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
72
+
73
+ events.subscribe "stripe.payment_intent.succeeded", Pay::Stripe::Webhooks::PaymentIntentSucceeded.new
74
+
75
+ # Warn user of upcoming charges for their subscription. This is handy for
76
+ # notifying annual users their subscription will renew shortly.
77
+ # This probably should be ignored for monthly subscriptions.
78
+ events.subscribe "stripe.invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
79
+
80
+ # Payment action is required to process an invoice
81
+ events.subscribe "stripe.invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
82
+
83
+ # If an invoice payment fails, we want to notify the user via email to update their payment details
84
+ events.subscribe "stripe.invoice.payment_failed", Pay::Stripe::Webhooks::PaymentFailed.new
85
+
86
+ # If a subscription is manually created on Stripe, we want to sync
87
+ events.subscribe "stripe.customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
88
+
89
+ # If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
90
+ events.subscribe "stripe.customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
91
+
92
+ # When a customers subscription is canceled, we want to update our records
93
+ events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
94
+
95
+ # When a customers subscription trial period is 3 days from ending or ended immediately this event is fired
96
+ events.subscribe "stripe.customer.subscription.trial_will_end", Pay::Stripe::Webhooks::SubscriptionTrialWillEnd.new
97
+
98
+ # Monitor changes for customer's default card changing and invoice credit updates
99
+ events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
100
+
101
+ # If a customer was deleted in Stripe, their subscriptions should be cancelled
102
+ events.subscribe "stripe.customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
103
+
104
+ # If a customer's payment source was deleted in Stripe, we should update as well
105
+ events.subscribe "stripe.payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodAttached.new
106
+ events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
107
+ events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
108
+ events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodDetached.new
109
+
110
+ # If an account is updated in stripe, we should update it as well
111
+ events.subscribe "stripe.account.updated", Pay::Stripe::Webhooks::AccountUpdated.new
112
+
113
+ # Handle subscriptions in Stripe Checkout Sessions
114
+ events.subscribe "stripe.checkout.session.completed", Pay::Stripe::Webhooks::CheckoutSessionCompleted.new
115
+ events.subscribe "stripe.checkout.session.async_payment_succeeded", Pay::Stripe::Webhooks::CheckoutSessionAsyncPaymentSucceeded.new
116
+ end
117
+ end
118
+
119
+ def self.to_client_reference_id(record)
120
+ raise ArgumentError, "#{record.class.name} does not include Pay. Allowed models: #{model_names.to_a.join(", ")}" unless model_names.include?(record.class.name)
121
+ [record.class.name, record.id].join("_")
122
+ end
123
+
124
+ def self.find_by_client_reference_id(client_reference_id)
125
+ # If there is a client reference ID, make sure we have a Pay::Customer record
126
+ # client_reference_id should be in the format of "User/1"
127
+ model_name, id = client_reference_id.split("_", 2)
128
+
129
+ # Only allow model names that use Pay
130
+ return unless model_names.include?(model_name)
131
+
132
+ model_name.constantize.find(id)
133
+ rescue ActiveRecord::RecordNotFound
134
+ Rails.logger.error "[Pay] Unable to locate record with: #{client_reference_id}"
135
+ nil
136
+ end
137
+ end
138
+ end
@@ -37,8 +37,8 @@ module Pay
37
37
  subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
38
38
  }
39
39
 
40
- if object.payment
41
- case object.payment.method_details.type.downcase
40
+ if (details = Array.wrap(object.payments).first&.method_details)
41
+ case details.type.downcase
42
42
  when "card"
43
43
  attrs[:payment_method_type] = "card"
44
44
  attrs[:brand] = details.card.type
@@ -5,6 +5,13 @@ module Pay
5
5
 
6
6
  delegate :customer, :processor_id, to: :pay_payment_method
7
7
 
8
+ def self.sync_from_transaction(pay_customer:, transaction:)
9
+ transaction = ::Paddle::Transaction.retrieve(id: transaction)
10
+ return unless transaction.status == "completed"
11
+ return if transaction.payments.empty?
12
+ sync(pay_customer: pay_customer, attributes: transaction.payments.first)
13
+ end
14
+
8
15
  def self.sync(pay_customer:, attributes:)
9
16
  details = attributes.method_details
10
17
  attrs = {
@@ -19,7 +26,7 @@ module Pay
19
26
  attrs[:exp_year] = details.card.expiry_year
20
27
  end
21
28
 
22
- payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.stored_payment_method_id)
29
+ payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
23
30
  payment_method.update!(attrs)
24
31
  payment_method
25
32
  end
@@ -56,9 +56,9 @@ module Pay
56
56
  # Remove payment methods since customer cannot be reused after cancelling
57
57
  Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
58
58
  when "trialing"
59
- attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
59
+ attributes[:trial_ends_at] = Time.parse(object.next_billed_at) if object.next_billed_at
60
60
  when "paused"
61
- attributes[:pause_starts_at] = Time.parse(object.paused_at)
61
+ attributes[:pause_starts_at] = Time.parse(object.paused_at) if object.paused_at
62
62
  when "active", "past_due"
63
63
  attributes[:trial_ends_at] = nil
64
64
  attributes[:pause_starts_at] = nil
@@ -109,7 +109,7 @@ module Pay
109
109
  )
110
110
  pay_subscription.update(
111
111
  status: response.status,
112
- ends_at: response.scheduled_change.effective_at
112
+ ends_at: response.scheduled_change&.effective_at || Time.current
113
113
  )
114
114
  rescue ::Paddle::Error => e
115
115
  raise Pay::PaddleBilling::Error, e
@@ -207,7 +207,7 @@ module Pay
207
207
  # cancel_now!(prorate: true)
208
208
  # cancel_now!(invoice_now: true)
209
209
  def cancel_now!(**options)
210
- return if canceled?
210
+ return if canceled? && ends_at.past?
211
211
 
212
212
  @stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
213
213
  pay_subscription.update(ends_at: Time.current, status: :canceled)
@@ -261,6 +261,8 @@ module Pay
261
261
  #
262
262
  # pause_behavior of `void` is considered active until the end of the current period and not active after that. The current_period_end is stored as `pause_starts_at`
263
263
  # Other pause_behaviors do not set `pause_starts_at` because they are used for offering free services
264
+ #
265
+ # https://docs.stripe.com/billing/subscriptions/pause-payment
264
266
  def pause(**options)
265
267
  attributes = {pause_collection: options.reverse_merge(behavior: "void")}
266
268
  @stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
@@ -273,8 +275,10 @@ module Pay
273
275
  end
274
276
 
275
277
  # Unpauses a subscription
278
+ #
279
+ # https://docs.stripe.com/billing/subscriptions/pause-payment#unpausing
276
280
  def unpause
277
- @stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: nil}.merge(expand_options), stripe_options)
281
+ @stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
278
282
  pay_subscription.update(
279
283
  pause_behavior: nil,
280
284
  pause_resumes_at: nil,
data/lib/pay/stripe.rb CHANGED
@@ -30,7 +30,7 @@ module Pay
30
30
 
31
31
  extend Env
32
32
 
33
- REQUIRED_VERSION = "~> 10"
33
+ REQUIRED_VERSION = "~> 11"
34
34
 
35
35
  # A list of database model names that include Pay
36
36
  # Used for safely looking up models with client_reference_id
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "7.1.1"
2
+ VERSION = "7.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pay
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.1
4
+ version: 7.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-01-12 00:00:00.000000000 Z
13
+ date: 2024-05-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rails
@@ -104,6 +104,14 @@ files:
104
104
  - lib/pay/fake_processor/merchant.rb
105
105
  - lib/pay/fake_processor/payment_method.rb
106
106
  - lib/pay/fake_processor/subscription.rb
107
+ - lib/pay/lemon_squeezy.rb
108
+ - lib/pay/lemon_squeezy/billable.rb
109
+ - lib/pay/lemon_squeezy/charge.rb
110
+ - lib/pay/lemon_squeezy/error.rb
111
+ - lib/pay/lemon_squeezy/payment_method.rb
112
+ - lib/pay/lemon_squeezy/subscription.rb
113
+ - lib/pay/lemon_squeezy/webhooks/subscription.rb
114
+ - lib/pay/lemon_squeezy/webhooks/transaction_completed.rb
107
115
  - lib/pay/nano_id.rb
108
116
  - lib/pay/paddle_billing.rb
109
117
  - lib/pay/paddle_billing/billable.rb
@@ -176,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
184
  - !ruby/object:Gem::Version
177
185
  version: '0'
178
186
  requirements: []
179
- rubygems_version: 3.5.3
187
+ rubygems_version: 3.4.22
180
188
  signing_key:
181
189
  specification_version: 4
182
190
  summary: Payments engine for Ruby on Rails