~singpolyma/sgx-jmp

c69a6218704ba1d09a24425d3ee43c47c2ba7b5d — Stephen Paul Weber 1 year, 4 months ago 221f4dc + 4f0083d
Merge branch 'new-signup-credit-card-decline'

* new-signup-credit-card-decline:
  Block repeated declines for 24 hours
  Handle credit card decline
M lib/registration.rb => lib/registration.rb +45 -4
@@ 219,15 219,56 @@ class Registration

				def write
					Transaction.sale(
						@customer.merchant_account,
						@payment_method,
						CONFIG[:activation_amount]
					).then(&:insert).then {
						@customer,
						CONFIG[:activation_amount],
						@payment_method
					).then(
						method(:sold),
						->(_) { declined }
					)
				end

			protected

				def sold(tx)
					tx.insert.then {
						@customer.bill_plan
					}.then do
						Finish.new(@iq, @customer, @tel).write
					end
				end

				DECLINE_MESSAGE =
					"Your bank declined the transaction. " \
					"Often this happens when a person's credit card " \
					"is a US card that does not support international " \
					"transactions, as JMP is not based in the USA, though " \
					"we do support transactions in USD.\n\n" \
					"If you were trying a prepaid card, you may wish to use "\
					"Privacy.com instead, as they do support international " \
					"transactions.\n\n " \
					"You may add another card and then choose next"

				def decline_oob(reply)
					oob = OOB.find_or_create(reply.command)
					oob.url = CONFIG[:credit_card_url].call(
						reply.to.stripped.to_s,
						@customer.customer_id
					)
					oob.desc = DECLINE_MESSAGE
					oob
				end

				def declined
					reply = @iq.reply
					reply_oob = decline_oob(reply)
					reply.allowed_actions = [:next]
					reply.note_type = :error
					reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}"
					COMMAND_MANAGER.write(reply).then do |riq|
						CreditCard.for(riq, @customer, @tel)
					end
				end
			end
		end
	end

M lib/transaction.rb => lib/transaction.rb +33 -9
@@ 1,18 1,42 @@
# frozen_string_literal: true

class Transaction
	def self.sale(merchant_account, payment_method, amount)
		BRAINTREE.transaction.sale(
			amount: amount,
			payment_method_token: payment_method.token,
			merchant_account_id: merchant_account,
			options: { submit_for_settlement: true }
		).then do |response|
			raise response.message unless response.success?
			new(response.transaction)
	def self.sale(customer, amount, payment_method=nil)
		REDIS.get("jmp_pay_decline-#{customer.customer_id}").then do |declines|
			raise "too many declines" if declines.to_i >= 2

			BRAINTREE.transaction.sale(
				amount: amount,
				**sale_args_for(customer, payment_method)
			).then do |response|
				decline_guard(customer, response)
				new(response.transaction)
			end
		end
	end

	def self.decline_guard(customer, response)
		return if response.success?

		REDIS.incr("jmp_pay_decline-#{customer.customer_id}").then do
			REDIS.expire("jmp_pay_decline-#{customer.customer_id}", 60 * 60 * 24)
		end
		raise response.message
	end

	def self.sale_args_for(customer, payment_method=nil)
		{
			merchant_account_id: customer.merchant_account,
			options: { submit_for_settlement: true }
		}.merge(
			if payment_method
				{ payment_method_token: payment_method.token }
			else
				{ customer_id: customer.id }
			end
		)
	end

	attr_reader :amount

	def initialize(braintree_transaction)

M sgx_jmp.rb => sgx_jmp.rb +3 -3
@@ 205,17 205,17 @@ command :execute?, node: "buy-credit", sessionid: nil do |iq|
		BuyAccountCreditForm.new(customer).add_to_form(reply.form).then { customer }
	}.then { |customer|
		EMPromise.all([
			customer,
			customer.payment_methods,
			customer.merchant_account,
			COMMAND_MANAGER.write(reply)
		])
	}.then { |(payment_methods, merchant_account, iq2)|
	}.then { |(customer, payment_methods, iq2)|
		iq = iq2 # This allows the catch to use it also
		payment_method = payment_methods.fetch(
			iq.form.field("payment_method")&.value.to_i
		)
		amount = iq.form.field("amount").value.to_s
		Transaction.sale(merchant_account, payment_method, amount)
		Transaction.sale(customer, amount, payment_method)
	}.then { |transaction|
		transaction.insert.then { transaction.amount }
	}.then { |amount|

M test/test_helper.rb => test/test_helper.rb +6 -2
@@ 81,8 81,12 @@ class Matching
end

class PromiseMock < Minitest::Mock
	def then
		yield self
	def then(succ=nil, _=nil)
		if succ
			succ.call(self)
		else
			yield self
		end
	end
end


M test/test_registration.rb => test/test_registration.rb +48 -6
@@ 205,6 205,8 @@ class RegistrationTest < Minitest::Test
				Minitest::Mock.new
			Registration::Payment::CreditCard::Activate::Transaction =
				Minitest::Mock.new
			Registration::Payment::CreditCard::Activate::COMMAND_MANAGER =
				Minitest::Mock.new

			def test_write
				transaction = PromiseMock.new


@@ 212,19 214,19 @@ class RegistrationTest < Minitest::Test
					:insert,
					EMPromise.resolve(nil)
				)
				customer = Minitest::Mock.new(
					Customer.new("test", plan_name: "test_usd")
				)
				Registration::Payment::CreditCard::Activate::Transaction.expect(
					:sale,
					transaction,
					[
						"merchant_usd",
						:test_default_method,
						CONFIG[:activation_amount]
						customer,
						CONFIG[:activation_amount],
						:test_default_method
					]
				)
				iq = Blather::Stanza::Iq::Command.new
				customer = Minitest::Mock.new(
					Customer.new("test", plan_name: "test_usd")
				)
				customer.expect(:bill_plan, nil)
				Registration::Payment::CreditCard::Activate::Finish.expect(
					:new,


@@ 240,8 242,48 @@ class RegistrationTest < Minitest::Test
				Registration::Payment::CreditCard::Activate::Transaction.verify
				transaction.verify
				customer.verify
				Registration::Payment::CreditCard::Activate::Finish.verify
			end
			em :test_write

			def test_write_declines
				customer = Minitest::Mock.new(
					Customer.new("test", plan_name: "test_usd")
				)
				Registration::Payment::CreditCard::Activate::Transaction.expect(
					:sale,
					EMPromise.reject("declined"),
					[
						customer,
						CONFIG[:activation_amount],
						:test_default_method
					]
				)
				iq = Blather::Stanza::Iq::Command.new
				iq.from = "test@example.com"
				result = Minitest::Mock.new
				result.expect(:then, nil)
				Registration::Payment::CreditCard::Activate::COMMAND_MANAGER.expect(
					:write,
					result,
					[Matching.new do |reply|
						assert_equal :error, reply.note_type
						assert_equal(
							Registration::Payment::CreditCard::Activate::DECLINE_MESSAGE +
							": http://creditcard.example.com",
							reply.note.content
						)
					end]
				)
				Registration::Payment::CreditCard::Activate.new(
					iq,
					customer,
					:test_default_method,
					"+15555550000"
				).write.sync
				Registration::Payment::CreditCard::Activate::Transaction.verify
			end
			em :test_write_declines
		end
	end


M test/test_transaction.rb => test/test_transaction.rb +29 -8
@@ 5,6 5,7 @@ require "transaction"

Transaction::DB = Minitest::Mock.new
Transaction::BRAINTREE = Minitest::Mock.new
Transaction::REDIS = Minitest::Mock.new

class TransactionTest < Minitest::Test
	FAKE_BRAINTREE_TRANSACTION =


@@ 16,26 17,46 @@ class TransactionTest < Minitest::Test
		)

	def test_sale_fails
		Transaction::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
			:incr,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
			:expire,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test", 60 * 60 * 24]
		)
		braintree_transaction = Minitest::Mock.new
		Transaction::BRAINTREE.expect(:transaction, braintree_transaction)
		braintree_transaction.expect(
			:sale,
			EMPromise.resolve(
				OpenStruct.new(success?: false)
				OpenStruct.new(success?: false, message: "declined")
			),
			[Hash]
		)
		assert_raises do
		assert_raises("declined") do
			Transaction.sale(
				"merchant_usd",
				OpenStruct.new(token: "token"),
				123
				Customer.new("test", plan_name: "test_usd"),
				123,
				OpenStruct.new(token: "token")
			).sync
		end
	end
	em :test_sale_fails

	def test_sale
		Transaction::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]
		)
		braintree_transaction = Minitest::Mock.new
		Transaction::BRAINTREE.expect(:transaction, braintree_transaction)
		braintree_transaction.expect(


@@ 54,9 75,9 @@ class TransactionTest < Minitest::Test
			}]
		)
		result = Transaction.sale(
			"merchant_usd",
			OpenStruct.new(token: "token"),
			123
			Customer.new("test", plan_name: "test_usd"),
			123,
			OpenStruct.new(token: "token")
		).sync
		assert_kind_of Transaction, result
	end