~singpolyma/sgx-jmp

29f56bde9116b2749748b908c1b5036d5c7e5742 — Stephen Paul Weber 13 days ago 7d3dd44 master
Initial churnbuster integration
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