From 29f56bde9116b2749748b908c1b5036d5c7e5742 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Oct 2024 10:48:04 -0500 Subject: [PATCH] Initial churnbuster integration --- config-schema.dhall | 1 + config.dhall.sample | 6 +++- lib/churnbuster.rb | 63 +++++++++++++++++++++++++++++++++++ lib/credit_card_sale.rb | 11 +++++- lib/low_balance.rb | 14 +++++++- test/test_credit_card_sale.rb | 2 +- test/test_low_balance.rb | 31 +++++++++++++++-- 7 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 lib/churnbuster.rb diff --git a/config-schema.dhall b/config-schema.dhall index 07f213f777c649a80f71abb17acff4656101d2fd..fff429e7654e1b5e26ac4cd6e1639a94d5e6ca9a 100644 --- a/config-schema.dhall +++ b/config-schema.dhall @@ -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 } diff --git a/config.dhall.sample b/config.dhall.sample index 4a1fffaaef496d15e3bd62d3b1e501b95cbc23b1..ba026d00b6478a2ac40db46acc6a734d18ace475 100644 --- a/config.dhall.sample +++ b/config.dhall.sample @@ -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 = "" + } } diff --git a/lib/churnbuster.rb b/lib/churnbuster.rb new file mode 100644 index 0000000000000000000000000000000000000000..668bca34bebe657b807b3cd7ffdcb677b203fd56 --- /dev/null +++ b/lib/churnbuster.rb @@ -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 diff --git a/lib/credit_card_sale.rb b/lib/credit_card_sale.rb index 7f5db49b976ca3cde0f5dba63f8e70ae98f425f7..5335ffaa8ec4f7c83f8ddcfc66fcb5fe0b9abd79 100644 --- a/lib/credit_card_sale.rb +++ b/lib/credit_card_sale.rb @@ -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 diff --git a/lib/low_balance.rb b/lib/low_balance.rb index 2b10a460a2419a1abe2858778af112f8da323388..dd4661fa25200df851a8d3522cf33dfc625b8b7f 100644 --- a/lib/low_balance.rb +++ b/lib/low_balance.rb @@ -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, diff --git a/test/test_credit_card_sale.rb b/test/test_credit_card_sale.rb index 19b882330e784305fd9b7068af6646d38d8cad33..7a1eea1f3d14423fd53b914c20b6b157498c2dbe 100644 --- a/test/test_credit_card_sale.rb +++ b/test/test_credit_card_sale.rb @@ -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, diff --git a/test/test_low_balance.rb b/test/test_low_balance.rb index 8ff4408e22625a8a66a5d0bc46743c5745247f55..53a05b7a62e372d6ef89bdebd6b3bc45a73e9914 100644 --- a/test/test_low_balance.rb +++ b/test/test_low_balance.rb @@ -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