Block repeated failed attempts to verify cards

Stephen Paul Weber created

Declined verifications are ultimately a kind of declined transaction, and still
reflect poorly on us.

Change summary

config.ru               | 55 ++++++++++++++++++++++++++++++++++--------
views/credit_cards.slim | 10 +++++++
2 files changed, 54 insertions(+), 11 deletions(-)

Detailed changes

config.ru 🔗

@@ -48,9 +48,10 @@ class CreditCardGateway
 		end
 	end
 
-	def initialize(jid, customer_id=nil)
+	def initialize(jid, customer_id, antifraud)
 		@jid = jid
 		@customer_id = customer_id
+		@antifraud = antifraud
 
 		@gateway = Braintree::Gateway.new(
 			environment: BRAINTREE_CONFIG[:environment].to_s,
@@ -115,18 +116,32 @@ class CreditCardGateway
 		!@gateway.customer.find(customer_id).payment_methods.empty?
 	end
 
+	def antifraud
+		REDIS.mget(@antifraud.map { |k| "jmp_antifraud-#{k}" }).find do |anti|
+			anti.to_i > 2
+		end &&
+			Braintree::ErrorResult.new(
+				@gateway, errors: {}, message: "Please contact support"
+			)
+	end
+
+	def incr_antifraud!
+		@antifraud.each do |k|
+			REDIS.incr("jmp_antifraud-#{k}")
+			REDIS.expire("jmp_antifraud-#{k}", 60 * 60 * 24)
+		end
+	end
+
 	def default_method(nonce)
-		result = @gateway.payment_method.create(
-			customer_id: customer_id,
-			payment_method_nonce: nonce,
-			options: {
-				verify_card: true,
-				make_default: true
-			}
+		result = antifraud || @gateway.payment_method.create(
+			customer_id: customer_id, payment_method_nonce: nonce,
+			options: { verify_card: true, make_default: true }
 		)
-		raise ErrorResult.for(result) unless result.success?
 
-		result
+		return result if result.success?
+
+		incr_antifraud!
+		raise ErrorResult.for(result)
 	end
 
 	def remove_method(token)
@@ -192,6 +207,7 @@ end
 class JmpPay < Roda
 	SENTRY_DSN = ENV["SENTRY_DSN"] && URI(ENV["SENTRY_DSN"])
 	plugin :render, engine: "slim"
+	plugin :cookies, path: "/"
 	plugin :common_logger, $stdout
 
 	extend Forwardable
@@ -230,16 +246,33 @@ class JmpPay < Roda
 		r.on :jid do |jid|
 			Sentry.set_user(id: params["customer_id"], jid: jid)
 
-			gateway = CreditCardGateway.new(jid, params["customer_id"])
+			atfd = r.cookies["atfd"] || SecureRandom.uuid
+			one_year = 60 * 60 * 24 * 365
+			response.set_cookie(
+				"atfd",
+				value: atfd, expires: Time.now + one_year
+			)
+			params.delete("atfd") if params["atfd"].to_s == ""
+			antifrauds = [atfd, r.ip, params["atfd"]].compact.uniq
+			customer_id = params["customer_id"]
+			gateway = CreditCardGateway.new(jid, customer_id, antifrauds)
 			topup = AutoTopUpRepo.new
 
 			r.on "credit_cards" do
 				r.get do
+					if gateway.antifraud
+						return view(
+							:message,
+							locals: { message: "Please contact support" }
+						)
+					end
+
 					view(
 						"credit_cards",
 						locals: {
 							token: gateway.client_token,
 							customer_id: gateway.customer_id,
+							antifraud: atfd,
 							auto_top_up: topup.find(gateway.customer_id) ||
 							             (gateway.payment_methods? ? "" : "15")
 						}

views/credit_cards.slim 🔗

@@ -32,12 +32,22 @@ form method="post" action=""
 			small Leave blank for no auto top-up.
 
 	input type="hidden" name="customer_id" value=customer_id
+	input type="hidden" name="atfd" value=antifraud
 	input type="hidden" name="braintree_nonce"
 
 script src="https://js.braintreegateway.com/web/dropin/1.33.0/js/dropin.js"
 javascript:
 	document.querySelector("#braintree").innerHTML = "";
 
+	if(window.localStorage) {
+		var atfd = localStorage.getItem("atfd");
+		if(!atfd) {
+			atfd = "#{antifraud}";
+			localStorage.setItem("atfd", atfd);
+		}
+		document.querySelector("input[name=atfd]").value = atfd;
+	}
+
 	var button = document.createElement("button");
 	button.innerHTML = "Save";
 	document.querySelector("form").appendChild(button);