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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
9
app/policies/user_upgrade_policy.rb
Normal file
9
app/policies/user_upgrade_policy.rb
Normal file
@@ -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
|
||||
@@ -101,11 +101,11 @@
|
||||
<p>You can pay with a credit or debit card. Safebooru uses <a href="https://www.stripe.com">Stripe</a>
|
||||
as a payment intermediary so none of your personal information will be stored on the site.</p>
|
||||
|
||||
<% if user.level < User::Levels::GOLD %>
|
||||
<p><%= button_to "Upgrade to Gold", user_upgrade_path(user_id: user.id, level: User::Levels::GOLD), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<p><%= button_to "Upgrade to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<% elsif user.level < User::Levels::PLATINUM %>
|
||||
<p><%= button_to "Upgrade Gold to Platinum", user_upgrade_path(user_id: user.id, level: User::Levels::PLATINUM), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<% if user.level == User::Levels::MEMBER %>
|
||||
<p><%= button_to "Upgrade to Gold", user_upgrades_path(user_id: user.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<p><%= button_to "Upgrade to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<% elsif user.level == User::Levels::GOLD %>
|
||||
<p><%= button_to "Upgrade Gold to Platinum", user_upgrades_path(user_id: user.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
<% page_title "Account Upgraded" %>
|
||||
<% page_title "User Upgrade Status" %>
|
||||
<%= render "users/secondary_links" %>
|
||||
|
||||
<div id="c-user-upgrades">
|
||||
<div id="a-show">
|
||||
<% if flash[:success] %>
|
||||
<h1>Congratulations!</h1>
|
||||
<h1>User Upgrade</h1>
|
||||
|
||||
<% if user != CurrentUser.user %>
|
||||
<p><%= user.name %> is now a <%= user.level_string %> user. Thanks for supporting the site!</p>
|
||||
<p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Purchased</strong>
|
||||
<%= 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 %>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Upgrade Type</strong>
|
||||
<%= @user_upgrade.upgrade_type.humanize %>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Status</strong>
|
||||
<%= @user_upgrade.status.humanize %>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<% if @user_upgrade.status == "complete" %>
|
||||
<% if @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.recipient %>
|
||||
<p><%= link_to_user @user_upgrade.purchaser %> has upgraded your account to <%= @user_upgrade.level_string %>. Enjoy your new account!</p>
|
||||
<% elsif @user_upgrade.is_gift? && CurrentUser.user == @user_upgrade.purchaser %>
|
||||
<p><%= link_to_user @user_upgrade.recipient %> is now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!</p>
|
||||
<% else %>
|
||||
<p>You are now a <%= user.level_string %> user. Thanks for supporting the site!</p>
|
||||
<p>You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site!</p>
|
||||
<% end %>
|
||||
|
||||
<p><%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to start using your new account.</p>
|
||||
<% elsif flash[:error] %>
|
||||
<h1>An error occurred!</h1>
|
||||
<p><%= flash[:error] %></p>
|
||||
<p><%= link_to "Try again", new_user_upgrade_path %></p>
|
||||
<p><%= link_to "Go back to #{Danbooru.config.canonical_app_name}", "https://danbooru.donmai.us" %> to continue using the site.</p>
|
||||
<% else %>
|
||||
<%= content_for :html_header do %>
|
||||
<meta http-equiv="refresh" content="5">
|
||||
<% end %>
|
||||
|
||||
<p>This order is still being processed. You will be notified as soon as the order is complete.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user