From fee70ea92607ad40580d6ee9cbe1b437adff637d Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Mar 2023 15:09:23 -0500 Subject: [PATCH 1/4] Factor out a Customer object --- config.ru | 26 +++----------------------- lib/customer.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 lib/customer.rb diff --git a/config.ru b/config.ru index 7e6b90595d0418389f718431e0efec89297483d3..f2cc88287bfb7cefd0486356e4ac96cbc49a0f48 100644 --- a/config.ru +++ b/config.ru @@ -16,6 +16,7 @@ if ENV["RACK_ENV"] == "development" end require_relative "lib/auto_top_up_repo" +require_relative "lib/customer" require_relative "lib/three_d_secure_repo" require_relative "lib/electrum" @@ -70,25 +71,14 @@ class CreditCardGateway end def customer_id - customer_id = REDIS.get(redis_key_jid) + customer_id = Customer.new(nil, @jid).customer_id return customer_id if check_customer_id(customer_id) result = @gateway.customer.create raise "Braintree customer create failed" unless result.success? @customer_id = result.customer.id - save_customer_id! - end - - def save_customer_id! - unless REDIS.set(redis_key_jid, @customer_id) == "OK" - raise "Saving new jid,customer to redis failed" - end - - unless REDIS.set(redis_key_customer_id, @jid) == "OK" - raise "Saving new customer,jid to redis failed" - end - + Customer.new(@customer_id, @jid).save! @customer_id end @@ -157,16 +147,6 @@ class CreditCardGateway def remove_method(token) @gateway.payment_method.delete(token) end - -protected - - def redis_key_jid - "jmp_customer_id-#{@jid}" - end - - def redis_key_customer_id - "jmp_customer_jid-#{@customer_id}" - end end class UnknownTransactions diff --git a/lib/customer.rb b/lib/customer.rb new file mode 100644 index 0000000000000000000000000000000000000000..01843622f2408bca14a07a90fc45b4eca2eeb388 --- /dev/null +++ b/lib/customer.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Customer + def initialize(customer_id, jid) + @customer_id = customer_id + @jid = jid + end + + def customer_id + @customer_id = REDIS.get(redis_key_jid) + end + + def save! + unless REDIS.set(redis_key_jid, @customer_id) == "OK" + raise "Saving new jid,customer to redis failed" + end + + return if REDIS.set(redis_key_customer_id, @jid) == "OK" + + raise "Saving new customer,jid to redis failed" + end + +protected + + def redis_key_jid + "jmp_customer_id-#{@jid}" + end + + def redis_key_customer_id + "jmp_customer_jid-#{@customer_id}" + end +end From dd0f2f2d6221e6bab0aa2a4f44dced8fc1b86c86 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Mar 2023 15:09:48 -0500 Subject: [PATCH 2/4] Factor out a transaction object --- bin/process_pending_btc_transactions | 21 +++------------ lib/transaction.rb | 40 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 lib/transaction.rb diff --git a/bin/process_pending_btc_transactions b/bin/process_pending_btc_transactions index 30a452543cbf1bbba91f52c1a40544e5c9d89550..2492cbc5d84a59dc3a1a310a3bb94a2534b2f834 100755 --- a/bin/process_pending_btc_transactions +++ b/bin/process_pending_btc_transactions @@ -25,6 +25,7 @@ require "redis" require_relative "../lib/blather_notify" require_relative "../lib/electrum" +require_relative "../lib/transaction" CONFIG = Dhall::Coder @@ -98,19 +99,6 @@ class Plan @plan[:currency] end - def bonus_for(fiat_amount) - return BigDecimal(0) if fiat_amount <= 15 - - fiat_amount * case fiat_amount - when (15..29.99) - 0.01 - when (30..139.99) - 0.03 - else - 0.05 - end - end - def price BigDecimal(@plan[:monthly_price].to_i) * 0.0001 end @@ -184,11 +172,10 @@ class Customer end def add_btc_credit(txid, btc_amount, fiat_amount) - return unless add_transaction(txid, fiat_amount, "Bitcoin payment") + tx = Transaction.new(txid, fiat_amount, "Bitcoin payment") + return unless tx.save - if (bonus = plan.bonus_for(fiat_amount)).positive? - add_transaction("bonus_for_#{txid}", bonus, "Bitcoin payment bonus") - end + tx.bonus&.save notify_btc_credit(txid, btc_amount, fiat_amount, bonus) end diff --git a/lib/transaction.rb b/lib/transaction.rb new file mode 100644 index 0000000000000000000000000000000000000000..4dc6c389222e4b26ca1236eb2bf1fdf3256d1e5e --- /dev/null +++ b/lib/transaction.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +class Transaction + def initialize(customer_id, id, amount, note) + @customer_id = customer_id + @id = id + @amount = amount + @note = note + end + + def bonus + return unless bonus_amount.positive? + + new(@customer_id, "bonus_for_#{@id}", bonus_amount, "#{@note} bonus") + end + + def bonus_amount + return BigDecimal(0) if @amount <= 15 + + @amount * case @amount + when (15..29.99) + 0.01 + when (30..139.99) + 0.03 + else + 0.05 + end + end + + def save + args = [@customer_id, @id, @amount, @note] + DB.exec_params(<<-SQL, args).cmd_tuples.positive? + INSERT INTO transactions + (customer_id, transaction_id, settled_after, amount, note) + VALUES + ($1, $2, LOCALTIMESTAMP, $3, $4) + ON CONFLICT (transaction_id) DO NOTHING + SQL + end +end From 61c7c01600df15ca4e67e44f7e919e37dd619a26 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Mar 2023 15:10:55 -0500 Subject: [PATCH 3/4] Factor out with_antifraud helper --- config.ru | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/config.ru b/config.ru index f2cc88287bfb7cefd0486356e4ac96cbc49a0f48..e7d18cacff7120239c99a7fbf86427cdc5dd1246 100644 --- a/config.ru +++ b/config.ru @@ -117,31 +117,29 @@ class CreditCardGateway ) end - def incr_antifraud! + def with_antifraud + result = antifraud || yield + return result if result.success? + @antifraud.each do |k| REDIS.incr("jmp_antifraud-#{k}") REDIS.expire("jmp_antifraud-#{k}", 60 * 60 * 24) end - end - def payment_method_create_options - options = { verify_card: true, make_default: true } - if merchant_account - options[:verification_merchant_account_id] = merchant_account.to_s - end - options + raise ErrorResult.for(result) end - def default_method(nonce) - result = antifraud || @gateway.payment_method.create( - customer_id: customer_id, payment_method_nonce: nonce, - options: payment_method_create_options - ) - return result if result.success? - - incr_antifraud! - raise ErrorResult.for(result) + def default_method(nonce) + with_antifraud do + @gateway.payment_method.create( + customer_id: customer_id, payment_method_nonce: nonce, + options: { + verify_card: true, make_default: true, + verification_merchant_account_id: merchant_account.to_s + } + ) + end end def remove_method(token) From e1cf684fe34b386a6c358f9163e0a2e45a796757 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Mar 2023 15:11:20 -0500 Subject: [PATCH 4/4] Allow fully 3DS'd transaction from the web --- config.ru | 82 +++++++++++++++++++++++++++++++++----- lib/three_d_secure_repo.rb | 11 +++-- views/credit_cards.slim | 8 +++- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/config.ru b/config.ru index e7d18cacff7120239c99a7fbf86427cdc5dd1246..103d0c9826cec0aa307f84b0147aed29a69d2f67 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,7 @@ # frozen_string_literal: true require "braintree" +require "bigdecimal/util" require "date" require "delegate" require "dhall" @@ -19,6 +20,7 @@ require_relative "lib/auto_top_up_repo" require_relative "lib/customer" require_relative "lib/three_d_secure_repo" require_relative "lib/electrum" +require_relative "lib/transaction" require "sentry-ruby" Sentry.init do |config| @@ -129,6 +131,17 @@ class CreditCardGateway raise ErrorResult.for(result) end + def sale(nonce, amount) + with_antifraud do + @gateway.transaction.sale( + customer_id: customer_id, payment_method_nonce: nonce, + amount: amount, merchant_account_id: merchant_account.to_s, + options: { + store_in_vault_on_success: true, submit_for_settlement: true + } + ) + end + end def default_method(nonce) with_antifraud do @@ -192,6 +205,61 @@ class UnknownTransactions end end +class CardVault + def self.for(gateway, nonce, amount=nil) + if amount&.positive? + CardDeposit.new(gateway, nonce, amount) + else + new(gateway, nonce) + end + end + + def initialize(gateway, nonce) + @gateway = gateway + @nonce = nonce + end + + def call(auto_top_up_amount) + result = vault! + ThreeDSecureRepo.new.put_from_result(result) + AutoTopUpRepo.new.put( + @gateway.customer_id, + auto_top_up_amount + ) + result + end + + def vault! + @gateway.default_method(@nonce) + end + + class CardDeposit < self + def initialize(gateway, nonce, amount) + super(gateway, nonce) + @amount = amount + + return unless @amount < 15 || @amount > 35 + + raise CreditCardGateway::ErrorResult, "amount too low or too high" + end + + def call(*) + result = super + Transaction.new( + @gateway.customer_id, + result.transaction.id, + @amount, + "Credit card payment" + ).save + result + end + + def vault! + @gateway.sale(@nonce, @amount) + end + end +end + class JmpPay < Roda SENTRY_DSN = ENV["SENTRY_DSN"] && URI(ENV["SENTRY_DSN"]) plugin :render, engine: "slim" @@ -268,15 +336,11 @@ class JmpPay < Roda end r.post do - result = gateway.default_method(params["braintree_nonce"]) - ThreeDSecureRepo.new.put_from_payment_method( - gateway.customer_id, - result.payment_method - ) - topup.put( - gateway.customer_id, - params["auto_top_up_amount"].to_i - ) + CardVault + .for( + gateway, params["braintree_nonce"], + params["amount"].to_d + ).call(params["auto_top_up_amount"].to_i) "OK" rescue ThreeDSecureRepo::Failed gateway.remove_method($!.message) diff --git a/lib/three_d_secure_repo.rb b/lib/three_d_secure_repo.rb index c71ebecb990ca3879fe5ab09f9569e14a0a135ba..7e7a41528dfc9132afeed49fab5a103590405b41 100644 --- a/lib/three_d_secure_repo.rb +++ b/lib/three_d_secure_repo.rb @@ -3,10 +3,15 @@ class ThreeDSecureRepo class Failed < StandardError; end - def put_from_payment_method(_customer_id, method) - return unless method.verification # Already vaulted + def put_from_result(result) + three_d = if result.payment_method + return unless result.payment_method.verification # Already vaulted + + result.payment_method.verification.three_d_secure_info + else + result.transaction.three_d_secure_info + end - three_d = method.verification.three_d_secure_info if !three_d || (three_d.liability_shift_possible && !three_d.liability_shifted) raise Failed, method.token diff --git a/views/credit_cards.slim b/views/credit_cards.slim index ec053e83465d186efc85bea3e0dbe86fcd40fe5b..f6b77923484d35d7cbce63371df929965c125230 100644 --- a/views/credit_cards.slim +++ b/views/credit_cards.slim @@ -24,11 +24,15 @@ form method="post" action="" #braintree | Unfortunately, our credit card processor requires JavaScript. + label#amount style="#{'display:none;' unless params['amount']}" + div Amount of initial deposit (minimum $15) + input type="number" name="amount" min="15" value="#{params.fetch('amount', '')}" + fieldset legend Auto top-up when account balance is low? label | When balance drops below $5, add $ - input type="number" name="auto_top_up_amount" min="15" value=auto_top_up + input type="number" name="auto_top_up_amount" min="15" max="35" value=auto_top_up small Leave blank for no auto top-up. input type="hidden" name="customer_id" value=customer_id @@ -93,7 +97,7 @@ javascript: instance.requestPaymentMethod({ threeDSecure: { - amount: "0.0", + amount: document.querySelector("input[name=amount]").value || "0.0", requireChallenge: true } }, function(err, payload) {