diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb index 37d8ef5cd..c2e5f0160 100644 --- a/app/controllers/user_upgrades_controller.rb +++ b/app/controllers/user_upgrades_controller.rb @@ -27,6 +27,14 @@ class UserUpgradesController < ApplicationController respond_with(@user_upgrade) end + def refund + @user_upgrade = authorize UserUpgrade.find(params[:id]) + @user_upgrade.refund! + flash[:notice] = "Upgrade refunded" + + respond_with(@user_upgrade) + end + def receipt @user_upgrade = authorize UserUpgrade.find(params[:id]) redirect_to @user_upgrade.receipt_url diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb index a69c95dce..9ed86e4ed 100644 --- a/app/models/user_upgrade.rb +++ b/app/models/user_upgrade.rb @@ -11,7 +11,8 @@ class UserUpgrade < ApplicationRecord enum status: { pending: 0, processing: 10, - complete: 20 + complete: 20, + refunded: 30, } scope :gifted, -> { where("recipient_id != purchaser_id") } @@ -62,6 +63,19 @@ class UserUpgrade < ApplicationRecord end end + def previous_level + case upgrade_type + when "gold" + User::Levels::MEMBER + when "platinum" + User::Levels::MEMBER + when "gold_to_platinum" + User::Levels::GOLD + else + raise NotImplementedError + end + end + def upgrade_price case upgrade_type when "gold" @@ -120,7 +134,7 @@ class UserUpgrade < ApplicationRecord concerning :UpgradeMethods do def process_upgrade!(payment_status) recipient.with_lock do - return if status == "complete" + return unless pending? || processing? if payment_status == "paid" upgrade_recipient! @@ -198,24 +212,38 @@ class UserUpgrade < ApplicationRecord checkout end + def refund!(reason: nil) + with_lock do + return if refunded? + + Stripe::Refund.create(payment_intent: payment_intent.id, reason: reason) + recipient.update!(level: previous_level) + update!(status: "refunded") + end + end + def receipt_url return nil if pending? || stripe_id.nil? - - checkout_session = Stripe::Checkout::Session.retrieve(stripe_id) - payment_intent = Stripe::PaymentIntent.retrieve(checkout_session.payment_intent) - charge = payment_intent.charges.data.first charge.receipt_url end def payment_url return nil if pending? || stripe_id.nil? - - checkout_session = Stripe::Checkout::Session.retrieve(stripe_id) - payment_intent = Stripe::PaymentIntent.retrieve(checkout_session.payment_intent) - "https://dashboard.stripe.com/payments/#{payment_intent.id}" end + def checkout_session + @checkout_session ||= Stripe::Checkout::Session.retrieve(stripe_id) + end + + def payment_intent + @payment_intent ||= Stripe::PaymentIntent.retrieve(checkout_session.payment_intent) + end + + def charge + payment_intent.charges.data.first + end + def has_receipt? !pending? end diff --git a/app/policies/user_upgrade_policy.rb b/app/policies/user_upgrade_policy.rb index 1da56ef5c..d5022a350 100644 --- a/app/policies/user_upgrade_policy.rb +++ b/app/policies/user_upgrade_policy.rb @@ -11,6 +11,10 @@ class UserUpgradePolicy < ApplicationPolicy record.recipient == user || record.purchaser == user || user.is_owner? end + def refund? + user.is_owner? && record.complete? + end + def receipt? (record.purchaser == user || user.is_owner?) && record.has_receipt? end diff --git a/app/views/user_upgrades/index.html.erb b/app/views/user_upgrades/index.html.erb index ea57f6ba2..2a0448406 100644 --- a/app/views/user_upgrades/index.html.erb +++ b/app/views/user_upgrades/index.html.erb @@ -40,6 +40,9 @@ <% if policy(user_upgrade).payment? %> | <%= link_to "Payment", payment_user_upgrade_path(user_upgrade), target: "_blank" %> <% end %> + <% if policy(user_upgrade).refund? %> + | <%= link_to "Refund", refund_user_upgrade_path(user_upgrade), remote: true, method: :put, "data-confirm": "Are you sure you want to refund this payment?" %> + <% end %> <% end %> <% end %> diff --git a/app/views/user_upgrades/refund.js.erb b/app/views/user_upgrades/refund.js.erb new file mode 100644 index 000000000..345366b9b --- /dev/null +++ b/app/views/user_upgrades/refund.js.erb @@ -0,0 +1 @@ +location.reload(); diff --git a/config/routes.rb b/config/routes.rb index ede8fa5bf..13e8a809c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -257,6 +257,7 @@ Rails.application.routes.draw do resources :user_upgrades, only: [:new, :create, :show, :index] do get :receipt, on: :member get :payment, on: :member + put :refund, on: :member end resources :user_feedbacks, except: [:destroy] resources :user_name_change_requests, only: [:new, :create, :show, :index] diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb index c1a02213a..8075eb7e0 100644 --- a/test/functional/user_upgrades_controller_test.rb +++ b/test/functional/user_upgrades_controller_test.rb @@ -183,6 +183,51 @@ class UserUpgradesControllerTest < ActionDispatch::IntegrationTest end end + context "refund action" do + mock_stripe! + + context "for a self upgrade" do + context "to Gold" do + should_eventually "refund the upgrade" do + @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete") + @user_upgrade.create_checkout! + + put_auth refund_user_upgrade_path(@user_upgrade), create(:owner_user), xhr: true + + assert_response :success + assert_equal("refunded", @user_upgrade.reload.status) + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level) + end + end + end + + context "for a gifted upgrade" do + context "to Platinum" do + should_eventually "refund the upgrade" do + @user_upgrade = create(:gift_platinum_upgrade, recipient: create(:platinum_user), status: "complete") + @user_upgrade.create_checkout! + + put_auth refund_user_upgrade_path(@user_upgrade), create(:owner_user), xhr: true + + assert_response :success + assert_equal("refunded", @user_upgrade.reload.status) + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level) + end + end + end + + should "not allow unauthorized users to create a refund" do + @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete") + @user_upgrade.create_checkout! + + put_auth refund_user_upgrade_path(@user_upgrade), @user_upgrade.purchaser, xhr: true + + assert_response 403 + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::GOLD, @user_upgrade.recipient.level) + end + end + context "create action" do mock_stripe! diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb index f90cbd4da..38102d2bf 100644 --- a/test/unit/user_upgrade_test.rb +++ b/test/unit/user_upgrade_test.rb @@ -82,5 +82,16 @@ class UserUpgradeTest < ActiveSupport::TestCase end end end + + context "the #refund! method" do + should_eventually "refund a Gold upgrade" do + @user_upgrade = create(:self_gold_upgrade, recipient: create(:gold_user), status: "complete") + @user_upgrade.create_checkout! + @user_upgrade.refund! + + assert_equal("refunded", @user_upgrade.reload.status) + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level) + end + end end end