A forms/admin_financial_info.rb => forms/admin_financial_info.rb +9 -0
@@ 0,0 1,9 @@
+result!
+title "Customer Financial Info"
+
+field(
+ var: "declines",
+ label: "Declines",
+ description: "out of 2",
+ value: @info.declines.to_s
+)
M forms/admin_info.rb => forms/admin_info.rb +8 -0
@@ 23,6 23,14 @@ field(
value: @admin_info.customer_id
)
+if @admin_info.info.tel
+ field(
+ var: "tel_link",
+ label: "Phone Number Info",
+ value: @admin_info.tel_link
+ )
+end
+
if @admin_info.fwd.uri
field(
var: "fwd",
A => +14 -0
@@ 0,0 1,14 @@
form!
title "Menu"
field(
var: "action",
type: "list-single",
open: true,
label: "Pick an action",
description: "or put a new customer info",
options: [
{ value: "info", label: "Customer Info" },
{ value: "financial", label: "Customer Billing Information" }
]
)
A forms/admin_payment_methods.rb => forms/admin_payment_methods.rb +10 -0
@@ 0,0 1,10 @@
+result!
+title "Customer Payment Methods"
+
+unless @payment_methods.empty?
+ field @payment_methods.to_list_single(label: "Credit Cards")
+end
+
+AltTopUpForm::HasBitcoinAddresses.new(@btc_addresses, desc: nil).each do |spec|
+ field spec
+end
A forms/admin_transaction_list.rb => forms/admin_transaction_list.rb +10 -0
@@ 0,0 1,10 @@
+result!
+title "Transactions"
+
+table(
+ @transactions,
+ formatted_amount: "Amount",
+ note: "Note",
+ created_at: "Date",
+ transaction_id: "Transaction ID"
+)
A forms/transactions.rb => forms/transactions.rb +9 -0
@@ 0,0 1,9 @@
+result!
+title "Transactions"
+
+table(
+ @transactions,
+ formatted_amount: "Amount",
+ note: "Note",
+ created_at: "Date"
+)
A lib/admin_command.rb => lib/admin_command.rb +79 -0
@@ 0,0 1,79 @@
+# frozen_string_literal: true
+
+require_relative "customer_info_form"
+require_relative "financial_info"
+require_relative "form_template"
+
+class AdminCommand
+ def initialize(target_customer)
+ @target_customer = target_customer
+ end
+
+ def start
+ action_info.then { menu }
+ end
+
+ def reply(form)
+ Command.reply { |reply|
+ reply.allowed_actions = [:next]
+ reply.command << form
+ }
+ end
+
+ def menu
+ reply(FormTemplate.render("admin_menu")).then do |response|
+ handle(response.form.field("action").value)
+ end
+ end
+
+ def handle(action)
+ if respond_to?("action_#{action}")
+ send("action_#{action}")
+ else
+ new_context(action)
+ end.then { menu }
+ end
+
+ def new_context(q)
+ CustomerInfoForm.new.parse_something(q).then do |new_customer|
+ if new_customer.respond_to?(:customer_id)
+ AdminCommand.new(new_customer).start
+ else
+ reply(new_customer.form)
+ end
+ end
+ end
+
+ def action_info
+ @target_customer.admin_info.then do |info|
+ reply(info.form)
+ end
+ end
+
+ def action_financial
+ AdminFinancialInfo.for(@target_customer).then do |financial_info|
+ reply(FormTemplate.render(
+ "admin_financial_info",
+ info: financial_info
+ )).then {
+ pay_methods(financial_info)
+ }.then {
+ transactions(financial_info)
+ }
+ end
+ end
+
+ def pay_methods(financial_info)
+ reply(FormTemplate.render(
+ "admin_payment_methods",
+ **financial_info.to_h
+ ))
+ end
+
+ def transactions(financial_info)
+ reply(FormTemplate.render(
+ "admin_transaction_list",
+ transactions: financial_info.transactions
+ ))
+ end
+end
M lib/alt_top_up_form.rb => lib/alt_top_up_form.rb +3 -2
@@ 101,8 101,9 @@ class AltTopUpForm
end
class HasBitcoinAddresses
- def initialize(addrs)
+ def initialize(addrs, desc: DESCRIPTION)
@addrs = addrs
+ @desc = desc
end
DESCRIPTION =
@@ 116,7 117,7 @@ class AltTopUpForm
var: "btc_address",
type: "fixed",
label: "Bitcoin Addresses",
- description: DESCRIPTION,
+ description: @desc,
value: @addrs
)
end
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/customer_info.rb => lib/customer_info.rb +9 -0
@@ 118,4 118,13 @@ class AdminInfo
def form
FormTemplate.render("admin_info", admin_info: self)
end
+
+ def tel_link
+ [
+ "https://dashboard.bandwidth.com/portal/r/a",
+ CONFIG[:creds][:account],
+ "numbers/details",
+ info.tel.gsub(/\A\+1/, "")
+ ].join("/")
+ end
end
A lib/financial_info.rb => lib/financial_info.rb +25 -0
@@ 0,0 1,25 @@
+# frozen_string_literal: true
+
+require "value_semantics/monkey_patched"
+
+class AdminFinancialInfo
+ value_semantics do
+ transactions ArrayOf(CustomerFinancials::TransactionInfo)
+ declines Integer
+ btc_addresses ArrayOf(String)
+ payment_methods PaymentMethods
+ end
+
+ def self.for(customer)
+ EMPromise.all([
+ customer.transactions, customer.declines,
+ customer.payment_methods, customer.btc_addresses
+ ]).then do |transactions, declines, payment_methods, btc_addresses|
+ new(
+ transactions: transactions,
+ declines: declines || 0,
+ payment_methods: payment_methods, btc_addresses: btc_addresses
+ )
+ end
+ end
+end
M lib/form_template.rb => lib/form_template.rb +11 -0
@@ 80,6 80,17 @@ class FormTemplate
open || regex || range
end
+ # Given a map of fields to labels, and a list of objects this will
+ # produce a table from calling each field's method on every object in the
+ # list. So, this list is value_semantics / OpenStruct style
+ def table(list, **fields)
+ keys = fields.keys
+ FormTable.new(
+ list.map { |x| keys.map { |k| x.public_send(k) } },
+ **fields
+ ).add_to_form(@__form)
+ end
+
def field(datatype: nil, open: false, regex: nil, range: nil, **kwargs)
f = Blather::Stanza::X::Field.new(kwargs)
if datatype || open || regex || range
M lib/payment_methods.rb => lib/payment_methods.rb +11 -1
@@ 50,7 50,13 @@ class PaymentMethods
false
end
- class Empty
+ def to_a
+ @methods
+ end
+
+ class Empty < PaymentMethods
+ def initialize; end
+
def default_payment_method; end
def to_list_single(*)
@@ 60,5 66,9 @@ class PaymentMethods
def empty?
true
end
+
+ def to_a
+ []
+ 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 sgx_jmp.rb => sgx_jmp.rb +14 -5
@@ 67,6 67,7 @@ end
require_relative "lib/polyfill"
require_relative "lib/alt_top_up_form"
+require_relative "lib/admin_command"
require_relative "lib/add_bitcoin_address"
require_relative "lib/backend_sgx"
require_relative "lib/bwmsgsv2_repo"
@@ 473,6 474,18 @@ Command.new(
}.register(self).then(&CommandList.method(:register))
Command.new(
+ "transactions",
+ "Show Transactions",
+ list_for: ->(customer:, **) { !!customer&.currency }
+) {
+ Command.customer.then(&:transactions).then do |txs|
+ Command.finish do |reply|
+ reply.command << FormTemplate.render("transactions", transactions: txs)
+ end
+ end
+}.register(self).then(&CommandList.method(:register))
+
+Command.new(
"configure calls",
"Configure Calls",
customer_repo: CustomerRepo.new(sgx_repo: Bwmsgsv2Repo.new)
@@ 714,11 727,7 @@ Command.new(
}.then { |response|
CustomerInfoForm.new.find_customer(response)
}.then do |target_customer|
- target_customer.admin_info.then do |info|
- Command.finish do |reply|
- reply.command << info.form
- end
- end
+ AdminCommand.new(target_customer).start
end
end
}.register(self).then(&CommandList.method(:register))
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