From 74ed2a8b966073cbc67677059a1cde1640929535 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 24 Dec 2020 06:40:20 -0600 Subject: [PATCH] user upgrades: add UserUpgrade model. Add a model to store the status of user upgrades. * Store the upgrade purchaser and the upgrade receiver (these are different for a gifted upgrade, the same for a self upgrade). * Store the upgrade type: gold, platinum, or gold-to-platinum upgrades. * Store the upgrade status: ** pending: User is still on the Stripe checkout page, no payment received yet. ** processing: User has completed checkout, but the checkout status in Stripe is still 'unpaid'. ** complete: We've received notification from Stripe that the payment has gone through and the user has been upgraded. * Store the Stripe checkout ID, to cross-reference the upgrade record on Danbooru with the checkout record on Stripe. This is the upgrade flow: * When the user clicks the upgrade button on the upgrade page, we call POST /user_upgrades and create a pending UserUpgrade. * We redirect the user to the checkout page on Stripe. * When the user completes checkout on Stripe, Stripe sends us a webhook notification at POST /webhooks/receive. * When we receive the webhook, we check the payment status, and if it's paid we mark the UserUpgrade as complete and upgrade the user. * After Stripe sees that we have successfully processed the webhook, they redirect the user to the /user_upgrades/:id page, where we show the user their upgrade receipt. --- app/controllers/user_upgrades_controller.rb | 7 +- app/models/user.rb | 2 + app/{logical => models}/user_upgrade.rb | 93 +++++---- app/policies/user_upgrade_policy.rb | 9 + app/views/user_upgrades/new.html.erb | 10 +- app/views/user_upgrades/show.html.erb | 47 +++-- config/routes.rb | 2 +- .../20201224101208_create_user_upgrades.rb | 20 ++ db/structure.sql | 88 ++++++++- test/factories/user_upgrade.rb | 38 ++++ .../user_upgrades_controller_test.rb | 176 ++++++++++++++++-- test/functional/webhooks_controller_test.rb | 108 +++++------ test/unit/user_upgrade_test.rb | 66 ++++--- 13 files changed, 502 insertions(+), 164 deletions(-) rename app/{logical => models}/user_upgrade.rb (61%) create mode 100644 app/policies/user_upgrade_policy.rb create mode 100644 db/migrate/20201224101208_create_user_upgrades.rb create mode 100644 test/factories/user_upgrade.rb diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb index e2b8729d6..4a0b199c2 100644 --- a/app/controllers/user_upgrades_controller.rb +++ b/app/controllers/user_upgrades_controller.rb @@ -3,8 +3,8 @@ class UserUpgradesController < ApplicationController respond_to :js, :html def create - @user_upgrade = UserUpgrade.new(recipient: user, purchaser: CurrentUser.user, level: params[:level].to_i) - @checkout = @user_upgrade.create_checkout + @user_upgrade = authorize UserUpgrade.create(recipient: user, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type]) + @checkout = @user_upgrade.create_checkout! respond_with(@user_upgrade) end @@ -13,7 +13,8 @@ class UserUpgradesController < ApplicationController end def show - authorize User, :upgrade? + @user_upgrade = authorize UserUpgrade.find(params[:id]) + respond_with(@user_upgrade) end def user diff --git a/app/models/user.rb b/app/models/user.rb index 071c42d04..01730294c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,6 +99,8 @@ class User < ApplicationRecord has_many :post_votes has_many :post_versions, foreign_key: :updater_id has_many :bans, -> {order("bans.id desc")} + has_many :received_upgrades, class_name: "UserUpgrade", foreign_key: :recipient_id, dependent: :destroy + has_many :purchased_upgrades, class_name: "UserUpgrade", foreign_key: :purchaser_id, dependent: :destroy has_one :recent_ban, -> {order("bans.id desc")}, :class_name => "Ban" has_one :api_key diff --git a/app/logical/user_upgrade.rb b/app/models/user_upgrade.rb similarity index 61% rename from app/logical/user_upgrade.rb rename to app/models/user_upgrade.rb index f9af14708..3fdc81045 100644 --- a/app/logical/user_upgrade.rb +++ b/app/models/user_upgrade.rb @@ -1,5 +1,18 @@ -class UserUpgrade - attr_reader :recipient, :purchaser, :level +class UserUpgrade < ApplicationRecord + belongs_to :recipient, class_name: "User" + belongs_to :purchaser, class_name: "User" + + enum upgrade_type: { + gold: 0, + platinum: 10, + gold_to_platinum: 20 + }, _suffix: "upgrade" + + enum status: { + pending: 0, + processing: 10, + complete: 20 + } def self.stripe_publishable_key Danbooru.config.stripe_publishable_key @@ -21,51 +34,63 @@ class UserUpgrade platinum_price - gold_price end - def initialize(recipient:, purchaser:, level:) - @recipient, @purchaser, @level = recipient, purchaser, level.to_i - end - - def upgrade_type - if level == User::Levels::GOLD && recipient.level == User::Levels::MEMBER - :gold_upgrade - elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::MEMBER - :platinum_upgrade - elsif level == User::Levels::PLATINUM && recipient.level == User::Levels::GOLD - :gold_to_platinum_upgrade + def level + case upgrade_type + when "gold" + User::Levels::GOLD + when "platinum" + User::Levels::PLATINUM + when "gold_to_platinum" + User::Levels::PLATINUM else - raise ArgumentError, "Invalid upgrade" + raise NotImplementedError end end def upgrade_price case upgrade_type - when :gold_upgrade + when "gold" UserUpgrade.gold_price - when :platinum_upgrade + when "platinum" UserUpgrade.platinum_price - when :gold_to_platinum_upgrade + when "gold_to_platinum" UserUpgrade.gold_to_platinum_price + else + raise NotImplementedError end end def upgrade_description case upgrade_type - when :gold_upgrade + when "gold" "Upgrade to Gold" - when :platinum_upgrade + when "platinum" "Upgrade to Platinum" - when :gold_to_platinum_upgrade + when "gold_to_platinum" "Upgrade Gold to Platinum" + else + raise NotImplementedError end end + def level_string + User.level_string(level) + end + def is_gift? recipient != purchaser end - def process_upgrade! + def process_upgrade!(payment_status) recipient.with_lock do - upgrade_recipient! + return if status == "complete" + + if payment_status == "paid" + upgrade_recipient! + update!(status: :complete) + else + update!(status: :processing) + end end end @@ -74,12 +99,12 @@ class UserUpgrade end concerning :StripeMethods do - def create_checkout - Stripe::Checkout::Session.create( + def create_checkout! + checkout = Stripe::Checkout::Session.create( mode: "payment", - success_url: Routes.user_upgrade_url(user_id: recipient.id), + success_url: Routes.user_upgrade_url(self), cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id), - client_reference_id: "user_#{purchaser.id}", + client_reference_id: "user_upgrade_#{id}", customer_email: recipient.email_address&.address, payment_method_types: ["card"], line_items: [{ @@ -93,6 +118,7 @@ class UserUpgrade quantity: 1, }], metadata: { + user_upgrade_id: id, purchaser_id: purchaser.id, recipient_id: recipient.id, purchaser_name: purchaser.name, @@ -102,6 +128,9 @@ class UserUpgrade level: level, }, ) + + update!(stripe_id: checkout.id) + checkout end class_methods do @@ -122,7 +151,7 @@ class UserUpgrade event = build_event(request) if event.type == "checkout.session.completed" - checkout_session_completed(event) + checkout_session_completed(event.data.object) end end @@ -132,13 +161,9 @@ class UserUpgrade Stripe::Webhook.construct_event(payload, signature, stripe_webhook_secret) end - def checkout_session_completed(event) - recipient = User.find(event.data.object.metadata.recipient_id) - purchaser = User.find(event.data.object.metadata.purchaser_id) - level = event.data.object.metadata.level - - user_upgrade = UserUpgrade.new(recipient: recipient, purchaser: purchaser, level: level) - user_upgrade.process_upgrade! + def checkout_session_completed(checkout) + user_upgrade = UserUpgrade.find(checkout.metadata.user_upgrade_id) + user_upgrade.process_upgrade!(checkout.payment_status) end end end diff --git a/app/policies/user_upgrade_policy.rb b/app/policies/user_upgrade_policy.rb new file mode 100644 index 000000000..78365557f --- /dev/null +++ b/app/policies/user_upgrade_policy.rb @@ -0,0 +1,9 @@ +class UserUpgradePolicy < ApplicationPolicy + def create? + user.is_member? + end + + def show? + record.recipient == user || record.purchaser == user || user.is_owner? + end +end diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb index 2ed284092..89d7ea4d6 100644 --- a/app/views/user_upgrades/new.html.erb +++ b/app/views/user_upgrades/new.html.erb @@ -101,11 +101,11 @@

You can pay with a credit or debit card. Safebooru uses Stripe as a payment intermediary so none of your personal information will be stored on the site.

- <% if user.level < User::Levels::GOLD %> -

<%= button_to "Upgrade to Gold", user_upgrade_path(user_id: user.id, level: User::Levels::GOLD), remote: true, disable_with: "Redirecting..." %>

-

<%= button_to "Upgrade to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>

- <% elsif user.level < User::Levels::PLATINUM %> -

<%= button_to "Upgrade Gold to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %>

+ <% if user.level == User::Levels::MEMBER %> +

<%= button_to "Upgrade to Gold", user_upgrades_path(user_id: user.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %>

+

<%= button_to "Upgrade to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %>

+ <% elsif user.level == User::Levels::GOLD %> +

<%= button_to "Upgrade Gold to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %>

<% end %> <% else %> diff --git a/app/views/user_upgrades/show.html.erb b/app/views/user_upgrades/show.html.erb index 014421551..7eeddffdf 100644 --- a/app/views/user_upgrades/show.html.erb +++ b/app/views/user_upgrades/show.html.erb @@ -1,22 +1,47 @@ -<% page_title "Account Upgraded" %> +<% page_title "User Upgrade Status" %> <%= render "users/secondary_links" %>
- <% if flash[:success] %> -

Congratulations!

+

User Upgrade

- <% if user != CurrentUser.user %> -

<%= user.name %> is now a <%= user.level_string %> user. Thanks for supporting the site!

+

+

    +
  • + Purchased + <%= time_ago_in_words_tagged @user_upgrade.updated_at %> + by <%= link_to_user @user_upgrade.purchaser %> + <% if @user_upgrade.is_gift? %> + for <%= link_to_user @user_upgrade.recipient %> + <% end %> +
  • +
  • + Upgrade Type + <%= @user_upgrade.upgrade_type.humanize %> +
  • +
  • + Status + <%= @user_upgrade.status.humanize %> +
  • +
+

+ + <% if @user_upgrade.status == "complete" %> + <% if @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.recipient %> +

<%= link_to_user @user_upgrade.purchaser %> has upgraded your account to <%= @user_upgrade.level_string %>. Enjoy your new account!

+ <% elsif @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.purchaser %> +

<%= link_to_user @user_upgrade.recipient %> is now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!

<% else %> -

You are now a <%= user.level_string %> user. Thanks for supporting the site!

+

You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!

<% end %> -

<%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to start using your new account.

- <% elsif flash[:error] %> -

An error occurred!

-

<%= flash[:error] %>

-

<%= link_to "Try again", new_user_upgrade_path %>

+

<%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to continue using the site.

+ <% else %> + <%= content_for :html_header do %> + + <% end %> + +

This order is still being processed. You will be notified as soon as the order is complete.

<% end %>
diff --git a/config/routes.rb b/config/routes.rb index b027bb3a3..4bceb91fe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -254,7 +254,7 @@ Rails.application.routes.draw do get :custom_style end end - resource :user_upgrade, :only => [:new, :create, :show] + resources :user_upgrades, only: [:new, :create, :show] resources :user_feedbacks, except: [:destroy] resources :user_name_change_requests, only: [:new, :create, :show, :index] resources :webhooks do diff --git a/db/migrate/20201224101208_create_user_upgrades.rb b/db/migrate/20201224101208_create_user_upgrades.rb new file mode 100644 index 000000000..46b44f22a --- /dev/null +++ b/db/migrate/20201224101208_create_user_upgrades.rb @@ -0,0 +1,20 @@ +class CreateUserUpgrades < ActiveRecord::Migration[6.1] + def change + create_table :user_upgrades do |t| + t.timestamps + + t.references :recipient, index: true, null: false + t.references :purchaser, index: true, null: false + t.integer :upgrade_type, index: true, null: false + t.integer :status, index: true, null: false + t.string :stripe_id, index: true, null: true + end + + # Reserve ID space for backfilling old upgrades. + reversible do |dir| + dir.up do + execute "SELECT setval('user_upgrades_id_seq', 25000, false)" + end + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 5f8a9b7da..f57281afa 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -3104,6 +3104,41 @@ CREATE SEQUENCE public.user_name_change_requests_id_seq ALTER SEQUENCE public.user_name_change_requests_id_seq OWNED BY public.user_name_change_requests.id; +-- +-- Name: user_upgrades; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_upgrades ( + id bigint NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + recipient_id bigint NOT NULL, + purchaser_id bigint NOT NULL, + upgrade_type integer NOT NULL, + status integer NOT NULL, + stripe_id character varying +); + + +-- +-- Name: user_upgrades_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_upgrades_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: user_upgrades_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_upgrades_id_seq OWNED BY public.user_upgrades.id; + + -- -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- @@ -4172,6 +4207,13 @@ ALTER TABLE ONLY public.user_feedback ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.user_name_change_requests ALTER COLUMN id SET DEFAULT nextval('public.user_name_change_requests_id_seq'::regclass); +-- +-- Name: user_upgrades id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_upgrades ALTER COLUMN id SET DEFAULT nextval('public.user_upgrades_id_seq'::regclass); + + -- -- Name: users id; Type: DEFAULT; Schema: public; Owner: - -- @@ -4537,6 +4579,14 @@ ALTER TABLE ONLY public.user_name_change_requests ADD CONSTRAINT user_name_change_requests_pkey PRIMARY KEY (id); +-- +-- Name: user_upgrades user_upgrades_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_upgrades + ADD CONSTRAINT user_upgrades_pkey PRIMARY KEY (id); + + -- -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -7045,6 +7095,41 @@ CREATE INDEX index_user_name_change_requests_on_original_name ON public.user_nam CREATE INDEX index_user_name_change_requests_on_user_id ON public.user_name_change_requests USING btree (user_id); +-- +-- Name: index_user_upgrades_on_purchaser_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_upgrades_on_purchaser_id ON public.user_upgrades USING btree (purchaser_id); + + +-- +-- Name: index_user_upgrades_on_recipient_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_upgrades_on_recipient_id ON public.user_upgrades USING btree (recipient_id); + + +-- +-- Name: index_user_upgrades_on_status; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_upgrades_on_status ON public.user_upgrades USING btree (status); + + +-- +-- Name: index_user_upgrades_on_stripe_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_upgrades_on_stripe_id ON public.user_upgrades USING btree (stripe_id); + + +-- +-- Name: index_user_upgrades_on_upgrade_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_user_upgrades_on_upgrade_type ON public.user_upgrades USING btree (upgrade_type); + + -- -- Name: index_users_on_created_at; Type: INDEX; Schema: public; Owner: - -- @@ -7437,6 +7522,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200816175151'), ('20201201211748'), ('20201213052805'), -('20201219201007'); +('20201219201007'), +('20201224101208'); diff --git a/test/factories/user_upgrade.rb b/test/factories/user_upgrade.rb new file mode 100644 index 000000000..a3b13c4fd --- /dev/null +++ b/test/factories/user_upgrade.rb @@ -0,0 +1,38 @@ +FactoryBot.define do + factory(:user_upgrade) do + recipient { create(:member_user) } + purchaser { recipient } + upgrade_type { "gold" } + status { "pending" } + stripe_id { nil } + + factory(:self_gold_upgrade) do + upgrade_type { "gold" } + end + + factory(:self_platinum_upgrade) do + upgrade_type { "platinum" } + end + + factory(:self_gold_to_platinum_upgrade) do + recipient { create(:gold_user) } + upgrade_type { "gold_to_platinum" } + end + + factory(:gift_gold_upgrade) do + purchaser { create(:user) } + upgrade_type { "gold" } + end + + factory(:gift_platinum_upgrade) do + purchaser { create(:user) } + upgrade_type { "platinum" } + end + + factory(:gift_gold_to_platinum_upgrade) do + recipient { create(:gold_user) } + purchaser { create(:user) } + upgrade_type { "gold_to_platinum" } + end + end +end diff --git a/test/functional/user_upgrades_controller_test.rb b/test/functional/user_upgrades_controller_test.rb index 58e92a8aa..ba6240730 100644 --- a/test/functional/user_upgrades_controller_test.rb +++ b/test/functional/user_upgrades_controller_test.rb @@ -3,38 +3,184 @@ require 'test_helper' class UserUpgradesControllerTest < ActionDispatch::IntegrationTest context "The user upgrades controller" do context "new action" do - should "render" do + should "render for a self upgrade" do + @user = create(:user) + get_auth new_user_upgrade_path, @user + + assert_response :success + end + + should "render for a gift upgrade" do + @recipient = create(:user) + get_auth new_user_upgrade_path(user_id: @recipient.id), create(:user) + + assert_response :success + end + + should "render for an anonymous user" do get new_user_upgrade_path + assert_response :success end end context "show action" do - should "render" do - get_auth user_upgrade_path, create(:user) - assert_response :success + context "for a completed upgrade" do + should "render for a self upgrade" do + @user_upgrade = create(:self_gold_upgrade, status: "complete") + get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser + + assert_response :success + end + + should "render for a gift upgrade for the purchaser" do + @user_upgrade = create(:gift_gold_upgrade, status: "complete") + get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser + + assert_response :success + end + + should "render for a gift upgrade for the recipient" do + @user_upgrade = create(:gift_gold_upgrade, status: "complete") + get_auth user_upgrade_path(@user_upgrade), @user_upgrade.recipient + + assert_response :success + end + + should "render for the site owner" do + @user_upgrade = create(:self_gold_upgrade, status: "complete") + get_auth user_upgrade_path(@user_upgrade), create(:owner_user) + + assert_response :success + end + + should "be inaccessible to other users" do + @user_upgrade = create(:self_gold_upgrade, status: "complete") + get_auth user_upgrade_path(@user_upgrade), create(:user) + + assert_response 403 + end + end + + context "for a pending upgrade" do + should "render" do + @user_upgrade = create(:self_gold_upgrade, status: "pending") + get_auth user_upgrade_path(@user_upgrade), @user_upgrade.purchaser + + assert_response :success + end end end context "create action" do mock_stripe! - context "for a self upgrade to Gold" do - should "redirect the user to the Stripe checkout page" do - user = create(:member_user) - post_auth user_upgrade_path(user_id: user.id), user, params: { level: User::Levels::GOLD }, xhr: true + context "for a self upgrade" do + context "to Gold" do + should "create a pending upgrade" do + @user = create(:member_user) - assert_response :success + post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "gold" }, xhr: true + assert_response :success + + @user_upgrade = @user.purchased_upgrades.last + assert_equal(@user, @user_upgrade.purchaser) + assert_equal(@user, @user_upgrade.recipient) + assert_equal("gold", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end + end + + context "to Platinum" do + should "create a pending upgrade" do + @user = create(:member_user) + + post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "platinum" }, xhr: true + assert_response :success + + @user_upgrade = @user.purchased_upgrades.last + assert_equal(@user, @user_upgrade.purchaser) + assert_equal(@user, @user_upgrade.recipient) + assert_equal("platinum", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end + end + + context "from Gold to Platinum" do + should "create a pending upgrade" do + @user = create(:member_user) + + post_auth user_upgrades_path(user_id: @user.id), @user, params: { upgrade_type: "gold_to_platinum" }, xhr: true + assert_response :success + + @user_upgrade = @user.purchased_upgrades.last + assert_equal(@user, @user_upgrade.purchaser) + assert_equal(@user, @user_upgrade.recipient) + assert_equal("gold_to_platinum", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end end end - context "for a gifted upgrade to Gold" do - should "redirect the user to the Stripe checkout page" do - recipient = create(:member_user) - purchaser = create(:member_user) - post_auth user_upgrade_path(user_id: recipient.id), purchaser, params: { level: User::Levels::GOLD }, xhr: true + context "for a gifted upgrade" do + context "to Gold" do + should "create a pending upgrade" do + @recipient = create(:member_user) + @purchaser = create(:member_user) - assert_response :success + post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "gold" }, xhr: true + assert_response :success + + @user_upgrade = @purchaser.purchased_upgrades.last + assert_equal(@purchaser, @user_upgrade.purchaser) + assert_equal(@recipient, @user_upgrade.recipient) + assert_equal("gold", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end + end + + context "to Platinum" do + should "create a pending upgrade" do + @recipient = create(:member_user) + @purchaser = create(:member_user) + + post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "platinum" }, xhr: true + assert_response :success + + @user_upgrade = @purchaser.purchased_upgrades.last + assert_equal(@purchaser, @user_upgrade.purchaser) + assert_equal(@recipient, @user_upgrade.recipient) + assert_equal("platinum", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end + end + + context "from Gold to Platinum" do + should "create a pending upgrade" do + @recipient = create(:gold_user) + @purchaser = create(:member_user) + + post_auth user_upgrades_path(user_id: @recipient.id), @purchaser, params: { upgrade_type: "gold_to_platinum" }, xhr: true + assert_response :success + + @user_upgrade = @purchaser.purchased_upgrades.last + assert_equal(@purchaser, @user_upgrade.purchaser) + assert_equal(@recipient, @user_upgrade.recipient) + assert_equal("gold_to_platinum", @user_upgrade.upgrade_type) + assert_equal("pending", @user_upgrade.status) + assert_not_nil(@user_upgrade.stripe_id) + assert_match(/redirectToCheckout/, response.body) + end end end end diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb index afb596be4..dd10f8df8 100644 --- a/test/functional/webhooks_controller_test.rb +++ b/test/functional/webhooks_controller_test.rb @@ -3,8 +3,8 @@ require 'test_helper' class WebhooksControllerTest < ActionDispatch::IntegrationTest mock_stripe! - def post_webhook(*args, **metadata) - event = StripeMock.mock_webhook_event(*args, metadata: metadata) + def post_webhook(*args, payment_status: "paid", **metadata) + event = StripeMock.mock_webhook_event(*args, payment_status: payment_status, metadata: metadata) signature = generate_stripe_signature(event) headers = { "Stripe-Signature": signature } @@ -58,105 +58,83 @@ class WebhooksControllerTest < ActionDispatch::IntegrationTest end context "for a checkout.session.completed event" do + context "for completed event with an unpaid payment status" do + should "not upgrade the user" do + @user_upgrade = create(:self_gold_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id, payment_status: "unpaid" }) + + assert_response 200 + assert_equal("processing", @user_upgrade.reload.status) + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.reload.level) + end + end + context "for a self upgrade" do - context "of a Member to Gold" do + context "to Gold" do should "upgrade the user" do - @user = create(:member_user) - - post_webhook("checkout.session.completed", { - recipient_id: @user.id, - purchaser_id: @user.id, - upgrade_type: "gold_upgrade", - level: User::Levels::GOLD, - }) + @user_upgrade = create(:self_gold_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::GOLD, @user.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level) end end - context "of a Member to Platinum" do + context "to Platinum" do should "upgrade the user" do - @user = create(:member_user) - - post_webhook("checkout.session.completed", { - recipient_id: @user.id, - purchaser_id: @user.id, - upgrade_type: "platinum_upgrade", - level: User::Levels::PLATINUM, - }) + @user_upgrade = create(:self_platinum_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::PLATINUM, @user.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level) end end - context "of a Gold user to Platinum" do + context "from Gold to Platinum" do should "upgrade the user" do - @user = create(:gold_user) - - post_webhook("checkout.session.completed", { - recipient_id: @user.id, - purchaser_id: @user.id, - upgrade_type: "gold_to_platinum_upgrade", - level: User::Levels::PLATINUM, - }) + @user_upgrade = create(:self_gold_to_platinum_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::PLATINUM, @user.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level) end end end context "for a gifted upgrade" do - context "of a Member to Gold" do + context "to Gold" do should "upgrade the user" do - @recipient = create(:member_user) - @purchaser = create(:member_user) - - post_webhook("checkout.session.completed", { - recipient_id: @recipient.id, - purchaser_id: @purchaser.id, - upgrade_type: "gold_upgrade", - level: User::Levels::GOLD, - }) + @user_upgrade = create(:gift_gold_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::GOLD, @recipient.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::GOLD, @user_upgrade.recipient.reload.level) end end - context "of a Member to Platinum" do + context "to Platinum" do should "upgrade the user" do - @recipient = create(:member_user) - @purchaser = create(:member_user) - - post_webhook("checkout.session.completed", { - recipient_id: @recipient.id, - purchaser_id: @purchaser.id, - upgrade_type: "platinum_upgrade", - level: User::Levels::PLATINUM, - }) + @user_upgrade = create(:gift_platinum_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::PLATINUM, @recipient.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level) end end - context "of a Gold user to Platinum" do + context "from Gold to Platinum" do should "upgrade the user" do - @recipient = create(:gold_user) - @purchaser = create(:member_user) - - post_webhook("checkout.session.completed", { - recipient_id: @recipient.id, - purchaser_id: @purchaser.id, - upgrade_type: "gold_to_platinum_upgrade", - level: User::Levels::PLATINUM, - }) + @user_upgrade = create(:gift_gold_to_platinum_upgrade) + post_webhook("checkout.session.completed", { user_upgrade_id: @user_upgrade.id }) assert_response 200 - assert_equal(User::Levels::PLATINUM, @recipient.reload.level) + assert_equal("complete", @user_upgrade.reload.status) + assert_equal(User::Levels::PLATINUM, @user_upgrade.recipient.reload.level) end end end diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb index b6d228246..070fa2f48 100644 --- a/test/unit/user_upgrade_test.rb +++ b/test/unit/user_upgrade_test.rb @@ -3,37 +3,45 @@ require 'test_helper' class UserUpgradeTest < ActiveSupport::TestCase context "UserUpgrade:" do context "the #process_upgrade! method" do - setup do - @user = create(:user) - @user_upgrade = UserUpgrade.new(recipient: @user, purchaser: @user, level: User::Levels::GOLD) - end - - should "update the user's level" do - @user_upgrade.process_upgrade! - assert_equal(User::Levels::GOLD, @user.reload.level) - end - - should "log an account upgrade modaction" do - assert_difference("ModAction.user_account_upgrade.count") do - @user_upgrade.process_upgrade! - end - end - - should "send the user a dmail" do - assert_difference("@user.dmails.received.count") do - @user_upgrade.process_upgrade! - end - end - - context "for an upgrade for a user above Platinum level" do - should "not demote the user" do - @user.update!(level: User::Levels::BUILDER) - - assert_raise(User::PrivilegeError) do - @user_upgrade.process_upgrade! + context "for a self upgrade" do + context "to Gold" do + setup do + @user_upgrade = create(:self_gold_upgrade) end - assert_equal(true, @user.reload.is_builder?) + should "update the user's level if the payment status is paid" do + @user_upgrade.process_upgrade!("paid") + + assert_equal(User::Levels::GOLD, @user_upgrade.recipient.level) + assert_equal("complete", @user_upgrade.status) + end + + should "not update the user's level if the payment is unpaid" do + @user_upgrade.process_upgrade!("unpaid") + + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level) + assert_equal("processing", @user_upgrade.status) + end + + should "not update the user's level if the upgrade status is complete" do + @user_upgrade.update!(status: "complete") + @user_upgrade.process_upgrade!("paid") + + assert_equal(User::Levels::MEMBER, @user_upgrade.recipient.level) + assert_equal("complete", @user_upgrade.status) + end + + should "log an account upgrade modaction" do + assert_difference("ModAction.user_account_upgrade.count") do + @user_upgrade.process_upgrade!("paid") + end + end + + should "send the recipient a dmail" do + assert_difference("@user_upgrade.recipient.dmails.received.count") do + @user_upgrade.process_upgrade!("paid") + end + end end end end