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