Files
danbooru/app/models/user_upgrade.rb
evazion d0bb4ed398 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.
2020-12-31 06:50:10 -06:00

323 lines
8.1 KiB
Ruby

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,
refunded: 30,
}
scope :gifted, -> { where("recipient_id != purchaser_id") }
scope :self_upgrade, -> { where("recipient_id = purchaser_id") }
def self.enabled?
stripe_secret_key.present? && stripe_publishable_key.present? && stripe_webhook_secret.present?
end
def self.stripe_secret_key
Danbooru.config.stripe_secret_key
end
def self.stripe_publishable_key
Danbooru.config.stripe_publishable_key
end
def self.stripe_webhook_secret
Danbooru.config.stripe_webhook_secret
end
def self.gold_price
if Danbooru.config.is_promotion?
1500
else
2000
end
end
def self.platinum_price
2 * gold_price
end
def self.gold_to_platinum_price
platinum_price - gold_price
end
def level
case upgrade_type
when "gold"
User::Levels::GOLD
when "platinum"
User::Levels::PLATINUM
when "gold_to_platinum"
User::Levels::PLATINUM
else
raise NotImplementedError
end
end
def previous_level
case upgrade_type
when "gold"
User::Levels::MEMBER
when "platinum"
User::Levels::MEMBER
when "gold_to_platinum"
User::Levels::GOLD
else
raise NotImplementedError
end
end
def level_string
User.level_string(level)
end
def is_gift?
recipient != purchaser
end
def self.visible(user)
if user.is_owner?
all
else
where(recipient: user).or(where(purchaser: user))
end
end
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :upgrade_type, :status, :stripe_id, :recipient, :purchaser)
if params[:is_gifted].to_s.truthy?
q = q.gifted
elsif params[:is_gifted].to_s.falsy?
q = q.self_upgrade
end
q = q.apply_default_order(params)
q
end
concerning :UpgradeMethods do
def process_upgrade!(payment_status)
recipient.with_lock do
return unless pending? || processing?
if payment_status == "paid"
upgrade_recipient!
create_mod_action!
dmail_recipient!
dmail_purchaser!
update!(status: :complete)
else
update!(status: :processing)
end
end
end
def upgrade_recipient!
recipient.update!(level: level, inviter: User.system)
end
def create_mod_action!
ModAction.log(%{"#{recipient.name}":#{Routes.user_path(recipient)} level changed #{User.level_string(recipient.level_before_last_save)} -> #{recipient.level_string}}, :user_account_upgrade, purchaser)
end
def dmail_recipient!
if is_gift?
body = "Congratulations, your account has been upgraded to #{level_string} by <@#{purchaser.name}>. Enjoy!"
else
body = "You are now a #{level_string} user. Thanks for supporting #{Danbooru.config.canonical_app_name}!"
end
title = "You have been upgraded to #{level_string}!"
Dmail.create_automated(to: recipient, title: title, body: body)
end
def dmail_purchaser!
return unless is_gift?
title = "#{recipient.name} has been upgraded to #{level_string}!"
body = "<@#{recipient.name}> is now a #{level_string} user. Thanks for supporting #{Danbooru.config.canonical_app_name}!"
Dmail.create_automated(to: purchaser, title: title, body: body)
end
end
concerning :StripeMethods do
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: methods,
line_items: [{
price: price_id,
quantity: 1,
}],
discounts: [{
coupon: promotion_discount_id,
}],
metadata: {
user_upgrade_id: id,
purchaser_id: purchaser.id,
recipient_id: recipient.id,
purchaser_name: purchaser.name,
recipient_name: recipient.name,
upgrade_type: upgrade_type,
country: country,
is_gift: is_gift?,
level: level,
},
)
update!(stripe_id: checkout.id)
checkout
end
def refund!(reason: nil)
with_lock do
return if refunded?
Stripe::Refund.create(payment_intent: payment_intent.id, reason: reason)
recipient.update!(level: previous_level)
update!(status: "refunded")
end
end
def receipt_url
return nil if pending? || stripe_id.nil?
charge.receipt_url
end
def payment_url
return nil if pending? || stripe_id.nil?
"https://dashboard.stripe.com/payments/#{payment_intent.id}"
end
def checkout_session
@checkout_session ||= Stripe::Checkout::Session.retrieve(stripe_id)
end
def payment_intent
@payment_intent ||= Stripe::PaymentIntent.retrieve(checkout_session.payment_intent)
end
def charge
payment_intent.charges.data.first
end
def has_receipt?
!pending?
end
def has_payment?
!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({
url: Routes.webhook_user_upgrade_url(source: "stripe"),
enabled_events: [
"payment_intent.created",
"payment_intent.payment_failed",
"checkout.session.completed",
],
})
webhook.secret
end
def receive_webhook(request)
event = build_event(request)
if event.type == "checkout.session.completed"
checkout_session_completed(event.data.object)
end
end
def build_event(request)
payload = request.body.read
signature = request.headers["Stripe-Signature"]
Stripe::Webhook.construct_event(payload, signature, stripe_webhook_secret)
end
def checkout_session_completed(checkout)
user_upgrade = UserUpgrade.find(checkout.metadata.user_upgrade_id)
user_upgrade.process_upgrade!(checkout.payment_status)
end
end
end
end