~singpolyma/sgx-jmp

a5cf98aa2aef066c2317bc3a822d099743f3a815 — Christopher Vollick 6 months ago c668050
Customer Financials

I've pulled out information about payment methods and bitcoin into its
own thing. It's kind of a repository, except that it only exposes
fetchers and doesn't load anything generally.

There's also a few new methods here that aren't used yet, but will be
shortly.
M lib/customer.rb => lib/customer.rb +8 -28
@@ 4,10 4,11 @@ require "forwardable"

require_relative "./api"
require_relative "./blather_ext"
require_relative "./customer_info"
require_relative "./customer_ogm"
require_relative "./customer_plan"
require_relative "./customer_usage"
require_relative "./customer_plan"
require_relative "./customer_ogm"
require_relative "./customer_info"
require_relative "./customer_finacials"
require_relative "./backend_sgx"
require_relative "./ibr"
require_relative "./payment_methods"


@@ 27,6 28,9 @@ class Customer
	def_delegators :@sgx, :register!, :registered?, :set_ogm_url,
	               :fwd, :transcription_enabled
	def_delegators :@usage, :usage_report, :message_usage, :incr_message_usage
	def_delegators :@financials, :payment_methods, :btc_addresses,
	               :add_btc_address, :declines, :mark_decline,
	               :transactions

	def initialize(
		customer_id,


@@ 38,6 42,7 @@ class Customer
	)
		@plan = plan
		@usage = CustomerUsage.new(customer_id)
		@financials = CustomerFinancials.new(customer_id)
		@customer_id = customer_id
		@jid = jid
		@balance = balance


@@ 61,14 66,6 @@ class Customer
		)
	end

	def payment_methods
		BRAINTREE
			.customer
			.find(@customer_id)
			.catch { OpenStruct.new(payment_methods: []) }
			.then(PaymentMethods.method(:for_braintree_customer))
	end

	def unused_invites
		promise = DB.query_defer(<<~SQL, [customer_id])
			SELECT code FROM unused_invites WHERE creator_id=$1


@@ 105,23 102,6 @@ class Customer
		sip_account.with_random_password.put
	end

	def btc_addresses
		REDIS.smembers("jmp_customer_btc_addresses-#{customer_id}")
	end

	def add_btc_address
		REDIS.spopsadd([
			"jmp_available_btc_addresses",
			"jmp_customer_btc_addresses-#{customer_id}"
		]).then do |addr|
			ELECTRUM.notify(
				addr,
				CONFIG[:electrum_notify_url].call(addr, customer_id)
			)
			addr
		end
	end

	def admin?
		CONFIG[:admins].include?(jid.to_s)
	end

A lib/customer_finacials.rb => lib/customer_finacials.rb +74 -0
@@ 0,0 1,74 @@
# frozen_string_literal: true

class CustomerFinancials
	def initialize(customer_id)
		@customer_id = customer_id
	end

	def payment_methods
		BRAINTREE
			.customer
			.find(@customer_id)
			.catch { OpenStruct.new(payment_methods: []) }
			.then(PaymentMethods.method(:for_braintree_customer))
	end

	def btc_addresses
		REDIS.smembers("jmp_customer_btc_addresses-#{@customer_id}")
	end

	def add_btc_address
		REDIS.spopsadd([
			"jmp_available_btc_addresses",
			"jmp_customer_btc_addresses-#{@customer_id}"
		]).then do |addr|
			ELECTRUM.notify(
				addr,
				CONFIG[:electrum_notify_url].call(addr, @customer_id)
			)
			addr
		end
	end

	def declines
		REDIS.get("jmp_pay_decline-#{@customer_id}")
	end

	def mark_decline
		REDIS.incr("jmp_pay_decline-#{@customer_id}").then do
			REDIS.expire("jmp_pay_decline-#{@customer_id}", 60 * 60 * 24)
		end
	end

	class TransactionInfo
		value_semantics do
			transaction_id String
			created_at Time
			amount BigDecimal
			note String
		end

		def formatted_amount
			"$%.4f" % amount
		end
	end

	TRANSACTIONS_SQL = <<~SQL
		SELECT
			transaction_id,
			created_at,
			amount,
			note
		FROM transactions WHERE customer_id = $1;
	SQL

	def transactions
		txns = DB.query_defer(TRANSACTIONS_SQL, [@customer_id])

		txns.then do |rows|
			rows.map { |row|
				TransactionInfo.new(**row.transform_keys(&:to_sym))
			}
		end
	end
end

M lib/transaction.rb => lib/transaction.rb +2 -4
@@ 4,7 4,7 @@ require "bigdecimal"

class Transaction
	def self.sale(customer, amount:, payment_method: nil)
		REDIS.get("jmp_pay_decline-#{customer.customer_id}").then do |declines|
		customer.declines.then do |declines|
			raise "too many declines" if declines.to_i >= 2

			BRAINTREE.transaction.sale(


@@ 20,9 20,7 @@ class Transaction
	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
		customer.mark_decline
		raise response.message
	end


M test/test_alt_top_up_form.rb => test/test_alt_top_up_form.rb +3 -3
@@ 6,7 6,7 @@ require "customer"

class AltTopUpFormTest < Minitest::Test
	def test_for
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]


@@ 19,7 19,7 @@ class AltTopUpFormTest < Minitest::Test
	em :test_for

	def test_for_addresses
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve(["testaddr"]),
			["jmp_customer_btc_addresses-test"]


@@ 32,7 32,7 @@ class AltTopUpFormTest < Minitest::Test
	em :test_for_addresses

	def test_for_cad
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]

M test/test_buy_account_credit_form.rb => test/test_buy_account_credit_form.rb +1 -1
@@ 17,7 17,7 @@ class BuyAccountCreditFormTest < Minitest::Test

	def test_for
		braintree_customer = Minitest::Mock.new
		Customer::BRAINTREE.expect(:customer, braintree_customer)
		CustomerFinancials::BRAINTREE.expect(:customer, braintree_customer)
		braintree_customer.expect(
			:find,
			EMPromise.resolve(OpenStruct.new(payment_methods: [])),

M test/test_customer.rb => test/test_customer.rb +8 -6
@@ 5,13 5,15 @@ require "customer"

Customer::BLATHER = Minitest::Mock.new
Customer::BRAINTREE = Minitest::Mock.new
Customer::ELECTRUM = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
Customer::DB = Minitest::Mock.new
Customer::IQ_MANAGER = Minitest::Mock.new
CustomerPlan::DB = Minitest::Mock.new
CustomerUsage::REDIS = Minitest::Mock.new
CustomerUsage::DB = Minitest::Mock.new
CustomerFinancials::REDIS = Minitest::Mock.new
CustomerFinancials::ELECTRUM = Minitest::Mock.new
CustomerFinancials::BRAINTREE = Minitest::Mock.new

class CustomerTest < Minitest::Test
	def test_bill_plan_activate


@@ 191,7 193,7 @@ class CustomerTest < Minitest::Test
	em :test_sip_account_error

	def test_btc_addresses
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve(["testaddr"]),
			["jmp_customer_btc_addresses-test"]


@@ 202,19 204,19 @@ class CustomerTest < Minitest::Test
	em :test_btc_addresses

	def test_add_btc_address
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:spopsadd,
			EMPromise.resolve("testaddr"),
			[["jmp_available_btc_addresses", "jmp_customer_btc_addresses-test"]]
		)
		Customer::ELECTRUM.expect(
		CustomerFinancials::ELECTRUM.expect(
			:notify,
			EMPromise.resolve(nil),
			["testaddr", "http://notify.example.com"]
		)
		assert_equal "testaddr", customer.add_btc_address.sync
		assert_mock Customer::REDIS
		assert_mock Customer::ELECTRUM
		assert_mock CustomerFinancials::REDIS
		assert_mock CustomerFinancials::ELECTRUM
	end
	em :test_add_btc_address
end

M test/test_low_balance.rb => test/test_low_balance.rb +2 -2
@@ 5,7 5,7 @@ require "low_balance"

ExpiringLock::REDIS = Minitest::Mock.new
CustomerPlan::REDIS = Minitest::Mock.new
Customer::REDIS = Minitest::Mock.new
CustomerFinancials::REDIS = Minitest::Mock.new

class LowBalanceTest < Minitest::Test
	def test_for_locked


@@ 24,7 24,7 @@ class LowBalanceTest < Minitest::Test
			EMPromise.resolve(0),
			["jmp_customer_low_balance-test"]
		)
		Customer::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:smembers,
			EMPromise.resolve([]),
			["jmp_customer_btc_addresses-test"]

M test/test_registration.rb => test/test_registration.rb +4 -4
@@ 254,7 254,7 @@ class RegistrationTest < Minitest::Test
	end

	class PaymentTest < Minitest::Test
		Customer::BRAINTREE = Minitest::Mock.new
		CustomerFinancials::BRAINTREE = Minitest::Mock.new

		def test_for_bitcoin
			cust = Minitest::Mock.new(customer)


@@ 273,7 273,7 @@ class RegistrationTest < Minitest::Test

		def test_for_credit_card
			braintree_customer = Minitest::Mock.new
			Customer::BRAINTREE.expect(
			CustomerFinancials::BRAINTREE.expect(
				:customer,
				braintree_customer
			)


@@ 313,7 313,7 @@ class RegistrationTest < Minitest::Test

		class BitcoinTest < Minitest::Test
			Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new
			Customer::REDIS = Minitest::Mock.new
			CustomerFinancials::REDIS = Minitest::Mock.new

			def setup
				@customer = Minitest::Mock.new(


@@ 330,7 330,7 @@ class RegistrationTest < Minitest::Test
			end

			def test_write
				Customer::REDIS.expect(
				CustomerFinancials::REDIS.expect(
					:smembers,
					EMPromise.resolve([]),
					["jmp_customer_btc_addresses-test"]

M test/test_transaction.rb => test/test_transaction.rb +6 -4
@@ 18,17 18,17 @@ class TransactionTest < Minitest::Test
		)

	def test_sale_fails
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:incr,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test"]
		)
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:expire,
			EMPromise.resolve(nil),
			["jmp_pay_decline-test", 60 * 60 * 24]


@@ 49,11 49,12 @@ class TransactionTest < Minitest::Test
				payment_method: OpenStruct.new(token: "token")
			).sync
		end
		assert_mock CustomerFinancials::REDIS
	end
	em :test_sale_fails

	def test_sale
		Transaction::REDIS.expect(
		CustomerFinancials::REDIS.expect(
			:get,
			EMPromise.resolve("1"),
			["jmp_pay_decline-test"]


@@ 81,6 82,7 @@ class TransactionTest < Minitest::Test
			payment_method: OpenStruct.new(token: "token")
		).sync
		assert_kind_of Transaction, result
		assert_mock CustomerFinancials::REDIS
	end
	em :test_sale