+
+ <% if CurrentUser.user.level <= User::Levels::MEMBER %>
+
Enter your upgrade code below to upgrade your account to Gold.
+
+
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.
+
+ <% if CurrentUser.user.is_anonymous? %>
+
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.
+
+ How do I buy <%= Danbooru.config.canonical_app_name %> Gold?
+
+
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.
+
+
What are the benefits of <%= Danbooru.config.canonical_app_name %> Gold?
diff --git a/app/views/user_upgrades/show.html.erb b/app/views/user_upgrades/show.html.erb
index a81978fc5..410425cc7 100644
--- a/app/views/user_upgrades/show.html.erb
+++ b/app/views/user_upgrades/show.html.erb
@@ -37,14 +37,10 @@
<% else %>
You are now a <%= @user_upgrade.level_string %> user. Thanks for supporting the site! A receipt has been sent to your email.
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,
please contact your bank for assistance.
<%= link_to "Upgrade your account for only #{number_to_currency(UserUpgrade.gold_price)}!", new_user_upgrade_path, id: "goto-upgrade-account" %>
+
<%= link_to "Upgrade your account for only #{number_to_currency(UserUpgrade.gold_price, precision: 0)}!", new_user_upgrade_path, id: "goto-upgrade-account" %>
<%= link_to "No thanks", "#", id: "hide-upgrade-account-notice" %>
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index aa0e952a7..7613ddd96 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -392,6 +392,14 @@ module Danbooru
true
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
end
diff --git a/config/routes.rb b/config/routes.rb
index 4836fad02..72a34327a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -248,6 +248,13 @@ Rails.application.routes.draw do
resources :tags
resources :tag_aliases, 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
collection do
get :batch, to: redirect(path: "/uploads/new")
diff --git a/db/migrate/20220525214746_create_upgrade_codes.rb b/db/migrate/20220525214746_create_upgrade_codes.rb
new file mode 100644
index 000000000..0407fed58
--- /dev/null
+++ b/db/migrate/20220525214746_create_upgrade_codes.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index 560b88215..4c3daceac 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -1909,6 +1909,41 @@ CREATE SEQUENCE public.tags_id_seq
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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -2993,6 +3035,14 @@ ALTER TABLE ONLY public.tags
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: -
--
@@ -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);
+--
+-- 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: -
--
@@ -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;
+--
+-- 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: -
--
@@ -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;
+--
+-- 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: -
--
@@ -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;
+--
+-- 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: -
--
@@ -5832,6 +5941,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20220407203236'),
('20220410050628'),
('20220504235329'),
-('20220514175125');
+('20220514175125'),
+('20220525214746');
diff --git a/script/fixes/110_generate_upgrade_codes.rb b/script/fixes/110_generate_upgrade_codes.rb
new file mode 100755
index 000000000..de84395c5
--- /dev/null
+++ b/script/fixes/110_generate_upgrade_codes.rb
@@ -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
diff --git a/test/factories/upgrade_code.rb b/test/factories/upgrade_code.rb
new file mode 100644
index 000000000..79d2c9e85
--- /dev/null
+++ b/test/factories/upgrade_code.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory(:upgrade_code) do
+ creator factory: :user
+ end
+end
diff --git a/test/functional/upgrade_codes_controller_test.rb b/test/functional/upgrade_codes_controller_test.rb
new file mode 100644
index 000000000..41eb43778
--- /dev/null
+++ b/test/functional/upgrade_codes_controller_test.rb
@@ -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