diff --git a/forms/admin_financial_info.rb b/forms/admin_financial_info.rb new file mode 100644 index 0000000000000000000000000000000000000000..d7883de082ba309ffafa3e12f9f3da4a5b04cb1b --- /dev/null +++ b/forms/admin_financial_info.rb @@ -0,0 +1,9 @@ +result! +title "Customer Financial Info" + +field( + var: "declines", + label: "Declines", + description: "out of 2", + value: @info.declines.to_s +) diff --git a/forms/admin_info.rb b/forms/admin_info.rb index bff2ca15f7fed9c8c66216baecae977a54ee92f8..1da597a6efb4b17c9ed2a36a3870ce1707aa6935 100644 --- a/forms/admin_info.rb +++ b/forms/admin_info.rb @@ -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", diff --git a/forms/admin_menu.rb b/forms/admin_menu.rb new file mode 100644 index 0000000000000000000000000000000000000000..486f3eb3524ccba3e70a4058f6ed28d70d5f06d1 --- /dev/null +++ b/forms/admin_menu.rb @@ -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" } + ] +) diff --git a/forms/admin_payment_methods.rb b/forms/admin_payment_methods.rb new file mode 100644 index 0000000000000000000000000000000000000000..cb99c8fc3beff1dd78d0c1aac25ece9d9a419e52 --- /dev/null +++ b/forms/admin_payment_methods.rb @@ -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 diff --git a/forms/admin_transaction_list.rb b/forms/admin_transaction_list.rb new file mode 100644 index 0000000000000000000000000000000000000000..3fb74aa86d2ad23651a5491cabf0f3cab5c57f6c --- /dev/null +++ b/forms/admin_transaction_list.rb @@ -0,0 +1,10 @@ +result! +title "Transactions" + +table( + @transactions, + formatted_amount: "Amount", + note: "Note", + created_at: "Date", + transaction_id: "Transaction ID" +) diff --git a/forms/transactions.rb b/forms/transactions.rb new file mode 100644 index 0000000000000000000000000000000000000000..b48b1deabcdff8fee104be11a67168fdaebb5095 --- /dev/null +++ b/forms/transactions.rb @@ -0,0 +1,9 @@ +result! +title "Transactions" + +table( + @transactions, + formatted_amount: "Amount", + note: "Note", + created_at: "Date" +) diff --git a/lib/admin_command.rb b/lib/admin_command.rb new file mode 100644 index 0000000000000000000000000000000000000000..5eedb57488600b26263f3bc3067a16566405340e --- /dev/null +++ b/lib/admin_command.rb @@ -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 diff --git a/lib/alt_top_up_form.rb b/lib/alt_top_up_form.rb index 916224ed0e05d0ebc19bb0800347888594d4c2e1..7c41d09f1e0f773c6d8409361e72358551218417 100644 --- a/lib/alt_top_up_form.rb +++ b/lib/alt_top_up_form.rb @@ -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 diff --git a/lib/customer.rb b/lib/customer.rb index a295875811855cb47014614502405f4d23ff4041..5cf426a180217952b97f9c1fd8456c1cf9b1d9bb 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -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 diff --git a/lib/customer_finacials.rb b/lib/customer_finacials.rb new file mode 100644 index 0000000000000000000000000000000000000000..a39dfa63ebaa958d5f4d51efb6a9f400e253236f --- /dev/null +++ b/lib/customer_finacials.rb @@ -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 diff --git a/lib/customer_info.rb b/lib/customer_info.rb index a6167bbaf4ce99eb64d5bea67a676658575bd1f1..29f0fbd9707f0df6621ee05e99a54f3807f18433 100644 --- a/lib/customer_info.rb +++ b/lib/customer_info.rb @@ -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 diff --git a/lib/financial_info.rb b/lib/financial_info.rb new file mode 100644 index 0000000000000000000000000000000000000000..8fb8cc929de3bde8948e78aa46203ca90403ac2b --- /dev/null +++ b/lib/financial_info.rb @@ -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 diff --git a/lib/form_template.rb b/lib/form_template.rb index b04bd64806b040f4ccffe1f04b7e37960fabc514..55fdcc6147853a043bc0acd6eec2de67d84d04ac 100644 --- a/lib/form_template.rb +++ b/lib/form_template.rb @@ -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 diff --git a/lib/payment_methods.rb b/lib/payment_methods.rb index 33d4f33c54f1447e10abf4652de017c7c72d43c6..d8799fdc7e6fde7bc9b257389868facf5d1a332b 100644 --- a/lib/payment_methods.rb +++ b/lib/payment_methods.rb @@ -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 diff --git a/lib/transaction.rb b/lib/transaction.rb index 5d55f8f5bed41c168815044e60bd66e93451d9c8..72b11324d3f4cc658f860e4a3a8988c5d4112ffc 100644 --- a/lib/transaction.rb +++ b/lib/transaction.rb @@ -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 diff --git a/sgx_jmp.rb b/sgx_jmp.rb index bcfe1019977077f9eb30ccb582ad2a18bb127952..53d2227213f383fc8f8dbc76832abf2e7e1cd233 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -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" @@ -472,6 +473,18 @@ Command.new( end }.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", @@ -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)) diff --git a/test/test_alt_top_up_form.rb b/test/test_alt_top_up_form.rb index 264c099e31ed8af8a2533cc29ac38f54b2d3603d..ab552d8559aa58f78dde9dde3a06690e17f22803 100644 --- a/test/test_alt_top_up_form.rb +++ b/test/test_alt_top_up_form.rb @@ -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"] diff --git a/test/test_buy_account_credit_form.rb b/test/test_buy_account_credit_form.rb index 8268bddc140df60636ac642340406c752103beda..0c5300838e9824dfc179abddab33189ccd77cff3 100644 --- a/test/test_buy_account_credit_form.rb +++ b/test/test_buy_account_credit_form.rb @@ -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: [])), diff --git a/test/test_customer.rb b/test/test_customer.rb index 9a0836d929e44a6423c40368433c1b6e6809759f..360cf4876336e45130d676f7244c4fa993de70a5 100644 --- a/test/test_customer.rb +++ b/test/test_customer.rb @@ -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 diff --git a/test/test_low_balance.rb b/test/test_low_balance.rb index afa81d19b16eff60dcb856beccefb8a56bbfaacb..d639d44bb00709bbb849e86b7301db70f4732d33 100644 --- a/test/test_low_balance.rb +++ b/test/test_low_balance.rb @@ -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"] diff --git a/test/test_registration.rb b/test/test_registration.rb index 06ee38ab314259536b359c5414d857b4bc920186..00b75e51761fbad390c517783391c723f2df9f7f 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -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"] diff --git a/test/test_transaction.rb b/test/test_transaction.rb index d87b7bd13e3785b5442f61271ab711d571b95777..69a4b0220f2573c34c157be566e88c9b3c7c7ccb 100644 --- a/test/test_transaction.rb +++ b/test/test_transaction.rb @@ -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