~singpolyma/jmp-pay

fe075a836ae5c8007fcdb987bc6e6ba25cc9142c — Stephen Paul Weber 6 days ago f3a2a3b + 061d465
Merge branch 'card-form-improvements'

* card-form-improvements:
  Allow bypassing antifraud for a customer
  Block repeated failed attempts to verify cards
  Capture exceptional cases to Sentry
  Show decline error text in more cases
2 files changed, 73 insertions(+), 18 deletions(-)

M config.ru
M views/credit_cards.slim
M config.ru => config.ru +46 -11
@@ 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,34 @@ class CreditCardGateway
		!@gateway.customer.find(customer_id).payment_methods.empty?
	end

	def antifraud
		return if REDIS.exists?("jmp_antifraud_bypass-#{customer_id}")

		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 209,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 248,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")
						}

M views/credit_cards.slim => views/credit_cards.slim +27 -7
@@ 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);


@@ 53,7 63,10 @@ javascript:
			chooseAnotherWayToPay: "Add a different payment source"
		}
	}, function (createErr, instance) {
		if(createErr) console.log(createErr);
		if(createErr) {
			console.log(createErr);
			Sentry.captureException(createErr);
		}

		document.querySelector("form").addEventListener("submit", function(e) {
			e.preventDefault();


@@ 82,17 95,24 @@ javascript:
							return Promise.reject(response);
						}
					}).catch(function(err) {
							console.log(err);
							err.text().then(function(msg) {
								instance._mainView.hideLoadingIndicator();
								instance.clearSelectedPaymentMethod();
								instance._mainView.showSheetError(msg);
							});
						if(!(err instanceof Response)) return Promise.reject(err);

						return err.text().then(function(msg) {
							console.log(msg);
							instance._mainView.hideLoadingIndicator();
							instance.clearSelectedPaymentMethod();
							instance._mainView.showSheetError(msg);
							var errEl = instance._mainView.sheetErrorText;
							if(errEl.innerHTML === instance._mainView.strings.genericError) {
								errEl.innerHTML = "Card Issuer Says: " + msg;
							}
						});
					}).catch(function(err) {
						console.log(err);
						instance._mainView.hideLoadingIndicator();
						instance.clearSelectedPaymentMethod();
						instance._mainView.showSheetError();
						Sentry.captureException(err);
					});
				}
			});