From e1cf684fe34b386a6c358f9163e0a2e45a796757 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 6 Mar 2023 15:11:20 -0500 Subject: [PATCH] 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) {