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.
This commit is contained in:
evazion
2020-12-31 04:32:07 -06:00
parent bf09940a55
commit d0bb4ed398
7 changed files with 219 additions and 43 deletions

View File

@@ -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

View File

@@ -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({

View File

@@ -112,12 +112,12 @@
<td><%= link_to "Get #{Danbooru.config.canonical_app_name} Platinum", login_path(url: new_user_upgrade_path), class: "login-button" %></td>
<% elsif @recipient.level == User::Levels::MEMBER %>
<td></td>
<td><%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", user_upgrades_path(user_id: @recipient.id, upgrade_type: "gold"), remote: true, disable_with: "Redirecting..." %></td>
<td><%= button_to "Get #{Danbooru.config.canonical_app_name} Platinum", user_upgrades_path(user_id: @recipient.id, upgrade_type: "platinum"), remote: true, disable_with: "Redirecting..." %></td>
<td><%= 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..." %></td>
<td><%= 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..." %></td>
<% elsif @recipient.level == User::Levels::GOLD %>
<td></td>
<td><%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %></td>
<td><%= 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..." %></td>
<td><%= 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..." %></td>
<% else %>
<td></td>
<td><%= button_to "Get #{Danbooru.config.canonical_app_name} Gold", nil, disabled: true %></td>
@@ -150,7 +150,10 @@
<summary>What payment methods do you support?</summary>
<p>We support all major credit and debit cards, including international
cards. Payments are securely handled by <a href="https://www.stripe.com">Stripe</a>.
cards. We also support bank payments in several European countries,
including Austria, Belgium, Germany, the Netherlands, and Poland.</p>
<p>Payments are securely handled by <a href="https://www.stripe.com">Stripe</a>.
We don't support PayPal or Bitcoin at this time.</p>
</details>

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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!