From 37cb77a406fb7041dab589d37b6703f4ecb7337a Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Thu, 8 Sep 2022 21:04:54 -0500 Subject: [PATCH 1/4] Show decline error text in more cases --- views/credit_cards.slim | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/views/credit_cards.slim b/views/credit_cards.slim index 05ca9e6ad48f222334da2be17374b9a4e7979627..650c71bf98383009da660489e89d1ff55514a963 100644 --- a/views/credit_cards.slim +++ b/views/credit_cards.slim @@ -82,12 +82,18 @@ 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(); From 2412b1e44ea8f89ee8d789a92936e517eddfdd15 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Thu, 8 Sep 2022 21:05:29 -0500 Subject: [PATCH 2/4] Capture exceptional cases to Sentry --- views/credit_cards.slim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/views/credit_cards.slim b/views/credit_cards.slim index 650c71bf98383009da660489e89d1ff55514a963..2e898164fe25d5d68f00e0e18700424a32fc1a32 100644 --- a/views/credit_cards.slim +++ b/views/credit_cards.slim @@ -53,7 +53,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(); @@ -99,6 +102,7 @@ javascript: instance._mainView.hideLoadingIndicator(); instance.clearSelectedPaymentMethod(); instance._mainView.showSheetError(); + Sentry.captureException(err); }); } }); From 85ad44c77b837b48a582092f04b74ac568b5a638 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Thu, 8 Sep 2022 21:43:18 -0500 Subject: [PATCH 3/4] Block repeated failed attempts to verify cards Declined verifications are ultimately a kind of declined transaction, and still reflect poorly on us. --- config.ru | 55 ++++++++++++++++++++++++++++++++--------- views/credit_cards.slim | 10 ++++++++ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/config.ru b/config.ru index 6e2cbb21c0de6f6e8dee016f88dc84feba281cd5..dee3006bde3a41bf9828b2ad7fb9d45bdb13ae5d 100644 --- a/config.ru +++ b/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") } diff --git a/views/credit_cards.slim b/views/credit_cards.slim index 2e898164fe25d5d68f00e0e18700424a32fc1a32..5856202e5210c10f64f2df02c5e116998270810f 100644 --- a/views/credit_cards.slim +++ b/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); From 061d465c6d67222e41c168d75e144e13aa071494 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 19 Sep 2022 12:42:37 -0500 Subject: [PATCH 4/4] Allow bypassing antifraud for a customer Support will need this --- config.ru | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config.ru b/config.ru index dee3006bde3a41bf9828b2ad7fb9d45bdb13ae5d..2fdb5e25c15fd44f621d1cc098eed14979d52641 100644 --- a/config.ru +++ b/config.ru @@ -117,6 +117,8 @@ class CreditCardGateway 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 &&