Handle credit card decline

Stephen Paul Weber created

Show user error on decline and give the chance to pick another card.

Change summary

lib/registration.rb       | 43 ++++++++++++++++++++++++++++++++++++++++
test/test_helper.rb       |  8 +++++-
test/test_registration.rb | 42 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 90 insertions(+), 3 deletions(-)

Detailed changes

lib/registration.rb 🔗

@@ -222,12 +222,53 @@ class Registration
 						@customer.merchant_account,
 						@payment_method,
 						CONFIG[:activation_amount]
-					).then(&:insert).then {
+					).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

test/test_helper.rb 🔗

@@ -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
 

test/test_registration.rb 🔗

@@ -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
@@ -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
+				Registration::Payment::CreditCard::Activate::Transaction.expect(
+					:sale,
+					EMPromise.reject("declined"),
+					[
+						"merchant_usd",
+						:test_default_method,
+						CONFIG[:activation_amount]
+					]
+				)
+				iq = Blather::Stanza::Iq::Command.new
+				iq.from = "test@example.com"
+				customer = Minitest::Mock.new(
+					Customer.new("test", plan_name: "test_usd")
+				)
+				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