~singpolyma/jmp-pay

28bbd70196938aceff56f8354624b2346c68c18b — Stephen Paul Weber 5 months ago 02f0825 + 31e7851
Merge branch 'three_d'

* three_d:
  3D Secure Valuting
3 files changed, 100 insertions(+), 10 deletions(-)

M config.ru
A lib/three_d_secure_repo.rb
M views/credit_cards.slim
M config.ru => config.ru +34 -3
@@ 16,6 16,7 @@ if ENV["RACK_ENV"] == "development"
end

require_relative "lib/auto_top_up_repo"
require_relative "lib/three_d_secure_repo"
require_relative "lib/electrum"

require "sentry-ruby"


@@ 35,6 36,17 @@ DB.type_map_for_results = PG::BasicTypeMapForResults.new(DB)
DB.type_map_for_queries = PG::BasicTypeMapForQueries.new(DB)

class CreditCardGateway
	class ErrorResult < StandardError
		def self.for(result)
			if result.verification&.status == "gateway_rejected" &&
			   result.verification&.gateway_rejection_reason == "cvv"
				new("fieldInvalidForCvv")
			else
				new(result.message)
			end
		end
	end

	def initialize(jid, customer_id=nil)
		@jid = jid
		@customer_id = customer_id


@@ 86,14 98,22 @@ class CreditCardGateway
		!@gateway.customer.find(customer_id).payment_methods.empty?
	end

	def default_payment_method=(nonce)
		@gateway.payment_method.create(
	def default_method(nonce)
		result = @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
	end

	def remove_method(token)
		@gateway.payment_method.delete(token)
	end

protected


@@ 210,12 230,23 @@ class JmpPay < Roda
				end

				r.post do
					gateway.default_payment_method = params["braintree_nonce"]
					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
					)
					"OK"
				rescue ThreeDSecureRepo::Failed
					gateway.remove_method($!.message)
					response.status = 400
					"hostedFieldsFieldsInvalidError"
				rescue CreditCardGateway::ErrorResult
					response.status = 400
					$!.message
				end
			end
		end

A lib/three_d_secure_repo.rb => lib/three_d_secure_repo.rb +46 -0
@@ 0,0 1,46 @@
# frozen_string_literal: true

class ThreeDSecureRepo
	class Failed < StandardError; end

	def initialize(redis: REDIS)
		@redis = redis
	end

	def find(customer_id, token)
		redis(:hget, customer_id, token)
	end

	def put(customer_id, token, authid)
		if !authid || authid.empty?
			redis(:hdel, customer_id, token)
		else
			redis(:hset, customer_id, token, authid)
		end
	end

	def put_from_payment_method(customer_id, method)
		return unless method.verification # Already vaulted

		three_d = method.verification.three_d_secure_info
		if !three_d ||
		   (three_d.liability_shift_possible && !three_d.liability_shifted)
			raise Failed, method.token
		end

		put(
			customer_id, method.token,
			three_d.three_d_secure_authentication_id
		)
	end

protected

	def redis(action, customer_id, *args)
		@redis.public_send(
			action,
			"jmp_customer_three_d_secure_authentication_id-#{customer_id}",
			*args
		)
	end
end

M views/credit_cards.slim => views/credit_cards.slim +20 -7
@@ 34,7 34,7 @@ form method="post" action=""
	input type="hidden" name="customer_id" value=customer_id
	input type="hidden" name="braintree_nonce"

script src="https://js.braintreegateway.com/web/dropin/1.26.0/js/dropin.min.js"
script src="https://js.braintreegateway.com/web/dropin/1.33.0/js/dropin.js"
javascript:
	document.querySelector("#braintree").innerHTML = "";



@@ 44,7 44,9 @@ javascript:
	braintree.dropin.create({
		authorization: #{{token.to_json}},
		container: "#braintree",
		card: { vault: { vaultCard: false } },
		vaultManager: true,
		threeDSecure: true,
		translations: {
			payWithCard: "Add a Card",
			payingWith: "Default payment source",


@@ 56,14 58,17 @@ javascript:
		document.querySelector("form").addEventListener("submit", function(e) {
			e.preventDefault();
			instance._mainView.hideSheetError();
			instance._mainView.showLoadingIndicator();

			instance.requestPaymentMethod(function(err, payload) {
			instance.requestPaymentMethod({
				threeDSecure: {
					amount: "0.0",
					requireChallenge: true
				}
			}, function(err, payload) {
				if(err) {
					console.log(err);
					instance._mainView.hideLoadingIndicator();
					instance._mainView.showSheetError();
				} else {
					instance._mainView.showLoadingIndicator();
					e.target.braintree_nonce.value = payload.nonce;
					fetch("", {
						"method": "POST",


@@ 76,8 81,16 @@ javascript:
						}
					}).catch(function(err) {
							console.log(err);
							instance._mainView.hideLoadingIndicator();
							instance._mainView.showSheetError();
							err.text().then(function(msg) {
								instance._mainView.hideLoadingIndicator();
								instance.clearSelectedPaymentMethod();
								instance._mainView.showSheetError(msg);
							});
					}).catch(function(err) {
						console.log(err);
						instance._mainView.hideLoadingIndicator();
						instance.clearSelectedPaymentMethod();
						instance._mainView.showSheetError();
					});
				}
			});