Allow fully 3DS'd transaction from the web

Stephen Paul Weber created

Change summary

config.ru                  | 82 +++++++++++++++++++++++++++++++++++----
lib/three_d_secure_repo.rb | 11 +++-
views/credit_cards.slim    |  8 ++
3 files changed, 87 insertions(+), 14 deletions(-)

Detailed changes

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)

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

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