From d0bb4ed39895754b323c884eea94a2555e8506ed Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 31 Dec 2020 04:32:07 -0600 Subject: [PATCH] user upgrades: add bank payment methods for European countries. Add the following bank redirect payment methods: * https://stripe.com/docs/payments/bancontact * https://stripe.com/docs/payments/eps * https://stripe.com/docs/payments/giropay * https://stripe.com/docs/payments/ideal * https://stripe.com/docs/payments/p24 These methods are used in Austria, Belgium, Germany, the Netherlands, and Poland. These methods require payments to be denominated in EUR, which means we have to set prices in both USD and EUR, and we have to automatically detect which currency to use based on the user's country. We also have to automatically detect which payment methods to offer based on the user's country. We do this by using Cloudflare's CF-IPCountry header to geolocate the user's country. This also switches to using prices and products defined in Stripe instead of generated on-the-fly when creating the checkout. --- app/controllers/user_upgrades_controller.rb | 3 +- app/models/user_upgrade.rb | 103 ++++++++++++------ app/views/user_upgrades/new.html.erb | 11 +- config/danbooru_default_config.rb | 21 ++++ test/functional/webhooks_controller_test.rb | 8 +- test/test_helpers/stripe_test_helper.rb | 4 +- test/unit/user_upgrade_test.rb | 112 ++++++++++++++++++++ 7 files changed, 219 insertions(+), 43 deletions(-) diff --git a/app/controllers/user_upgrades_controller.rb b/app/controllers/user_upgrades_controller.rb index c2e5f0160..e0e86e566 100644 --- a/app/controllers/user_upgrades_controller.rb +++ b/app/controllers/user_upgrades_controller.rb @@ -3,7 +3,8 @@ class UserUpgradesController < ApplicationController def create @user_upgrade = authorize UserUpgrade.create(recipient: recipient, purchaser: CurrentUser.user, status: "pending", upgrade_type: params[:upgrade_type]) - @checkout = @user_upgrade.create_checkout! + @country = params[:country] || CurrentUser.country || "US" + @checkout = @user_upgrade.create_checkout!(country: @country) respond_with(@user_upgrade) end diff --git a/app/models/user_upgrade.rb b/app/models/user_upgrade.rb index 9ed86e4ed..69f580f5c 100644 --- a/app/models/user_upgrade.rb +++ b/app/models/user_upgrade.rb @@ -76,32 +76,6 @@ class UserUpgrade < ApplicationRecord end end - def upgrade_price - case upgrade_type - when "gold" - UserUpgrade.gold_price - when "platinum" - UserUpgrade.platinum_price - when "gold_to_platinum" - UserUpgrade.gold_to_platinum_price - else - raise NotImplementedError - end - end - - def upgrade_description - case upgrade_type - when "gold" - "Upgrade to Gold" - when "platinum" - "Upgrade to Platinum" - when "gold_to_platinum" - "Upgrade Gold to Platinum" - else - raise NotImplementedError - end - end - def level_string User.level_string(level) end @@ -178,24 +152,25 @@ class UserUpgrade < ApplicationRecord end concerning :StripeMethods do - def create_checkout! + def create_checkout!(country: "US") + methods = payment_method_types(country) + currency = preferred_currency(country) + price_id = upgrade_price_id(currency) + checkout = Stripe::Checkout::Session.create( mode: "payment", success_url: Routes.user_upgrade_url(self), cancel_url: Routes.new_user_upgrade_url(user_id: recipient.id), client_reference_id: "user_upgrade_#{id}", customer_email: purchaser.email_address&.address, - payment_method_types: ["card"], + payment_method_types: methods, line_items: [{ - price_data: { - unit_amount: upgrade_price, - currency: "usd", - product_data: { - name: upgrade_description, - }, - }, + price: price_id, quantity: 1, }], + discounts: [{ + coupon: promotion_discount_id, + }], metadata: { user_upgrade_id: id, purchaser_id: purchaser.id, @@ -203,6 +178,7 @@ class UserUpgrade < ApplicationRecord purchaser_name: purchaser.name, recipient_name: recipient.name, upgrade_type: upgrade_type, + country: country, is_gift: is_gift?, level: level, }, @@ -252,6 +228,63 @@ class UserUpgrade < ApplicationRecord !pending? end + def promotion_discount_id + if Danbooru.config.is_promotion? + Danbooru.config.stripe_promotion_discount_id + end + end + + def upgrade_price_id(currency) + case [upgrade_type, currency] + when ["gold", "usd"] + Danbooru.config.stripe_gold_usd_price_id + when ["gold", "eur"] + Danbooru.config.stripe_gold_eur_price_id + when ["platinum", "usd"] + Danbooru.config.stripe_platinum_usd_price_id + when ["platinum", "eur"] + Danbooru.config.stripe_platinum_eur_price_id + when ["gold_to_platinum", "usd"] + Danbooru.config.stripe_gold_to_platinum_usd_price_id + when ["gold_to_platinum", "eur"] + Danbooru.config.stripe_gold_to_platinum_eur_price_id + else + raise NotImplementedError + end + end + + def payment_method_types(country) + case country.to_s.upcase + # Austria, https://stripe.com/docs/payments/bancontact + when "AT" + ["card", "eps"] + # Belgium, https://stripe.com/docs/payments/eps + when "BE" + ["card", "bancontact"] + # Germany, https://stripe.com/docs/payments/giropay + when "DE" + ["card", "giropay"] + # Netherlands, https://stripe.com/docs/payments/ideal + when "NL" + ["card", "ideal"] + # Poland, https://stripe.com/docs/payments/p24 + when "PL" + ["card", "p24"] + else + ["card"] + end + end + + def preferred_currency(country) + case country.to_s.upcase + # Austria, Belgium, Germany, Netherlands, Poland + when "AT", "BE", "DE", "NL", "PL" + "eur" + else + "usd" + end + end + class_methods do def register_webhook webhook = Stripe::WebhookEndpoint.create({ diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb index 6f125ebed..31a85d34d 100644 --- a/app/views/user_upgrades/new.html.erb +++ b/app/views/user_upgrades/new.html.erb @@ -112,12 +112,12 @@ <%= link_to "Get #{Danbooru.config.canonical_app_name} Platinum", login_path(url: new_user_upgrade_path), class: "login-button" %> <% elsif @recipient.level == User::Levels::MEMBER %> - <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %> - <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %> + <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold", country: params[:country]), remote: true, disable_with: "Redirecting..." %> + <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "platinum", country: params[:country]), remote: true, disable_with: "Redirecting..." %> <% elsif @recipient.level == User::Levels::GOLD %> <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %> - <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold_to_platinum"), remote: true, disable_with: "Redirecting..." %> + <%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold_to_platinum", country: params[:country]), remote: true, disable_with: "Redirecting..." %> <% else %> <%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %> @@ -150,7 +150,10 @@ What payment methods do you support?

We support all major credit and debit cards, including international - cards. Payments are securely handled by Stripe. + cards. We also support bank payments in several European countries, + including Austria, Belgium, Germany, the Netherlands, and Poland.

+ +

Payments are securely handled by Stripe. We don't support PayPal or Bitcoin at this time.

diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb index 947dc75c1..3467e4dcc 100644 --- a/config/danbooru_default_config.rb +++ b/config/danbooru_default_config.rb @@ -375,6 +375,27 @@ module Danbooru def stripe_webhook_secret end + def stripe_gold_usd_price_id + end + + def stripe_platinum_usd_price_id + end + + def stripe_gold_to_platinum_usd_price_id + end + + def stripe_gold_eur_price_id + end + + def stripe_platinum_eur_price_id + end + + def stripe_gold_to_platinum_eur_price_id + end + + def stripe_promotion_discount_id + end + def twitter_api_key end diff --git a/test/functional/webhooks_controller_test.rb b/test/functional/webhooks_controller_test.rb index dd10f8df8..3ac99ae1e 100644 --- a/test/functional/webhooks_controller_test.rb +++ b/test/functional/webhooks_controller_test.rb @@ -1,7 +1,13 @@ require 'test_helper' class WebhooksControllerTest < ActionDispatch::IntegrationTest - mock_stripe! + setup do + StripeMock.start + end + + teardown do + StripeMock.stop + end def post_webhook(*args, payment_status: "paid", **metadata) event = StripeMock.mock_webhook_event(*args, payment_status: payment_status, metadata: metadata) diff --git a/test/test_helpers/stripe_test_helper.rb b/test/test_helpers/stripe_test_helper.rb index d1f0f7126..9bfc845f9 100644 --- a/test/test_helpers/stripe_test_helper.rb +++ b/test/test_helpers/stripe_test_helper.rb @@ -3,11 +3,11 @@ StripeMock.webhook_fixture_path = "test/fixtures/stripe-webhooks" module StripeTestHelper def mock_stripe! setup do - StripeMock.start + StripeMock.start unless UserUpgrade.enabled? end teardown do - StripeMock.stop + StripeMock.stop unless UserUpgrade.enabled? end end end diff --git a/test/unit/user_upgrade_test.rb b/test/unit/user_upgrade_test.rb index 38102d2bf..d73eee270 100644 --- a/test/unit/user_upgrade_test.rb +++ b/test/unit/user_upgrade_test.rb @@ -58,6 +58,116 @@ class UserUpgradeTest < ActiveSupport::TestCase end end end + + context "for each upgrade type" do + setup do + skip unless UserUpgrade.enabled? + end + + should "choose the right price in USD for a gold upgrade" do + @user_upgrade = create(:self_gold_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "US") + + assert_equal(UserUpgrade.gold_price, @user_upgrade.payment_intent.amount) + assert_equal("usd", @user_upgrade.payment_intent.currency) + end + + should "choose the right price in USD for a platinum upgrade" do + @user_upgrade = create(:self_platinum_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "US") + + assert_equal(UserUpgrade.platinum_price, @user_upgrade.payment_intent.amount) + assert_equal("usd", @user_upgrade.payment_intent.currency) + end + + should "choose the right price in USD for a gold to platinum upgrade" do + @user_upgrade = create(:self_gold_to_platinum_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "US") + + assert_equal(UserUpgrade.gold_to_platinum_price, @user_upgrade.payment_intent.amount) + assert_equal("usd", @user_upgrade.payment_intent.currency) + end + + should "choose the right price in EUR for a gold upgrade" do + @user_upgrade = create(:self_gold_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "DE") + + assert_equal(0.8 * UserUpgrade.gold_price, @user_upgrade.payment_intent.amount) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right price in EUR for a platinum upgrade" do + @user_upgrade = create(:self_platinum_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "DE") + + assert_equal(0.8 * UserUpgrade.platinum_price, @user_upgrade.payment_intent.amount) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right price in EUR for a gold to platinum upgrade" do + @user_upgrade = create(:self_gold_to_platinum_upgrade) + @checkout = @user_upgrade.create_checkout!(country: "DE") + + assert_equal(0.8 * UserUpgrade.gold_to_platinum_price, @user_upgrade.payment_intent.amount) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + end + + context "for each country" do + setup do + @user_upgrade = create(:self_gold_upgrade) + skip unless UserUpgrade.enabled? + end + + should "choose the right payment methods for US" do + @checkout = @user_upgrade.create_checkout!(country: "US") + + assert_equal(["card"], @checkout.payment_method_types) + assert_equal("usd", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for AT" do + @checkout = @user_upgrade.create_checkout!(country: "AT") + + assert_equal(["card", "eps"], @checkout.payment_method_types) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for BE" do + @checkout = @user_upgrade.create_checkout!(country: "BE") + + assert_equal(["card", "bancontact"], @checkout.payment_method_types) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for DE" do + @checkout = @user_upgrade.create_checkout!(country: "DE") + + assert_equal(["card", "giropay"], @checkout.payment_method_types) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for NL" do + @checkout = @user_upgrade.create_checkout!(country: "NL") + + assert_equal(["card", "ideal"], @checkout.payment_method_types) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for PL" do + @checkout = @user_upgrade.create_checkout!(country: "PL") + + assert_equal(["card", "p24"], @checkout.payment_method_types) + assert_equal("eur", @user_upgrade.payment_intent.currency) + end + + should "choose the right payment methods for an unsupported country" do + @checkout = @user_upgrade.create_checkout!(country: "MX") + + assert_equal(["card"], @checkout.payment_method_types) + assert_equal("usd", @user_upgrade.payment_intent.currency) + end + end end context "the #receipt_url method" do @@ -65,6 +175,8 @@ class UserUpgradeTest < ActiveSupport::TestCase context "a pending upgrade" do should "not have a receipt" do + skip unless UserUpgrade.enabled? + @user_upgrade = create(:self_gold_upgrade, status: "pending") @user_upgrade.create_checkout!