user upgrades: add upgrade code system.
Add a system for upgrading accounts using upgrade codes. Users purchase an upgrade code off-site then redeem it on-site to upgrade their account to Gold. Upgrade codes are randomly pre-generated and are one time use only. Codes have enough randomness that guessing a code is infeasible.
This commit is contained in:
@@ -136,6 +136,8 @@ class ApplicationController < ActionController::Base
|
|||||||
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
|
render_error_page(422, exception, template: "static/tag_limit_error", message: "You cannot search for more than #{CurrentUser.tag_query_limit} tags at a time.")
|
||||||
when PostQuery::Error
|
when PostQuery::Error
|
||||||
render_error_page(422, exception, message: exception.message)
|
render_error_page(422, exception, message: exception.message)
|
||||||
|
when UpgradeCode::InvalidCodeError, UpgradeCode::RedeemedCodeError, UpgradeCode::AlreadyUpgradedError
|
||||||
|
render_error_page(422, exception, message: exception.message)
|
||||||
when RateLimiter::RateLimitError
|
when RateLimiter::RateLimitError
|
||||||
render_error_page(429, exception, message: "Rate limit exceeded. You're doing that too fast")
|
render_error_page(429, exception, message: "Rate limit exceeded. You're doing that too fast")
|
||||||
when PageRemovedError
|
when PageRemovedError
|
||||||
|
|||||||
19
app/controllers/upgrade_codes_controller.rb
Normal file
19
app/controllers/upgrade_codes_controller.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpgradeCodesController < ApplicationController
|
||||||
|
respond_to :js, :html, :json, :xml
|
||||||
|
|
||||||
|
def index
|
||||||
|
@upgrade_codes = authorize UpgradeCode.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||||
|
respond_with(@upgrade_codes)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redeem
|
||||||
|
end
|
||||||
|
|
||||||
|
def upgrade
|
||||||
|
@upgrade_code = UpgradeCode.redeem!(code: params.dig(:upgrade_code, :code), redeemer: CurrentUser.user)
|
||||||
|
|
||||||
|
respond_with(@upgrade_code, location: @upgrade_code.user_upgrade)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,6 +11,9 @@ class WebhooksController < ApplicationController
|
|||||||
when "stripe"
|
when "stripe"
|
||||||
PaymentTransaction::Stripe.receive_webhook(request)
|
PaymentTransaction::Stripe.receive_webhook(request)
|
||||||
head 200
|
head 200
|
||||||
|
when "shopify"
|
||||||
|
PaymentTransaction::Shopify.receive_webhook(request)
|
||||||
|
head 200
|
||||||
when "discord"
|
when "discord"
|
||||||
json = DiscordSlashCommand.receive_webhook(request)
|
json = DiscordSlashCommand.receive_webhook(request)
|
||||||
render json: json
|
render json: json
|
||||||
|
|||||||
30
app/logical/payment_transaction/shopify.rb
Normal file
30
app/logical/payment_transaction/shopify.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PaymentTransaction::Shopify < PaymentTransaction
|
||||||
|
class InvalidWebhookError < StandardError; end
|
||||||
|
|
||||||
|
def create!(**params)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def refund!(reason = nil)
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
concerning :WebhookMethods do
|
||||||
|
class_methods do
|
||||||
|
def receive_webhook(request)
|
||||||
|
verify_webhook!(request)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def verify_webhook!(request)
|
||||||
|
payload = request.body.read
|
||||||
|
actual_signature = request.headers["X-Shopify-Hmac-Sha256"].to_s
|
||||||
|
calculated_signature = Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", Danbooru.config.shopify_webhook_secret, payload))
|
||||||
|
raise InvalidWebhookError unless ActiveSupport::SecurityUtils::secure_compare(actual_signature, calculated_signature)
|
||||||
|
|
||||||
|
request
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
62
app/models/upgrade_code.rb
Normal file
62
app/models/upgrade_code.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# A code that can be redeemed for a Gold account. Codes are pre-generated and
|
||||||
|
# one time use only. Upgrade codes are sold in the Danbooru store.
|
||||||
|
class UpgradeCode < ApplicationRecord
|
||||||
|
class InvalidCodeError < StandardError; end
|
||||||
|
class RedeemedCodeError < StandardError; end
|
||||||
|
class AlreadyUpgradedError < StandardError; end
|
||||||
|
|
||||||
|
UPGRADE_CODE_LENGTH = 8
|
||||||
|
|
||||||
|
attribute :code, default: -> { UpgradeCode.generate_code }
|
||||||
|
attribute :status, default: :unsold
|
||||||
|
|
||||||
|
belongs_to :creator, class_name: "User"
|
||||||
|
belongs_to :redeemer, class_name: "User", optional: true
|
||||||
|
belongs_to :user_upgrade, optional: true
|
||||||
|
|
||||||
|
enum status: {
|
||||||
|
unsold: 0,
|
||||||
|
unredeemed: 100,
|
||||||
|
redeemed: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
def self.visible(user)
|
||||||
|
if user.is_owner?
|
||||||
|
all
|
||||||
|
else
|
||||||
|
where(redeemer: user).or(where(creator: user))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.search(params)
|
||||||
|
q = search_attributes(params, :id, :created_at, :updated_at, :code, :status, :creator, :redeemer, :user_upgrade)
|
||||||
|
q.apply_default_order(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.generate_code
|
||||||
|
SecureRandom.send(:choose, [*"0".."9", *"A".."Z", *"a".."z"], UPGRADE_CODE_LENGTH) # base62
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.redeem!(code:, redeemer:)
|
||||||
|
upgrade_code = UpgradeCode.find_by(code: code)
|
||||||
|
raise InvalidCodeError, "This upgrade code is invalid" if upgrade_code.nil?
|
||||||
|
|
||||||
|
upgrade_code.redeem!(redeemer)
|
||||||
|
end
|
||||||
|
|
||||||
|
def redeem!(redeemer)
|
||||||
|
transaction do
|
||||||
|
raise RedeemedCodeError, "This upgrade code has already been used" if redeemed?
|
||||||
|
raise AlreadyUpgradedError, "Your account is already Gold or higher" if redeemer.is_gold?
|
||||||
|
|
||||||
|
user_upgrade = UserUpgrade.create!(recipient: redeemer, purchaser: redeemer, status: "processing", upgrade_type: "gold", payment_processor: "upgrade_code")
|
||||||
|
user_upgrade.process_upgrade!("paid")
|
||||||
|
|
||||||
|
update!(status: :redeemed, redeemer: redeemer, user_upgrade: user_upgrade)
|
||||||
|
|
||||||
|
self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -22,6 +22,8 @@ class UserUpgrade < ApplicationRecord
|
|||||||
enum payment_processor: {
|
enum payment_processor: {
|
||||||
stripe: 0,
|
stripe: 0,
|
||||||
authorize_net: 100,
|
authorize_net: 100,
|
||||||
|
shopify: 200,
|
||||||
|
upgrade_code: 300,
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :gifted, -> { where("recipient_id != purchaser_id") }
|
scope :gifted, -> { where("recipient_id != purchaser_id") }
|
||||||
@@ -113,7 +115,7 @@ class UserUpgrade < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
concerning :UpgradeMethods do
|
concerning :UpgradeMethods do
|
||||||
def process_upgrade!(payment_status)
|
def process_upgrade!(payment_status = "paid")
|
||||||
recipient.with_lock do
|
recipient.with_lock do
|
||||||
return unless pending? || processing?
|
return unless pending? || processing?
|
||||||
|
|
||||||
@@ -174,6 +176,8 @@ class UserUpgrade < ApplicationRecord
|
|||||||
PaymentTransaction::Stripe.new(self)
|
PaymentTransaction::Stripe.new(self)
|
||||||
in "authorize_net"
|
in "authorize_net"
|
||||||
PaymentTransaction::AuthorizeNet.new(self)
|
PaymentTransaction::AuthorizeNet.new(self)
|
||||||
|
in "shopify"
|
||||||
|
PaymentTransaction::Shopify.new(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
15
app/policies/upgrade_code_policy.rb
Normal file
15
app/policies/upgrade_code_policy.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UpgradeCodePolicy < ApplicationPolicy
|
||||||
|
def index?
|
||||||
|
user.is_owner?
|
||||||
|
end
|
||||||
|
|
||||||
|
def redeem?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def upgrade?
|
||||||
|
unbanned?
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
<li><%= link_to "Favorite groups", favorite_groups_path %></li>
|
<li><%= link_to "Favorite groups", favorite_groups_path %></li>
|
||||||
<li><%= link_to "Saved searches", saved_searches_path %></li>
|
<li><%= link_to "Saved searches", saved_searches_path %></li>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li><%= link_to "Upgrade information", new_user_upgrade_path %></li>
|
<li><%= link_to "Upgrade account", new_user_upgrade_path %></li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul>
|
<ul>
|
||||||
<li><h2>Users</h2></li>
|
<li><h2>Users</h2></li>
|
||||||
|
|||||||
29
app/views/upgrade_codes/index.html.erb
Normal file
29
app/views/upgrade_codes/index.html.erb
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<div id="c-upgrade-codes">
|
||||||
|
<div id="a-index">
|
||||||
|
<%= search_form_for(upgrade_codes_path) do |f| %>
|
||||||
|
<%= f.input :redeemer_name, label: "Redeemer", input_html: { value: params[:search][:redeemer_name], data: { autocomplete: "user" } } %>
|
||||||
|
<%= f.input :status, collection: UpgradeCode.statuses, include_blank: true, selected: params[:search][:status] %>
|
||||||
|
<%= f.submit "Search" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= table_for @upgrade_codes, class: "striped autofit" do |t| %>
|
||||||
|
<% t.column :code do |user_upgrade| %>
|
||||||
|
<%= tag.span user_upgrade.code, class: "font-monospace" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% t.column "Redeemer" do |upgrade_code| %>
|
||||||
|
<% if upgrade_code.redeemer.present? %>
|
||||||
|
<%= link_to_user(upgrade_code.redeemer) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% t.column :status %>
|
||||||
|
|
||||||
|
<% t.column "Updated" do |upgrade_code| %>
|
||||||
|
<%= time_ago_in_words_tagged(upgrade_code.updated_at) %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= numbered_paginator(@upgrade_codes) %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
28
app/views/upgrade_codes/redeem.html.erb
Normal file
28
app/views/upgrade_codes/redeem.html.erb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<% page_title "Redeem upgrade code" %>
|
||||||
|
|
||||||
|
<%= render "users/secondary_links" %>
|
||||||
|
|
||||||
|
<div id="c-user-upgrades">
|
||||||
|
<div id="a-redeem" class="fixed-width-container mx-auto">
|
||||||
|
<h1 class="text-center mb-4">Redeem upgrade code</h1>
|
||||||
|
|
||||||
|
<% if CurrentUser.user.level <= User::Levels::MEMBER %>
|
||||||
|
<p>Enter your upgrade code below to upgrade your account to Gold.</p>
|
||||||
|
|
||||||
|
<p>After you purchase a Gold account, you will receive an upgrade code. Enter it here to upgrade your account. If you don't have a code,
|
||||||
|
go to the <%= link_to "upgrade page", new_user_upgrade_path %> first to buy one, then come back here to redeem it.</p>
|
||||||
|
|
||||||
|
<% if CurrentUser.user.is_anonymous? %>
|
||||||
|
<p>You must <%= link_to "login", login_path(url: redeem_upgrade_codes_path) %> or
|
||||||
|
<%= link_to "create a new account", new_user_path(url: redeem_upgrade_codes_path) %> first before you can redeem your upgrade code.</p>
|
||||||
|
<% else %>
|
||||||
|
<%= simple_form_for(:upgrade_code, url: upgrade_upgrade_codes_path, remote: true) do |f| %>
|
||||||
|
<%= f.input :code, placeholder: "abcd1234", input_html: { value: params[:code] } %>
|
||||||
|
<%= f.submit "Upgrade account", class: "button-primary" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-center">Your account has already been upgraded.</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
1
app/views/upgrade_codes/upgrade.js.erb
Normal file
1
app/views/upgrade_codes/upgrade.js.erb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
window.location.assign("<%= j user_upgrade_path(@upgrade_code.user_upgrade) %>");
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
var $input = $('<input type="hidden" name="token">').val(token);
|
var $input = $('<input type="hidden" name="token">').val(token);
|
||||||
$form.append($input).appendTo("body").submit();
|
$form.append($input).appendTo("body").submit();
|
||||||
});
|
});
|
||||||
|
<% elsif @user_upgrade.shopify? %>
|
||||||
|
window.history.pushState({}, "", location.href);
|
||||||
|
window.location.assign("<%= j Danbooru.config.shopify_purchase_url %>?attributes[user_upgrade_id]=<%= @user_upgrade.id %>&attributes[purchaser_id]=<%= @user_upgrade.purchaser_id %>&attributes[purchaser_name]=<%= @user_upgrade.purchaser.name %>&checkout[email]=<%= @user_upgrade.purchaser&.email_address&.address %>");
|
||||||
<% else %>
|
<% else %>
|
||||||
<% raise NotImplementedError, "payment method not implemented" %>
|
<% raise NotImplementedError, "payment method not implemented" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<% if Danbooru.config.is_promotion? %>
|
<% if Danbooru.config.is_promotion? %>
|
||||||
<s>$20</s>
|
<s>$20</s>
|
||||||
<% end %>
|
<% end %>
|
||||||
<b><%= number_to_currency(UserUpgrade.gold_price) %></b>
|
<b><%= number_to_currency(UserUpgrade.gold_price, precision: 0) %></b>
|
||||||
<div class="fineprint">One time fee</div>
|
<div class="fineprint">One time fee</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -90,15 +90,6 @@
|
|||||||
<td>3 seconds</td>
|
<td>3 seconds</td>
|
||||||
<td>6 seconds</td>
|
<td>6 seconds</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td></td>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<%= image_pack_tag("static/mastercard-logo.svg", width: 28, class: "icon") %>
|
|
||||||
<%= image_pack_tag("static/visa-logo.svg", width: 28, class: "icon") %>
|
|
||||||
<%= image_pack_tag("static/discover-logo.svg", width: 28, class: "icon") %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -108,13 +99,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<% if !UserUpgrade.enabled? %>
|
<% if !UserUpgrade.enabled? %>
|
||||||
<%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id), class: "button-primary", disabled: true %>
|
<%= link_to "Upgrade to Gold", user_upgrades_path(user_id: @recipient.id), class: "button-primary", disabled: true %>
|
||||||
<% elsif @user_upgrade.purchaser.is_anonymous? %>
|
<% elsif @user_upgrade.purchaser.is_anonymous? %>
|
||||||
<%= link_to "Get #{Danbooru.config.canonical_app_name} Gold", new_user_path(url: new_user_upgrade_path), class: "button-primary" %>
|
<%= link_to "Upgrade to Gold", new_user_path(url: new_user_upgrade_path), class: "button-primary" %>
|
||||||
|
<%= link_to "Redeem upgrade code", redeem_upgrade_codes_path, class: "text-sm" %>
|
||||||
<% elsif @user_upgrade.recipient.level <= User::Levels::MEMBER %>
|
<% elsif @user_upgrade.recipient.level <= User::Levels::MEMBER %>
|
||||||
<%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold", country: params[:country], promo: params[:promo], payment_processor: "authorize_net"), class: "button-primary", remote: true, disable_with: "Redirecting..." %>
|
<%= button_to "Upgrade to Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold", country: params[:country], promo: params[:promo], payment_processor: "shopify"), class: "button-primary mb-2", remote: true, disable_with: "Redirecting..." %>
|
||||||
|
<%= link_to "Redeem upgrade code", redeem_upgrade_codes_path, class: "text-sm" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id), class: "button-primary", disabled: true %>
|
<%= link_to "Upgrade to Gold", user_upgrades_path(user_id: @recipient.id), class: "button-primary", disabled: true %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -130,6 +123,13 @@
|
|||||||
<h2 class="mb-4">Frequently Asked Questions</h2>
|
<h2 class="mb-4">Frequently Asked Questions</h2>
|
||||||
|
|
||||||
<div id="frequently-asked-questions" class="divide-y-1">
|
<div id="frequently-asked-questions" class="divide-y-1">
|
||||||
|
<details>
|
||||||
|
<summary>How do I buy <%= Danbooru.config.canonical_app_name %> Gold?</summary>
|
||||||
|
|
||||||
|
<p>Click the "Upgrade to Gold" button on this page. After you purchase an upgrade, you will receive a code you can
|
||||||
|
<%= link_to "redeem here", redeem_upgrade_codes_path %> to upgrade your account to Gold.</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?</summary>
|
<summary>What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?</summary>
|
||||||
|
|
||||||
|
|||||||
@@ -37,14 +37,10 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site! A receipt has been sent to your email.</p>
|
<p>You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site! A receipt has been sent to your email.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= render "stripe_links", user_upgrade: @user_upgrade %>
|
|
||||||
<% elsif @user_upgrade.refunded? %>
|
<% elsif @user_upgrade.refunded? %>
|
||||||
<p>This purchase has been refunded. A receipt has been sent to your email. It can take up to
|
<p>This purchase has been refunded. A receipt has been sent to your email. It can take up to
|
||||||
5-10 days for the refund to appear on your credit card or bank statement. If it takes longer,
|
5-10 days for the refund to appear on your credit card or bank statement. If it takes longer,
|
||||||
please contact your bank for assistance.</p>
|
please contact your bank for assistance.</p>
|
||||||
|
|
||||||
<%= render "stripe_links", user_upgrade: @user_upgrade %>
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= content_for :html_header do %>
|
<%= content_for :html_header do %>
|
||||||
<meta http-equiv="refresh" content="5">
|
<meta http-equiv="refresh" content="5">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="notice notice-info notice-large" id="upgrade-account-notice">
|
<div class="notice notice-info notice-large" id="upgrade-account-notice">
|
||||||
<h2><%= link_to "Upgrade your account for only #{number_to_currency(UserUpgrade.gold_price)}!", new_user_upgrade_path, id: "goto-upgrade-account" %></h2>
|
<h2><%= link_to "Upgrade your account for only #{number_to_currency(UserUpgrade.gold_price, precision: 0)}!", new_user_upgrade_path, id: "goto-upgrade-account" %></h2>
|
||||||
<div><%= link_to "No thanks", "#", id: "hide-upgrade-account-notice" %></div>
|
<div><%= link_to "No thanks", "#", id: "hide-upgrade-account-notice" %></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -392,6 +392,14 @@ module Danbooru
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The URL of the Shopify checkout page where account upgrades are sold.
|
||||||
|
def shopify_checkout_url
|
||||||
|
end
|
||||||
|
|
||||||
|
# The secret used to verify webhooks from Shopify. Get it from the https://xxx.myshopify.com/admin/settings/notifications page.
|
||||||
|
def shopify_webhook_secret
|
||||||
|
end
|
||||||
|
|
||||||
def stripe_secret_key
|
def stripe_secret_key
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -248,6 +248,13 @@ Rails.application.routes.draw do
|
|||||||
resources :tags
|
resources :tags
|
||||||
resources :tag_aliases, only: [:show, :index, :destroy]
|
resources :tag_aliases, only: [:show, :index, :destroy]
|
||||||
resources :tag_implications, only: [:show, :index, :destroy]
|
resources :tag_implications, only: [:show, :index, :destroy]
|
||||||
|
|
||||||
|
get "/redeem", to: "upgrade_codes#redeem", as: "redeem_upgrade_codes"
|
||||||
|
resources :upgrade_codes, only: [:create, :index] do
|
||||||
|
get :redeem, on: :collection
|
||||||
|
post :upgrade, on: :collection
|
||||||
|
end
|
||||||
|
|
||||||
resources :uploads do
|
resources :uploads do
|
||||||
collection do
|
collection do
|
||||||
get :batch, to: redirect(path: "/uploads/new")
|
get :batch, to: redirect(path: "/uploads/new")
|
||||||
|
|||||||
22
db/migrate/20220525214746_create_upgrade_codes.rb
Normal file
22
db/migrate/20220525214746_create_upgrade_codes.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class CreateUpgradeCodes < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :upgrade_codes do |t|
|
||||||
|
t.timestamps
|
||||||
|
t.string :code, null: false
|
||||||
|
t.integer :status, null: false
|
||||||
|
t.integer :creator_id, null: false
|
||||||
|
t.integer :redeemer_id
|
||||||
|
t.integer :user_upgrade_id
|
||||||
|
|
||||||
|
t.index :code, unique: true
|
||||||
|
t.index :status
|
||||||
|
t.index :creator_id
|
||||||
|
t.index :redeemer_id, where: "redeemer_id IS NOT NULL"
|
||||||
|
t.index :user_upgrade_id, where: "user_upgrade_id IS NOT NULL"
|
||||||
|
|
||||||
|
t.foreign_key :users, column: :creator_id
|
||||||
|
t.foreign_key :users, column: :redeemer_id
|
||||||
|
t.foreign_key :user_upgrades, column: :user_upgrade_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
112
db/structure.sql
112
db/structure.sql
@@ -1909,6 +1909,41 @@ CREATE SEQUENCE public.tags_id_seq
|
|||||||
ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id;
|
ALTER SEQUENCE public.tags_id_seq OWNED BY public.tags.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes; Type: TABLE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.upgrade_codes (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_at timestamp(6) without time zone NOT NULL,
|
||||||
|
updated_at timestamp(6) without time zone NOT NULL,
|
||||||
|
code character varying NOT NULL,
|
||||||
|
status integer NOT NULL,
|
||||||
|
creator_id integer NOT NULL,
|
||||||
|
redeemer_id integer,
|
||||||
|
user_upgrade_id integer
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.upgrade_codes_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.upgrade_codes_id_seq OWNED BY public.upgrade_codes.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: upload_media_assets; Type: TABLE; Schema: public; Owner: -
|
-- Name: upload_media_assets; Type: TABLE; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -2539,6 +2574,13 @@ ALTER TABLE ONLY public.tag_implications ALTER COLUMN id SET DEFAULT nextval('pu
|
|||||||
ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id_seq'::regclass);
|
ALTER TABLE ONLY public.tags ALTER COLUMN id SET DEFAULT nextval('public.tags_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upgrade_codes ALTER COLUMN id SET DEFAULT nextval('public.upgrade_codes_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: upload_media_assets id; Type: DEFAULT; Schema: public; Owner: -
|
-- Name: upload_media_assets id; Type: DEFAULT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -2993,6 +3035,14 @@ ALTER TABLE ONLY public.tags
|
|||||||
ADD CONSTRAINT tags_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT tags_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes upgrade_codes_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upgrade_codes
|
||||||
|
ADD CONSTRAINT upgrade_codes_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: upload_media_assets upload_media_assets_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
-- Name: upload_media_assets upload_media_assets_pkey; Type: CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -4565,6 +4615,41 @@ CREATE INDEX index_tags_on_name_trgm ON public.tags USING gin (name public.gin_t
|
|||||||
CREATE INDEX index_tags_on_post_count ON public.tags USING btree (post_count);
|
CREATE INDEX index_tags_on_post_count ON public.tags USING btree (post_count);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_upgrade_codes_on_code; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX index_upgrade_codes_on_code ON public.upgrade_codes USING btree (code);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_upgrade_codes_on_creator_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_upgrade_codes_on_creator_id ON public.upgrade_codes USING btree (creator_id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_upgrade_codes_on_redeemer_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_upgrade_codes_on_redeemer_id ON public.upgrade_codes USING btree (redeemer_id) WHERE (redeemer_id IS NOT NULL);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_upgrade_codes_on_status; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_upgrade_codes_on_status ON public.upgrade_codes USING btree (status);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: index_upgrade_codes_on_user_upgrade_id; Type: INDEX; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE INDEX index_upgrade_codes_on_user_upgrade_id ON public.upgrade_codes USING btree (user_upgrade_id) WHERE (user_upgrade_id IS NOT NULL);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: index_upload_media_assets_on_error; Type: INDEX; Schema: public; Owner: -
|
-- Name: index_upload_media_assets_on_error; Type: INDEX; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -5255,6 +5340,14 @@ ALTER TABLE ONLY public.post_approvals
|
|||||||
ADD CONSTRAINT fk_rails_74f76ef71e FOREIGN KEY (post_id) REFERENCES public.posts(id) DEFERRABLE INITIALLY DEFERRED;
|
ADD CONSTRAINT fk_rails_74f76ef71e FOREIGN KEY (post_id) REFERENCES public.posts(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes fk_rails_778e1e40b5; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upgrade_codes
|
||||||
|
ADD CONSTRAINT fk_rails_778e1e40b5 FOREIGN KEY (redeemer_id) REFERENCES public.users(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: favorite_groups fk_rails_796204a5e3; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: favorite_groups fk_rails_796204a5e3; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -5263,6 +5356,14 @@ ALTER TABLE ONLY public.favorite_groups
|
|||||||
ADD CONSTRAINT fk_rails_796204a5e3 FOREIGN KEY (creator_id) REFERENCES public.users(id) DEFERRABLE INITIALLY DEFERRED;
|
ADD CONSTRAINT fk_rails_796204a5e3 FOREIGN KEY (creator_id) REFERENCES public.users(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes fk_rails_80bbec9661; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upgrade_codes
|
||||||
|
ADD CONSTRAINT fk_rails_80bbec9661 FOREIGN KEY (user_upgrade_id) REFERENCES public.user_upgrades(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: user_feedback fk_rails_81884ec765; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: user_feedback fk_rails_81884ec765; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -5431,6 +5532,14 @@ ALTER TABLE ONLY public.uploads
|
|||||||
ADD CONSTRAINT fk_rails_d29b037216 FOREIGN KEY (uploader_id) REFERENCES public.users(id) DEFERRABLE INITIALLY DEFERRED;
|
ADD CONSTRAINT fk_rails_d29b037216 FOREIGN KEY (uploader_id) REFERENCES public.users(id) DEFERRABLE INITIALLY DEFERRED;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Name: upgrade_codes fk_rails_d5a4e5e1a6; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.upgrade_codes
|
||||||
|
ADD CONSTRAINT fk_rails_d5a4e5e1a6 FOREIGN KEY (creator_id) REFERENCES public.users(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: tag_implications fk_rails_dba2c19f93; Type: FK CONSTRAINT; Schema: public; Owner: -
|
-- Name: tag_implications fk_rails_dba2c19f93; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||||
--
|
--
|
||||||
@@ -5832,6 +5941,7 @@ INSERT INTO "schema_migrations" (version) VALUES
|
|||||||
('20220407203236'),
|
('20220407203236'),
|
||||||
('20220410050628'),
|
('20220410050628'),
|
||||||
('20220504235329'),
|
('20220504235329'),
|
||||||
('20220514175125');
|
('20220514175125'),
|
||||||
|
('20220525214746');
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
8
script/fixes/110_generate_upgrade_codes.rb
Executable file
8
script/fixes/110_generate_upgrade_codes.rb
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require_relative "base"
|
||||||
|
|
||||||
|
5_000.times do
|
||||||
|
code = UpgradeCode.create!(creator: User.system)
|
||||||
|
puts "id=#{code.id} code=#{code.code}"
|
||||||
|
end
|
||||||
5
test/factories/upgrade_code.rb
Normal file
5
test/factories/upgrade_code.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FactoryBot.define do
|
||||||
|
factory(:upgrade_code) do
|
||||||
|
creator factory: :user
|
||||||
|
end
|
||||||
|
end
|
||||||
87
test/functional/upgrade_codes_controller_test.rb
Normal file
87
test/functional/upgrade_codes_controller_test.rb
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class UserUpgradesControllerTest < ActionDispatch::IntegrationTest
|
||||||
|
context "The upgrade codes controller" do
|
||||||
|
context "index action" do
|
||||||
|
should "render for the owner" do
|
||||||
|
create(:upgrade_code)
|
||||||
|
@user = create(:owner_user)
|
||||||
|
get_auth upgrade_codes_path, @user
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
should "not render for non-privileged users" do
|
||||||
|
create(:upgrade_code)
|
||||||
|
@user = create(:admin_user)
|
||||||
|
get_auth upgrade_codes_path, @user
|
||||||
|
|
||||||
|
assert_response 403
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "redeem action" do
|
||||||
|
should "render for an anonymous user" do
|
||||||
|
get redeem_upgrade_codes_path
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
should "render for a member user" do
|
||||||
|
get_auth redeem_upgrade_codes_path, create(:user)
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
|
||||||
|
should "render for a Gold user" do
|
||||||
|
get_auth redeem_upgrade_codes_path, create(:gold_user)
|
||||||
|
|
||||||
|
assert_response :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "upgrade action" do
|
||||||
|
should "return an error for an invalid code" do
|
||||||
|
code = create(:upgrade_code)
|
||||||
|
user = create(:user)
|
||||||
|
post_auth upgrade_upgrade_codes_path, user, params: { upgrade_code: { code: "abcd" }}, xhr: true
|
||||||
|
|
||||||
|
assert_response 422
|
||||||
|
assert_equal(false, user.reload.is_gold?)
|
||||||
|
assert_equal(false, code.reload.redeemed?)
|
||||||
|
assert_nil(code.redeemer)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return an error for an already redeemed code" do
|
||||||
|
code = create(:upgrade_code, status: :redeemed)
|
||||||
|
user = create(:user)
|
||||||
|
post_auth upgrade_upgrade_codes_path, user, params: { upgrade_code: { code: code.code }}, xhr: true
|
||||||
|
|
||||||
|
assert_response 422
|
||||||
|
assert_equal(false, user.reload.is_gold?)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "return an error for an already upgraded user" do
|
||||||
|
code = create(:upgrade_code)
|
||||||
|
user = create(:builder_user)
|
||||||
|
post_auth upgrade_upgrade_codes_path, user, params: { upgrade_code: { code: code.code }}, xhr: true
|
||||||
|
|
||||||
|
assert_response 422
|
||||||
|
assert_equal(true, user.reload.is_builder?)
|
||||||
|
assert_equal(false, code.reload.redeemed?)
|
||||||
|
assert_nil(code.redeemer)
|
||||||
|
end
|
||||||
|
|
||||||
|
should "upgrade the user for a unredeemed code" do
|
||||||
|
code = create(:upgrade_code)
|
||||||
|
user = create(:user)
|
||||||
|
post_auth upgrade_upgrade_codes_path, user, params: { upgrade_code: { code: code.code }}, xhr: true
|
||||||
|
|
||||||
|
assert_response 200
|
||||||
|
assert_equal(true, user.reload.is_gold?)
|
||||||
|
assert_equal(true, code.reload.redeemed?)
|
||||||
|
assert_equal(user, code.redeemer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user