M config-schema.dhall => config-schema.dhall +1 -0
@@ 12,6 12,7 @@
, private_key : Text
, public_key : Text
}
+, churnbuster : { account_id : Text, api_key : Text }
, component : { jid : Text, secret : Text }
, credit_card_url : forall (jid : Text) -> forall (customer_id : Text) -> Text
, creds : { account : Text, password : Text, username : Text }
M config.dhall.sample => config.dhall.sample +5 -1
@@ 114,5 114,9 @@ in
simpleswap_api_key = "",
reachability_senders = [ "+14445556666" ],
support_link = \(customer_jid: Text) ->
- "http://localhost:3002/app/accounts/2/contacts/custom_attributes/jid/${customer_jid}"
+ "http://localhost:3002/app/accounts/2/contacts/custom_attributes/jid/${customer_jid}",
+ churnbuster = {
+ api_key = "",
+ account_id = ""
+ }
}
A lib/churnbuster.rb => lib/churnbuster.rb +63 -0
@@ 0,0 1,63 @@
+# frozen_string_literal: true
+
+require "bigdecimal"
+require "em-http"
+require "em_promise"
+require "em-synchrony/em-http" # For apost vs post
+require "json"
+require "securerandom"
+
+class Churnbuster
+ def initialize(
+ account_id: CONFIG.dig(:churnbuster, :account_id),
+ api_key: CONFIG.dig(:churnbuster, :api_key)
+ )
+ @account_id = account_id
+ @api_key = api_key
+ end
+
+ def failed_payment(customer, amount, txid)
+ post_json(
+ "https://api.churnbuster.io/v1/failed_payments",
+ {
+ customer: format_customer(customer),
+ payment: format_tx(customer, amount, txid)
+ }
+ )
+ end
+
+protected
+
+ def format_tx(customer, amount, txid)
+ {
+ source: "braintree",
+ source_id: txid,
+ amount_in_cents: (amount * 100).to_i,
+ currency: customer.currency
+ }
+ end
+
+ def format_customer(customer)
+ unprox = ProxiedJID.new(customer.jid).unproxied.to_s
+ email = "#{unprox.gsub(/@/, '=40').gsub(/\./, '=2e')}@smtp.cheogram.com"
+ {
+ source: "braintree",
+ source_id: customer.customer_id,
+ email: email,
+ properties: {}
+ }
+ end
+
+ def post_json(url, data)
+ EM::HttpRequest.new(
+ url,
+ tls: { verify_peer: true }
+ ).apost(
+ head: {
+ "Authorization" => [@account_id, @api_key],
+ "Content-Type" => "application/json"
+ },
+ body: data.to_json
+ )
+ end
+end
M lib/credit_card_sale.rb => lib/credit_card_sale.rb +10 -1
@@ 91,6 91,15 @@ protected
end
@customer.mark_decline
- raise response.message
+ raise BraintreeFailure, response
+ end
+end
+
+class BraintreeFailure < StandardError
+ attr_reader :response
+
+ def initialize(response)
+ super response.message
+ @response = response
end
end
M lib/low_balance.rb => lib/low_balance.rb +13 -1
@@ 1,8 1,9 @@
# frozen_string_literal: true
+require_relative "churnbuster"
+require_relative "credit_card_sale"
require_relative "expiring_lock"
require_relative "transaction"
-require_relative "credit_card_sale"
class LowBalance
def self.for(customer, transaction_amount=0)
@@ 122,7 123,18 @@ class LowBalance
CreditCardSale.create(@customer, amount: top_up_amount)
end
+ def churnbuster(e)
+ return unless e.is_a?(BraintreeFailure)
+
+ Churnbuster.new.failed_payment(
+ @customer,
+ top_up_amount,
+ e.response.transaction.id
+ )
+ end
+
def failed(e)
+ churnbuster(e)
@method && REDIS.setex(
"jmp_auto_top_up_block-#{@method.unique_number_identifier}",
60 * 60 * 24 * 30,
M test/test_credit_card_sale.rb => test/test_credit_card_sale.rb +1 -1
@@ 62,7 62,7 @@ class CreditCardSaleTest < Minitest::Test
options: { submit_for_settlement: true },
payment_method_token: "token"
)
- assert_raises(RuntimeError) do
+ assert_raises(BraintreeFailure) do
CreditCardSale.new(
customer(plan_name: "test_usd"),
amount: 99,
M test/test_low_balance.rb => test/test_low_balance.rb +29 -2
@@ 159,7 159,9 @@ class LowBalanceTest < Minitest::Test
LowBalance::AutoTopUp::CreditCardSale = Minitest::Mock.new
def setup
- @customer = Minitest::Mock.new(customer(auto_top_up_amount: 100))
+ @customer = Minitest::Mock.new(
+ customer(auto_top_up_amount: 100, plan_name: "test_usd")
+ )
@auto_top_up = LowBalance::AutoTopUp.new(@customer)
end
@@ 242,6 244,28 @@ class LowBalanceTest < Minitest::Test
em :test_border_low_balance_notify!
def test_decline_notify!
+ stub_request(:post, "https://api.churnbuster.io/v1/failed_payments")
+ .with(
+ body: {
+ customer: {
+ source: "braintree",
+ source_id: "test",
+ email: "test@smtp.cheogram.com",
+ properties: {}
+ },
+ payment: {
+ source: "braintree",
+ source_id: "tx",
+ amount_in_cents: 10000,
+ currency: "USD"
+ }
+ }.to_json,
+ headers: {
+ "Authorization" => ["", ""],
+ "Content-Type" => "application/json"
+ }
+ ).to_return(status: 200, body: "", headers: {})
+
@customer.expect(
:stanza_to,
nil,
@@ 254,7 278,10 @@ class LowBalanceTest < Minitest::Test
)
LowBalance::AutoTopUp::CreditCardSale.expect(
:create,
- EMPromise.reject(RuntimeError.new("test")),
+ EMPromise.reject(BraintreeFailure.new(OpenStruct.new(
+ message: "test",
+ transaction: OpenStruct.new(id: "tx")
+ ))),
[@customer], amount: 100.to_d
)
@auto_top_up.notify!.sync