Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/commands/payments/stripe/handle_event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ def call
subscription_id: data_object['subscription'],
payment_intent_id: data_object['payment_intent']
)

# The invoice is paid by now, so record the subscription in case
# it wasn't recorded when the payment intent succeeded.
Payments::Stripe::PaymentIntent::HandleSuccess.(id: data_object['payment_intent'])
end
end
end
Expand Down
12 changes: 10 additions & 2 deletions app/commands/payments/stripe/payment_intent/cancel.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
class Payments::Stripe::PaymentIntent::Cancel
include Mandate

initialize_with :id
initialize_with :user, :id

def call
return unless belongs_to_user?

if invoice
void_invoice!
else
Expand All @@ -24,9 +26,15 @@ def delete_subscription!(subscription_id)
Rails.logger.error(e)
end

def belongs_to_user?
user.stripe_customer_id.present? && payment_intent.customer == user.stripe_customer_id
end

memoize
def payment_intent = Stripe::PaymentIntent.retrieve(id)

memoize
def invoice
payment_intent = Stripe::PaymentIntent.retrieve(id)
return nil unless payment_intent.invoice

Stripe::Invoice.retrieve(payment_intent.invoice)
Expand Down
21 changes: 18 additions & 3 deletions app/commands/payments/stripe/payment_intent/handle_success.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ def initialize(id: nil, payment_intent: nil)
end

def call
return unless payment_intent_succeeded?
return unless user
return unless should_record_payment?
return if subscription_data && !subscription_paid?

subscription = Payments::Stripe::Subscription::Create.(user, subscription_data) if subscription_data
Payments::Stripe::Payment::Create.(user, payment_intent, subscription:)
Expand All @@ -19,6 +21,15 @@ def call
private
attr_reader :id

def payment_intent_succeeded? = payment_intent.status == "succeeded"

# A subscription only counts once its first invoice has been paid.
def subscription_paid?
return true if invoice.status == "paid"

%w[active trialing].include?(subscription_data.status)
end

memoize
def user
raise "No customer in the payment intent" unless payment_intent.customer
Expand All @@ -27,11 +38,15 @@ def user
end

memoize
def subscription_data
def invoice
return unless payment_intent.invoice

invoice = Stripe::Invoice.retrieve(payment_intent.invoice)
return unless invoice.subscription
Stripe::Invoice.retrieve(payment_intent.invoice)
end

memoize
def subscription_data
return unless invoice&.subscription

Stripe::Subscription.retrieve(invoice.subscription)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def succeeded
end

def failed
::Payments::Stripe::PaymentIntent::Cancel.(params[:id])
::Payments::Stripe::PaymentIntent::Cancel.(current_user, params[:id])

render json: {}
end
Expand Down
42 changes: 42 additions & 0 deletions test/commands/payments/stripe/handle_event_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require_relative '../test_base'

class Payments::Stripe::HandleEventTest < Payments::TestBase
test "handles a succeeded payment intent" do
payment_intent = OpenStruct.new(id: SecureRandom.uuid)
event = OpenStruct.new(type: 'payment_intent.succeeded', data: OpenStruct.new(object: payment_intent))

Payments::Stripe::PaymentIntent::HandleSuccess.expects(:call).with(payment_intent:)

Payments::Stripe::HandleEvent.(event)
end

test "handles the first invoice of a subscription being paid" do
subscription_id = SecureRandom.uuid
payment_intent_id = SecureRandom.uuid
invoice = {
'billing_reason' => 'subscription_create',
'subscription' => subscription_id,
'payment_intent' => payment_intent_id
}
event = OpenStruct.new(type: 'invoice.payment_succeeded', data: OpenStruct.new(object: invoice))

Payments::Stripe::Subscription::HandleCreated.expects(:call).with(subscription_id:, payment_intent_id:)
Payments::Stripe::PaymentIntent::HandleSuccess.expects(:call).with(id: payment_intent_id)

Payments::Stripe::HandleEvent.(event)
end

test "ignores later subscription invoices" do
invoice = {
'billing_reason' => 'subscription_cycle',
'subscription' => SecureRandom.uuid,
'payment_intent' => SecureRandom.uuid
}
event = OpenStruct.new(type: 'invoice.payment_succeeded', data: OpenStruct.new(object: invoice))

Payments::Stripe::Subscription::HandleCreated.expects(:call).never
Payments::Stripe::PaymentIntent::HandleSuccess.expects(:call).never

Payments::Stripe::HandleEvent.(event)
end
end
47 changes: 41 additions & 6 deletions test/commands/payments/stripe/payment_intent/cancel_test.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
require_relative '../../test_base'

class Payments::Stripe::PaymentIntent::CancelTest < Payments::TestBase
setup do
@customer_id = SecureRandom.uuid
@user = create :user, stripe_customer_id: @customer_id
end

test "cancels for subscription" do
payment_intent_id = SecureRandom.uuid
subscription_id = SecureRandom.uuid
invoice_id = SecureRandom.uuid
invoice = mock_stripe_invoice(invoice_id, subscription_id)
payment_intent = mock_stripe_payment_intent(
payment_intent_id,
invoice_id:
invoice_id:,
customer: @customer_id
)

Stripe::PaymentIntent.expects(:retrieve).with(payment_intent_id).returns(payment_intent)
Stripe::Invoice.expects(:retrieve).with(invoice_id).returns(invoice)
Stripe::Subscription.expects(:cancel).with(subscription_id)
Stripe::Invoice.expects(:void_invoice).with(invoice_id)

Payments::Stripe::PaymentIntent::Cancel.(payment_intent_id)
Payments::Stripe::PaymentIntent::Cancel.(@user, payment_intent_id)
end

test "does not void unopen invoices" do
Expand All @@ -26,26 +32,55 @@ class Payments::Stripe::PaymentIntent::CancelTest < Payments::TestBase
invoice = mock_stripe_invoice(invoice_id, subscription_id, status: 'closed')
payment_intent = mock_stripe_payment_intent(
payment_intent_id,
invoice_id:
invoice_id:,
customer: @customer_id
)

Stripe::PaymentIntent.expects(:retrieve).with(payment_intent_id).returns(payment_intent)
Stripe::Invoice.expects(:retrieve).with(invoice_id).returns(invoice)
Stripe::Subscription.expects(:cancel).with(subscription_id)
Stripe::Invoice.expects(:void_invoice).never

Payments::Stripe::PaymentIntent::Cancel.(payment_intent_id)
Payments::Stripe::PaymentIntent::Cancel.(@user, payment_intent_id)
end

test "cancels payments" do
payment_intent_id = SecureRandom.uuid
payment_intent = mock_stripe_payment_intent(
payment_intent_id
payment_intent_id,
customer: @customer_id
)

Stripe::PaymentIntent.expects(:retrieve).with(payment_intent_id).returns(payment_intent)
Stripe::PaymentIntent.expects(:cancel).with(payment_intent_id)

Payments::Stripe::PaymentIntent::Cancel.(payment_intent_id)
Payments::Stripe::PaymentIntent::Cancel.(@user, payment_intent_id)
end

test "does nothing for another customer's payment intent" do
payment_intent_id = SecureRandom.uuid
payment_intent = mock_stripe_payment_intent(
payment_intent_id,
invoice_id: SecureRandom.uuid,
customer: SecureRandom.uuid
)

Stripe::PaymentIntent.expects(:retrieve).with(payment_intent_id).returns(payment_intent)
Stripe::Invoice.expects(:retrieve).never
Stripe::Subscription.expects(:cancel).never
Stripe::Invoice.expects(:void_invoice).never
Stripe::PaymentIntent.expects(:cancel).never

Payments::Stripe::PaymentIntent::Cancel.(@user, payment_intent_id)
end

test "does nothing if the user has no stripe customer" do
user = create :user
payment_intent_id = SecureRandom.uuid

Stripe::PaymentIntent.expects(:retrieve).never
Stripe::PaymentIntent.expects(:cancel).never

Payments::Stripe::PaymentIntent::Cancel.(user, payment_intent_id)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
require_relative '../../test_base'

class Payments::Stripe::PaymentIntent::HandleSuccessTest < Payments::TestBase
setup do
@customer_id = SecureRandom.uuid
@user = create :user, stripe_customer_id: @customer_id
end

test "records a succeeded payment" do
payment_intent = mock_stripe_payment_intent(SecureRandom.uuid, customer: @customer_id, status: "succeeded")
Stripe::Checkout::Session.expects(:list).with({ payment_intent: payment_intent.id }).returns(OpenStruct.new(data: []))

Payments::Stripe::Subscription::Create.expects(:call).never
Payments::Stripe::Payment::Create.expects(:call).with(@user, payment_intent, subscription: nil)

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end

test "retrieves the payment intent by id" do
payment_intent = mock_stripe_payment_intent(SecureRandom.uuid, customer: @customer_id, status: "succeeded")
Stripe::PaymentIntent.expects(:retrieve).with(payment_intent.id).returns(payment_intent)
Stripe::Checkout::Session.expects(:list).returns(OpenStruct.new(data: []))

Payments::Stripe::Payment::Create.expects(:call).with(@user, payment_intent, subscription: nil)

Payments::Stripe::PaymentIntent::HandleSuccess.(id: payment_intent.id)
end

%w[requires_payment_method requires_confirmation requires_action processing canceled].each do |status|
test "does not record a payment with status #{status}" do
payment_intent = mock_stripe_payment_intent(SecureRandom.uuid, customer: @customer_id, status:)

Payments::Stripe::Subscription::Create.expects(:call).never
Payments::Stripe::Payment::Create.expects(:call).never

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end
end

test "records a subscription and payment once the invoice is paid" do
invoice_id = SecureRandom.uuid
subscription_id = SecureRandom.uuid
payment_intent = mock_stripe_payment_intent(
SecureRandom.uuid, invoice_id:, customer: @customer_id, status: "succeeded"
)
invoice = mock_stripe_invoice(invoice_id, subscription_id, status: "paid")
subscription_data = mock_stripe_subscription(subscription_id, 1500, status: "incomplete")
Stripe::Invoice.expects(:retrieve).with(invoice_id).returns(invoice)
Stripe::Subscription.expects(:retrieve).with(subscription_id).returns(subscription_data)

subscription = create(:payments_subscription, user: @user)
Payments::Stripe::Subscription::Create.expects(:call).with(@user, subscription_data).returns(subscription)
Payments::Stripe::Payment::Create.expects(:call).with(@user, payment_intent, subscription:)

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end

test "records a subscription and payment once the subscription is active" do
invoice_id = SecureRandom.uuid
subscription_id = SecureRandom.uuid
payment_intent = mock_stripe_payment_intent(
SecureRandom.uuid, invoice_id:, customer: @customer_id, status: "succeeded"
)
invoice = mock_stripe_invoice(invoice_id, subscription_id, status: "open")
subscription_data = mock_stripe_subscription(subscription_id, 1500, status: "active")
Stripe::Invoice.expects(:retrieve).with(invoice_id).returns(invoice)
Stripe::Subscription.expects(:retrieve).with(subscription_id).returns(subscription_data)

Payments::Stripe::Subscription::Create.expects(:call).with(@user, subscription_data)
Payments::Stripe::Payment::Create.expects(:call)

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end

test "does not record an unpaid subscription" do
invoice_id = SecureRandom.uuid
subscription_id = SecureRandom.uuid
payment_intent = mock_stripe_payment_intent(
SecureRandom.uuid, invoice_id:, customer: @customer_id, status: "succeeded"
)
invoice = mock_stripe_invoice(invoice_id, subscription_id, status: "open")
subscription_data = mock_stripe_subscription(subscription_id, 1500, status: "incomplete")
Stripe::Invoice.expects(:retrieve).with(invoice_id).returns(invoice)
Stripe::Subscription.expects(:retrieve).with(subscription_id).returns(subscription_data)

Payments::Stripe::Subscription::Create.expects(:call).never
Payments::Stripe::Payment::Create.expects(:call).never

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end

test "does nothing for an unknown customer" do
payment_intent = mock_stripe_payment_intent(SecureRandom.uuid, customer: SecureRandom.uuid, status: "succeeded")

Payments::Stripe::Payment::Create.expects(:call).never

Payments::Stripe::PaymentIntent::HandleSuccess.(payment_intent:)
end
end
4 changes: 3 additions & 1 deletion test/commands/payments/test_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ def mock_stripe_invoice(id, subscription_id, status: 'open', customer: nil)
)
end

def mock_stripe_payment_intent(id, invoice_id: nil, payment_method: nil)
def mock_stripe_payment_intent(id, invoice_id: nil, payment_method: nil, customer: nil, status: nil)
data = RecursiveOpenStruct.new(id:)
data.invoice = invoice_id if invoice_id
data.payment_method = payment_method if payment_method
data.customer = customer if customer
data.status = status if status
data
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class API::Payments::PaymentIntentsControllerTest < API::BaseTestCase
user = create :user

id = SecureRandom.uuid
::Payments::Stripe::PaymentIntent::Cancel.expects(:call).with(id)
::Payments::Stripe::PaymentIntent::Cancel.expects(:call).with(user, id)

setup_user(user)
patch failed_api_payments_payment_intent_path(id), headers: @headers, as: :json
Expand Down
Loading