From cf99029185ec9252bedd637db072d02b25fe1950 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Jun 2025 10:04:13 -0500 Subject: [PATCH 01/16] Remove legacy card charge behaviour We used to have them add card in web then charge after, but these days we charge them in web for this first tx to get 3DS, so this code path is all but dead and we can remove it. --- lib/registration.rb | 70 ++-------------------------- test/test_registration.rb | 97 +-------------------------------------- 2 files changed, 7 insertions(+), 160 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index 910db903fb32822ff35443893741c8802561bfa9..1884cf59dd4e088bc02d5ad35bfdefb87c270893 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -396,24 +396,19 @@ class Registration Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } def self.for(in_customer, tel, finish: Finish, **) - reload_customer(in_customer).then do |(customer, payment_methods)| + reload_customer(in_customer).then do |customer| if customer.balance >= CONFIG[:activation_amount_accept] next BillPlan.new(customer, tel, finish: finish) end - if (method = payment_methods.default_payment_method) - next Activate.new(customer, method, tel, finish: finish) - end - new(customer, tel, finish: finish) end end def self.reload_customer(customer) - EMPromise.all([ - Command.execution.customer_repo.find(customer.customer_id), - customer.payment_methods - ]) + EMPromise.resolve(nil).then do + Command.execution.customer_repo.find(customer.customer_id) + end end def initialize(customer, tel, finish: Finish) @@ -428,7 +423,7 @@ class Registration reply.to.stripped.to_s.gsub("\\", "%5C"), @customer.customer_id ) + "&amount=#{CONFIG[:activation_amount]}" - oob.desc = "Add credit card, save, then next here to continue" + oob.desc = "Pay by credit card, save, then next here to continue" oob end @@ -444,61 +439,6 @@ class Registration CreditCard.for(@customer, @tel, finish: @finish).then(&:write) end end - - class Activate - def initialize(customer, payment_method, tel, finish: Finish) - @customer = customer - @payment_method = payment_method - @tel = tel - @finish = finish - end - - def write - CreditCardSale.create( - @customer, - amount: CONFIG[:activation_amount], - payment_method: @payment_method - ).then( - ->(_) { sold }, - ->(_) { declined } - ) - end - - protected - - def sold - BillPlan.new(@customer, @tel, finish: @finish).write - end - - DECLINE_MESSAGE = - "Your bank declined the transaction. " \ - "Often this happens when a person's credit card " \ - "is a US card that does not support international " \ - "transactions, as JMP is not based in the USA, though " \ - "we do support transactions in USD.\n\n" \ - "You may add another card" - - def decline_oob(reply) - oob = OOB.find_or_create(reply.command) - oob.url = CONFIG[:credit_card_url].call( - reply.to.stripped.to_s.gsub("\\", "%5C"), - @customer.customer_id - ) + "&amount=#{CONFIG[:activation_amount]}" - oob.desc = DECLINE_MESSAGE - oob - end - - def declined - Command.reply { |reply| - reply_oob = decline_oob(reply) - reply.allowed_actions = [:next] - reply.note_type = :error - reply.note_text = "#{reply_oob.desc}: #{reply_oob.url}" - }.then do - CreditCard.for(@customer, @tel, finish: @finish).then(&:write) - end - end - end end class InviteCode diff --git a/test/test_registration.rb b/test/test_registration.rb index d40fd78fc92ca17f79a840afc6306222430c3ded..d510a9000565c06a06a9a132c92dc0a72f5e5814 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -616,17 +616,6 @@ class RegistrationTest < Minitest::Test end def test_for_credit_card - braintree_customer = Minitest::Mock.new - CustomerFinancials::BRAINTREE.expect( - :customer, - braintree_customer - ) - CustomerFinancials::REDIS.expect(:smembers, [], ["block_credit_cards"]) - braintree_customer.expect( - :find, - EMPromise.resolve(OpenStruct.new(payment_methods: [])), - ["test"] - ) iq = Blather::Stanza::Iq::Command.new iq.from = "test@example.com" iq.form.fields = [ @@ -780,7 +769,7 @@ class RegistrationTest < Minitest::Test execute_command do Command.execution.customer_repo.expect(:find, cust, ["test"]) assert_kind_of( - Registration::Payment::CreditCard::Activate, + Registration::Payment::CreditCard, Registration::Payment::CreditCard.for( cust, "+15555550000" @@ -815,7 +804,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |reply| assert_equal [:execute, :next, :prev], reply.allowed_actions assert_equal( - "Add credit card, save, then next here to continue: " \ + "Pay by credit card, save, then next here to continue: " \ "http://creditcard.example.com?&amount=1", reply.note.content ) @@ -865,88 +854,6 @@ class RegistrationTest < Minitest::Test em :test_write end - class ActivateTest < Minitest::Test - Registration::Payment::CreditCard::Activate::Finish = - Minitest::Mock.new - Registration::Payment::CreditCard::Activate::CreditCardSale = - Minitest::Mock.new - Command::COMMAND_MANAGER = Minitest::Mock.new - - def test_write - customer = Minitest::Mock.new( - customer(plan_name: "test_usd") - ) - Registration::Payment::CreditCard::Activate::CreditCardSale.expect( - :create, - EMPromise.resolve(nil) - ) do |acustomer, amount:, payment_method:| - assert_operator customer, :===, acustomer - assert_equal CONFIG[:activation_amount], amount - assert_equal :test_default_method, payment_method - end - customer.expect( - :bill_plan, - nil, - note: "Bill +15555550000 for first month" - ) - Registration::Payment::CreditCard::Activate::Finish.expect( - :new, - OpenStruct.new(write: nil), - [customer, "+15555550000"] - ) - execute_command do - Registration::Payment::CreditCard::Activate.new( - customer, - :test_default_method, - "+15555550000" - ).write - end - Registration::Payment::CreditCard::Activate::CreditCardSale.verify - customer.verify - Registration::Payment::CreditCard::Activate::Finish.verify - end - em :test_write - - def test_write_declines - customer = Minitest::Mock.new( - customer(plan_name: "test_usd") - ) - iq = Blather::Stanza::Iq::Command.new - iq.from = "test@example.com" - msg = Registration::Payment::CreditCard::Activate::DECLINE_MESSAGE - Command::COMMAND_MANAGER.expect( - :write, - EMPromise.reject(:test_result), - [Matching.new do |reply| - assert_equal :error, reply.note_type - assert_equal( - "#{msg}: http://creditcard.example.com?&amount=1", - reply.note.content - ) - end] - ) - result = execute_command do - Registration::Payment::CreditCard::Activate::CreditCardSale.expect( - :create, - EMPromise.reject("declined") - ) do |acustomer, amount:, payment_method:| - assert_operator customer, :===, acustomer - assert_equal CONFIG[:activation_amount], amount - assert_equal :test_default_method, payment_method - end - - Registration::Payment::CreditCard::Activate.new( - customer, - :test_default_method, - "+15555550000" - ).write.catch { |e| e } - end - assert_equal :test_result, result - Registration::Payment::CreditCard::Activate::CreditCardSale.verify - end - em :test_write_declines - end - class InviteCodeTest < Minitest::Test Registration::Payment::InviteCode::DB = Minitest::Mock.new From cf0db8da4138daebc2a8e04b9902974b080bb310 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Jun 2025 10:27:17 -0500 Subject: [PATCH 02/16] Remove mostly-unused final_message Was only being used for PayPal transitions (which are mostly done) and only when transitioning to BTC (which they usually don't) and only provided extra reinforcement of a message they've already seen by now. Let's not complicate the code for that. --- forms/registration/bch.rb | 5 +---- forms/registration/btc.rb | 5 +---- forms/registration/mail.rb | 3 +-- lib/registration.rb | 14 +++++--------- sgx_jmp.rb | 1 - 5 files changed, 8 insertions(+), 20 deletions(-) diff --git a/forms/registration/bch.rb b/forms/registration/bch.rb index 3af32deddbcfffc86ac5ca354ab3d2242f356bd5..e01390d7f07adce0869d8814d9055f23b917c76e 100644 --- a/forms/registration/bch.rb +++ b/forms/registration/bch.rb @@ -13,7 +13,4 @@ field( value: @addr ) -instructions( - "You will received a notification when your payment is complete." \ - "#{@final_message}" -) +instructions "You will received a notification when your payment is complete." diff --git a/forms/registration/btc.rb b/forms/registration/btc.rb index 9451e7681766b01b0f09266c326ecaa6d3fb1706..9228e0eb655805cd81629b47618322d45c9cb00c 100644 --- a/forms/registration/btc.rb +++ b/forms/registration/btc.rb @@ -13,7 +13,4 @@ field( value: @addr ) -instructions( - "You will received a notification when your payment is complete." \ - "#{@final_message}" -) +instructions "You will received a notification when your payment is complete." diff --git a/forms/registration/mail.rb b/forms/registration/mail.rb index f801b0fd76f8622f11759dfbcab596ab1655d695..8020de25516553ab59bfa60f02ae8585a43db7e9 100644 --- a/forms/registration/mail.rb +++ b/forms/registration/mail.rb @@ -5,8 +5,7 @@ instructions( "Activate your account by sending at least " \ "$#{CONFIG[:activation_amount]}\nWe support payment by " \ "postal mail or, in Canada, by Interac e-Transfer.\n\n" \ - "You will receive a notification when your payment is complete." \ - "#{@final_message}" + "You will receive a notification when your payment is complete." ) if @customer_id diff --git a/lib/registration.rb b/lib/registration.rb index 1884cf59dd4e088bc02d5ad35bfdefb87c270893..7c2e23c0dcac3da4f5bc29c5524948ca86a4d2c9 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -279,10 +279,10 @@ class Registration @kinds ||= {} end - def self.for(iq, customer, tel, final_message: nil, finish: Finish) + def self.for(iq, customer, tel, finish: Finish) kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) { raise "Invalid activation method" - }.call(customer, tel, final_message: final_message, finish: finish) + }.call(customer, tel, finish: finish) end class CryptoPaymentMethod @@ -298,11 +298,10 @@ class Registration raise NotImplementedError, "Subclass must implement" end - def initialize(customer, tel, final_message: nil, **) + def initialize(customer, tel, **) @customer = customer @customer_id = customer.customer_id @tel = tel - @final_message = final_message end def save @@ -317,8 +316,7 @@ class Registration FormTemplate.render( reg_form_name, amount: amount, - addr: addr, - final_message: @final_message + addr: addr ) end @@ -537,17 +535,15 @@ class Registration class Mail Payment.kinds[:mail] = method(:new) - def initialize(customer, tel, final_message: nil, **) + def initialize(customer, tel, **) @customer = customer @tel = tel - @final_message = final_message end def form FormTemplate.render( "registration/mail", currency: @customer.currency, - final_message: @final_message, **onboarding_extras ) end diff --git a/sgx_jmp.rb b/sgx_jmp.rb index ca1827c340cc25e6ce4ee09a72cb9a53ca185b91..de932d7c6c8470b49dda58a7078f4433c17644a0 100644 --- a/sgx_jmp.rb +++ b/sgx_jmp.rb @@ -621,7 +621,6 @@ Command.new( customer.save_plan!.then { Registration::Payment.for( iq, customer, customer.registered?.phone, - final_message: PaypalDone::MESSAGE, finish: PaypalDone ) }.then(&:write).catch_only(Command::Execution::FinalStanza) do |s| From 223ee6c8ece43eb81ca2ad70b9638d61f4958694 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Jun 2025 13:12:30 -0500 Subject: [PATCH 03/16] Unify more invite code handling Using the optional field on Activation or the InviteCode flow now use the same code and do the same things. Always check balance and proceed to bill if we've used a parent code, otherwise never do that. Fixes the subaccount-from-onboarding guard as well. --- lib/invites_repo.rb | 4 +- lib/parent_code_repo.rb | 17 +++++ lib/registration.rb | 115 +++++++++++------------------ test/test_helper.rb | 2 +- test/test_registration.rb | 147 +++++++++++++++++++++++++++----------- 5 files changed, 165 insertions(+), 120 deletions(-) diff --git a/lib/invites_repo.rb b/lib/invites_repo.rb index 023a06bb4aad43c5722f3dc63db0c198e99a3d09..a760988eb6143850356ef7f035f6ce5f4c17f1e1 100644 --- a/lib/invites_repo.rb +++ b/lib/invites_repo.rb @@ -133,7 +133,9 @@ protected end def invalid_code(customer_id, code) - @redis.incr("jmp_invite_tries-#{customer_id}").then { + stash_code(customer_id, code).then { + @redis.incr("jmp_invite_tries-#{customer_id}") + }.then { @redis.expire("jmp_invite_tries-#{customer_id}", 60 * 60) }.then { @redis.hexists("jmp_group_codes", code) diff --git a/lib/parent_code_repo.rb b/lib/parent_code_repo.rb index ed92d25103053df67548f787595e53d5ad9269a4..5631a913d8ff5aaed04bad79ed5fb58f1eba5202 100644 --- a/lib/parent_code_repo.rb +++ b/lib/parent_code_repo.rb @@ -7,6 +7,8 @@ require_relative "customer" require_relative "trust_level_repo" class ParentCodeRepo + class Invalid < StandardError; end + def initialize( redis: REDIS, db: DB, @@ -17,6 +19,21 @@ class ParentCodeRepo @trust_level_repo = trust_level_repo end + def claim_code(customer, code) + customer_domain = ProxiedJID.new(customer.jid).unproxied.domain + find(code).then do |parent| + raise Invalid, "Not a valid code" unless parent + + if parent && customer_domain == CONFIG[:onboarding_domain] + raise "Please create a new Jabber ID before creating a subaccount." + end + + plan_name = customer.plan_name + customer = customer.with_plan(plan_name, parent_customer_id: parent) + customer.save_plan!.then { block_given? ? yield(customer) : customer } + end + end + def find(code) @redis.hget("jmp_parent_codes", code).then do |parent_id| trust_level_guard(parent_id).then { parent_id } diff --git a/lib/registration.rb b/lib/registration.rb index 7c2e23c0dcac3da4f5bc29c5524948ca86a4d2c9..c8c1acc6b84e40a6f713e3878a296bca8dc5d54d 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -41,13 +41,6 @@ class Registration end end - def self.guard_onboarding_subaccounts(customer) - customer_domain = ProxiedJID.new(customer.jid).domain - return unless customer_domain == CONFIG[:onboarding_domain] - - raise "Please create a new Jabber ID before creating a subaccount." - end - class Registered def self.for(customer, tel) jid = ProxiedJID.new(customer.jid).unproxied @@ -108,7 +101,6 @@ class Registration def initialize(customer, tel) @customer = customer @tel = tel - @invites = InvitesRepo.new(DB, REDIS) end attr_reader :customer, :tel @@ -125,33 +117,15 @@ class Registration end def next_step(iq) - code = iq.form.field("code")&.value&.to_s - save_customer_plan(iq, code).then { - finish_if_valid_invite(code) - }.catch_only(InvitesRepo::Invalid) do - @invites.stash_code(customer.customer_id, code).then do - Payment.for(iq, @customer, @tel).then(&:write) - end - end - end - - protected - - def finish_if_valid_invite(code) - @invites.claim_code(@customer.customer_id, code) { - @customer.activate_plan_starting_now - }.then do - Finish.new(@customer, @tel).write - end - end - - def save_customer_plan(iq, code) - Registration.guard_onboarding_subaccounts(@customer) - - ParentCodeRepo.new(redis: REDIS, db: DB).find(code).then do |parent| + EMPromise.resolve(nil).then do plan = Plan.for_registration(iq.form.field("plan_name").value.to_s) - @customer = @customer.with_plan(plan.name, parent_customer_id: parent) - @customer.save_plan! + @customer = @customer.with_plan(plan.name) + Registration::Payment::InviteCode.new( + @customer, @tel, finish: Finish, db: DB, redis: REDIS + ).parse(iq, force_save_plan: true) + .catch_only(InvitesRepo::Invalid) do + Payment.for(iq, @customer, @tel).then(&:write) + end end end @@ -440,24 +414,7 @@ class Registration end class InviteCode - Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) } - - def self.for(in_customer, tel, finish: Finish, **) - reload_customer(in_customer).then do |customer| - if customer.balance >= CONFIG[:activation_amount_accept] - next BillPlan.new(customer, tel, finish: finish) - end - - msg = if customer.balance.positive? - "Account balance not enough to cover the activation" - end - new(customer, tel, error: msg, finish: Finish) - end - end - - def self.reload_customer(customer) - Command.execution.customer_repo.find(customer.customer_id) - end + Payment.kinds[:code] = method(:new) FIELDS = [{ var: "code", @@ -466,12 +423,16 @@ class Registration required: true }].freeze - def initialize(customer, tel, error: nil, finish: Finish, **) + def initialize( + customer, tel, + error: nil, finish: Finish, db: DB, redis: REDIS, ** + ) @customer = customer @tel = tel @error = error @finish = finish - @parent_code_repo = ParentCodeRepo.new(redis: REDIS, db: DB) + @invites_repo = InvitesRepo.new(db, redis) + @parent_code_repo = ParentCodeRepo.new(db: db, redis: redis) end def add_form(reply) @@ -486,48 +447,52 @@ class Registration Command.reply { |reply| reply.allowed_actions = [:next, :prev] add_form(reply) - }.then(&method(:parse)) + }.then(&method(:parse)).catch_only(InvitesRepo::Invalid) { |e| + invalid_code(e).write + } end - def parse(iq) + def parse(iq, force_save_plan: false) return Activation.for(@customer, nil, @tel).then(&:write) if iq.prev? - verify(iq.form.field("code")&.value&.to_s) - .catch_only(InvitesRepo::Invalid, &method(:invalid_code)) + verify(iq.form.field("code")&.value&.to_s, force_save_plan) .then(&:write) end protected def invalid_code(e) - InviteCode.new(@customer, @tel, error: e.message) + self.class.new(@customer, @tel, error: e.message, finish: @finish) end def customer_id @customer.customer_id end - def verify(code) - @parent_code_repo.find(code).then do |parent_customer_id| - if parent_customer_id - set_parent(parent_customer_id) - else - InvitesRepo.new(DB, REDIS).claim_code(customer_id, code) { + def verify(code, force_save_plan) + @parent_code_repo.claim_code(@customer, code) { + check_parent_balance + }.catch_only(ParentCodeRepo::Invalid) { + (@customer.save_plan! if force_save_plan).then do + @invites_repo.claim_code(customer_id, code) { @customer.activate_plan_starting_now - }.then { Finish.new(@customer, @tel) } + }.then { @finish.new(@customer, @tel) } end - end + } end - def set_parent(parent_customer_id) - Registration.guard_onboarding_subaccounts(@customer) + def reload_customer + Command.execution.customer_repo.find(@customer.customer_id) + end - @customer = @customer.with_plan( - @customer.plan_name, - parent_customer_id: parent_customer_id - ) - @customer.save_plan!.then do - self.class.for(@customer, @tel, finish: @finish) + def check_parent_balance + reload_customer.then do |customer| + if customer.balance >= CONFIG[:activation_amount_accept] + next BillPlan.new(customer, @tel, finish: @finish) + end + + msg = "Account balance not enough to cover the activation" + invalid_code(RuntimeError.new(msg)) end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index 539f3e5ae84b5135bdc65c7f1cf3d9f9677a77ec..7729ff95c5fcb1373721cc7b1ced347bea51a889 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -304,7 +304,7 @@ class FakeRedis end def hget(key, field) - @values.dig(key, field) + EMPromise.resolve(@values.dig(key, field)) end def hexists(key, field) diff --git a/test/test_registration.rb b/test/test_registration.rb index d510a9000565c06a06a9a132c92dc0a72f5e5814..388ce05fcac268f46ad211e35faaacff0b8c2e45 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -191,6 +191,43 @@ class RegistrationTest < Minitest::Test end em :test_write + def test_write_with_onboarding_jid + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [{ var: "plan_name", value: "test_usd" }] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert_equal( + "You've selected +15555550000 as your JMP number.", + iq.form.instructions.lines.first.chomp + ) + end] + ) + @customer.expect( + :jid, + Blather::JID.new("test\\40onboarding.example.com@proxy") + ) + @customer.expect(:with_plan, @customer) do |*args, **| + assert_equal ["test_usd"], args + end + @customer.expect(:save_plan!, EMPromise.resolve(nil), []) + Registration::Activation::Payment.expect( + :for, + EMPromise.reject(:test_result), + [Blather::Stanza::Iq, @customer, "+15555550000"] + ) + assert_equal( + :test_result, + execute_command { @activation.write.catch { |e| e } } + ) + assert_mock Command::COMMAND_MANAGER + assert_mock @customer + assert_mock Registration::Activation::Payment + end + em :test_write_with_onboarding_jid + def test_write_bad_plan Command::COMMAND_MANAGER.expect( :write, @@ -308,48 +345,52 @@ class RegistrationTest < Minitest::Test em :test_write_with_group_code def test_write_with_parent_code - Command::COMMAND_MANAGER.expect( - :write, - EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| - iq.form.fields = [ - { var: "plan_name", value: "test_usd" }, - { var: "code", value: "PARENT_CODE" } - ] - }), - [Matching.new do |iq| - assert_equal :form, iq.form.type - assert_equal( - "You've selected +15555550000 as your JMP number.", - iq.form.instructions.lines.first.chomp - ) - end] - ) - Registration::Activation::DB.expect( - :query_one, {}, [String, "1"], default: {} - ) - Registration::Activation::DB.expect( - :query_one, { c: 0 }, [String, "1"], default: { c: 0 } - ) - @customer.expect(:with_plan, @customer) do |*args, **kwargs| - assert_equal ["test_usd"], args - assert_equal({ parent_customer_id: "1" }, kwargs) + execute_command do + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [ + { var: "plan_name", value: "test_usd" }, + { var: "code", value: "PARENT_CODE" } + ] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert_equal( + "You've selected +15555550000 as your JMP number.", + iq.form.instructions.lines.first.chomp + ) + end] + ) + Registration::Activation::DB.expect( + :query_one, {}, [String, "1"], default: {} + ) + Registration::Activation::DB.expect( + :query_one, { c: 0 }, [String, "1"], default: { c: 0 } + ) + @customer.expect(:with_plan, @customer) do |*args, **| + assert_equal ["test_usd"], args + end + @customer.expect(:with_plan, @customer) do |*, **kwargs| + assert_equal({ parent_customer_id: "1" }, kwargs) + end + @customer.expect(:save_plan!, EMPromise.resolve(nil), []) + @customer.expect(:balance, 100, []) + Command.execution.customer_repo.expect( + :find, + EMPromise.resolve(@customer), ["test"] + ) + Registration::Payment::InviteCode::BillPlan.expect( + :new, + EMPromise.reject(:test_result) + ) do |*args, **| + assert_equal "+15555550000", args[1] + end + assert_equal( + :test_result, + @activation.write.catch { |e| e }.sync + ) end - @customer.expect(:save_plan!, EMPromise.resolve(nil), []) - Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call } - Registration::Activation::DB.expect( - :exec, - OpenStruct.new(cmd_tuples: 0), - [String, ["test", "PARENT_CODE"]] - ) - Registration::Activation::Payment.expect( - :for, - EMPromise.reject(:test_result), - [Blather::Stanza::Iq, @customer, "+15555550000"] - ) - assert_equal( - :test_result, - execute_command { @activation.write.catch { |e| e } } - ) assert_mock Command::COMMAND_MANAGER assert_mock @customer assert_mock Registration::Activation::Payment @@ -374,9 +415,19 @@ class RegistrationTest < Minitest::Test ) end] ) - @customer.expect(:jid, Blather::JID.new("test@onboarding.example.com")) + Registration::Activation::DB.expect( + :query_one, {}, [String, "1"], default: {} + ) + Registration::Activation::DB.expect( + :query_one, { c: 0 }, [String, "1"], default: { c: 0 } + ) + @customer.expect(:with_plan, @customer, ["test_usd"]) + @customer.expect( + :jid, + Blather::JID.new("test\\40onboarding.example.com@proxy") + ) iq = Blather::Stanza::Iq::Command.new - iq.from = "test@onboarding.example.com" + iq.from = "test\\40onboarding.example.com@proxied" assert_equal( "Please create a new Jabber ID before creating a subaccount.", execute_command(iq) { @activation.write.catch(&:to_s) } @@ -978,6 +1029,11 @@ class RegistrationTest < Minitest::Test def test_write_bad_code result = execute_command do customer = customer(plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :set, + EMPromise.resolve(nil), + ["jmp_customer_pending_invite-test", "abc"] + ) Registration::Payment::InviteCode::REDIS.expect( :get, EMPromise.resolve(0), @@ -1051,6 +1107,11 @@ class RegistrationTest < Minitest::Test def test_write_group_code result = execute_command do customer = customer(plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :set, + EMPromise.resolve(nil), + ["jmp_customer_pending_invite-test", "abc"] + ) Registration::Payment::InviteCode::REDIS.expect( :get, EMPromise.resolve(0), From e38ec7e45184101bb17e57d72fc8ee595a32b400 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 18 Jun 2025 14:35:26 -0500 Subject: [PATCH 04/16] Factor out reload customer, check balance, maybe bill --- lib/registration.rb | 50 ++++++++++++++++++++++++++++----------- test/test_registration.rb | 13 ++++++---- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index c8c1acc6b84e40a6f713e3878a296bca8dc5d54d..554503757e21f71929edd2e0c3ba3b6e8fec8cda 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -364,15 +364,45 @@ class Registration end end - class CreditCard - Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } + class MaybeBill + def initialize(customer, tel, finish: Finish) + @customer = customer + @tel = tel + @finish = finish + end - def self.for(in_customer, tel, finish: Finish, **) - reload_customer(in_customer).then do |customer| + def call + reload_customer.then do |customer| if customer.balance >= CONFIG[:activation_amount_accept] - next BillPlan.new(customer, tel, finish: finish) + next BillPlan.new(customer, @tel, finish: @finish) end + yield customer + end + end + + def reload_customer + EMPromise.resolve(nil).then do + Command.execution.customer_repo.find(@customer.customer_id) + end + end + end + + class JustCharge + def initialize(customer) + @customer = customer + end + + def call + yield @customer + end + end + + class CreditCard + Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } + + def self.for(in_customer, tel, finish: Finish, maybe_bill: MaybeBill, **) + maybe_bill.new(in_customer, tel, finish: finish).call do |customer| new(customer, tel, finish: finish) end end @@ -481,16 +511,8 @@ class Registration } end - def reload_customer - Command.execution.customer_repo.find(@customer.customer_id) - end - def check_parent_balance - reload_customer.then do |customer| - if customer.balance >= CONFIG[:activation_amount_accept] - next BillPlan.new(customer, @tel, finish: @finish) - end - + MaybeBill.new(@customer, @tel, finish: @finish).call do msg = "Account balance not enough to cover the activation" invalid_code(RuntimeError.new(msg)) end diff --git a/test/test_registration.rb b/test/test_registration.rb index 388ce05fcac268f46ad211e35faaacff0b8c2e45..6503c18a9987c680567f57add1d6052d9188ccd8 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -380,7 +380,7 @@ class RegistrationTest < Minitest::Test :find, EMPromise.resolve(@customer), ["test"] ) - Registration::Payment::InviteCode::BillPlan.expect( + Registration::Payment::MaybeBill::BillPlan.expect( :new, EMPromise.reject(:test_result) ) do |*args, **| @@ -395,6 +395,7 @@ class RegistrationTest < Minitest::Test assert_mock @customer assert_mock Registration::Activation::Payment assert_mock Registration::Activation::DB + assert_mock Registration::Payment::MaybeBill::BillPlan end em :test_write_with_parent_code @@ -834,6 +835,10 @@ class RegistrationTest < Minitest::Test cust = Minitest::Mock.new(customer) cust.expect(:balance, 100) cust.expect(:payment_methods, EMPromise.resolve(nil)) + Registration::Payment::MaybeBill::BillPlan.expect( + :new, + Registration::BillPlan.new(nil, nil) + ) { true } execute_command do Command.execution.customer_repo.expect(:find, cust, ["test"]) assert_kind_of( @@ -913,7 +918,7 @@ class RegistrationTest < Minitest::Test Command::COMMAND_MANAGER = Minitest::Mock.new Registration::Payment::InviteCode::Finish = Minitest::Mock.new - Registration::Payment::InviteCode::BillPlan = + Registration::Payment::MaybeBill::BillPlan = Minitest::Mock.new def test_write @@ -965,7 +970,7 @@ class RegistrationTest < Minitest::Test def test_write_parent_code customer = customer(plan_name: "test_usd") - Registration::Payment::InviteCode::BillPlan.expect( + Registration::Payment::MaybeBill::BillPlan.expect( :new, OpenStruct.new(write: nil) ) { |*| true } @@ -1022,7 +1027,7 @@ class RegistrationTest < Minitest::Test assert_mock Command::COMMAND_MANAGER assert_mock Registration::Payment::InviteCode::DB assert_mock Registration::Payment::InviteCode::REDIS - assert_mock Registration::Payment::InviteCode::BillPlan + assert_mock Registration::Payment::MaybeBill::BillPlan end em :test_write_parent_code From 47bf2a23d31a1e020a124ddf1a24e79fe96480e9 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Tue, 17 Jun 2025 11:12:07 -0400 Subject: [PATCH 05/16] two fixes related to eSIM nicks: 1. actually expose nickanem at eSIM order-time 2. pass nickname to `SIMOrder.commit` as non-kw arg in eSIM sublcass --- lib/sim_order.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/sim_order.rb b/lib/sim_order.rb index d1999afdb7c0cc53e40afec4f3f93f1f0c9712e7..ae91fa058f7b31aa51e81e399efa113b1266c67a 100644 --- a/lib/sim_order.rb +++ b/lib/sim_order.rb @@ -119,7 +119,14 @@ protected end def self.fillable_fields - [] + [ + { + type: "text-single", + var: "nickname", + label: "Nickname", + required: false + } + ] end # @param [Blather::Stanza::Iq] iq the stanza @@ -127,7 +134,7 @@ protected def complete(iq) EMPromise.resolve(nil).then { commit( - nickname: iq.form.field("nickname")&.value.presence || self.class.label + iq.form.field("nickname")&.value.presence || self.class.label ) }.then do |sim| ActivationCode.new(sim).complete From bc6541215f89effbd2e1ca5a2f322ed3d8e315d9 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 25 Jun 2025 15:07:16 -0400 Subject: [PATCH 06/16] Remove legacy card charge behaviour We used to have them add card in web then charge after, but these days we charge them in web for this first tx to get 3DS, so this code path is all but dead and we can remove it. --- lib/statsd.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/statsd.rb b/lib/statsd.rb index 5fd77c3fb0c6ef89a7b16bee8dbe7b89bf3fc142..f54a225500aeca335a2366e7309ad69948218d27 100644 --- a/lib/statsd.rb +++ b/lib/statsd.rb @@ -17,10 +17,6 @@ Registration::Payment::Bitcoin.statsd_count :write, "registration.payment.bitcoi Registration::Payment::CreditCard.extend StatsD::Instrument Registration::Payment::CreditCard.statsd_count :write, "registration.payment.credit_card" -Registration::Payment::CreditCard::Activate.extend StatsD::Instrument -Registration::Payment::CreditCard::Activate.statsd_count :write, "registration.payment.credit_card.activate" -Registration::Payment::CreditCard::Activate.statsd_count :declined, "registration.payment.credit_card.activate_declined" - Registration::Payment::InviteCode.extend StatsD::Instrument Registration::Payment::InviteCode.statsd_count :write, "registration.payment.invite_code" From 5851ae1dfe6487860011af5161ce750660c40a85 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Wed, 25 Jun 2025 15:07:20 -0400 Subject: [PATCH 07/16] price functionality on Tns 0 unless LocalInventory, in which case price is fetched from postgres --- lib/tel_selections.rb | 51 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/tel_selections.rb b/lib/tel_selections.rb index 0f6ce10e6dab0febee0ef0951bd9c4d9e7a278a9..48ea30a5c2b5b68c375cbdafe737b17905734ff1 100644 --- a/lib/tel_selections.rb +++ b/lib/tel_selections.rb @@ -155,7 +155,7 @@ class TelSelections full_number: row["tel"].sub(/\A\+1/, ""), city: row["locality"], state: row["region"] - ), row["bandwidth_account_id"]) + ), row["bandwidth_account_id"], price: row["premium_price"]) } } end @@ -215,13 +215,49 @@ class TelSelections def self.for_pending_value(value) if value.start_with?("LocalInventory/") - tel, account = value.sub(/\ALocalInventory\//, "").split("/", 2) - LocalInventory.new(Tn.new(tel), account) + tel, account, price = + value.sub(/\ALocalInventory\//, "").split("/", 3) + LocalInventory.new(Tn.new(tel), account, price: price.to_d) else Bandwidth.new(Tn.new(value)) end end + def price + 0 + end + + # Creates and inserts transaction charging the customer + # for the phone number. If price <= 0 this is a noop. + # This method never checks customer balance. + # + # @param customer [Customer] the customer to charge + def charge(customer) + return if price <= 0 + + transaction(customer).insert + end + + # @param customer [Customer] the customer to charge + def transaction(customer) + Transaction.new( + customer_id: customer.customer_id, + transaction_id: transaction_id(customer), + amount: -price, + note: transaction_note, + ignore_duplicate: false + ) + end + + # @param customer [Customer] the customer to charge + def transaction_id(customer) + "#{customer.customer_id}-bill-#{customer.plan}-at-#{Time.now.to_i}" + end + + def transaction_note + "One-time charge for number: #{formatted_tel}" + end + def initialize(tel) @tel = tel end @@ -286,6 +322,8 @@ class TelSelections end class LocalInventory < SimpleDelegator + attr_reader :price + def self.fetch(tn, db: DB) db.query_defer("SELECT * FROM tel_inventory WHERE tel = $1", [tn]) .then { |rows| @@ -294,18 +332,19 @@ class TelSelections full_number: row["tel"].sub(/\A\+1/, ""), city: row["locality"], state: row["region"] - ), row["bandwidth_account_id"]) + ), row["bandwidth_account_id"], price: row["premium_price"]) } } end - def initialize(tn, bandwidth_account_id) + def initialize(tn, bandwidth_account_id, price: 0) super(tn) @bandwidth_account_id = bandwidth_account_id + @price = price end def pending_value - "LocalInventory/#{tel}/#{@bandwidth_account_id}" + "LocalInventory/#{tel}/#{@bandwidth_account_id}/#{price}" end def reserve(*) From 4ee67a444b167cebe0adedf715e2cd9d1d04488c Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Wed, 25 Jun 2025 15:07:21 -0400 Subject: [PATCH 08/16] two changes related to registration forms - new buy_number.rb form for premium nums - change mail.rb to accept a @price --- forms/registration/mail.rb | 2 +- lib/registration.rb | 17 ++++++++++++++--- test/test_registration.rb | 3 ++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/forms/registration/mail.rb b/forms/registration/mail.rb index 8020de25516553ab59bfa60f02ae8585a43db7e9..c4fe3f76c73e95b0bf7993e803b8731c86c443fb 100644 --- a/forms/registration/mail.rb +++ b/forms/registration/mail.rb @@ -3,7 +3,7 @@ title "Activate by Mail or Interac e-Tranfer" instructions( "Activate your account by sending at least " \ - "$#{CONFIG[:activation_amount]}\nWe support payment by " \ + "$#{'%.2f' % @price}\nWe support payment by " \ "postal mail or, in Canada, by Interac e-Transfer.\n\n" \ "You will receive a notification when your payment is complete." ) diff --git a/lib/registration.rb b/lib/registration.rb index 554503757e21f71929edd2e0c3ba3b6e8fec8cda..510698e9fc8757883f983c8a4ba11fc99d7a9990 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -253,10 +253,10 @@ class Registration @kinds ||= {} end - def self.for(iq, customer, tel, finish: Finish) + def self.for(iq, customer, tel, finish: Finish, maybe_bill: MaybeBill) kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) { raise "Invalid activation method" - }.call(customer, tel, finish: finish) + }.call(customer, tel, finish: finish, maybe_bill: maybe_bill) end class CryptoPaymentMethod @@ -386,6 +386,10 @@ class Registration Command.execution.customer_repo.find(@customer.customer_id) end end + + def self.bill? + true + end end class JustCharge @@ -396,6 +400,10 @@ class Registration def call yield @customer end + + def self.bill? + false + end end class CreditCard @@ -522,15 +530,18 @@ class Registration class Mail Payment.kinds[:mail] = method(:new) - def initialize(customer, tel, **) + def initialize(customer, tel, maybe_bill: MaybeBill, **) @customer = customer @tel = tel + @maybe_bill = maybe_bill end def form + price = @maybe_bill.bill? ? CONFIG[:activation_amount] + @tel.price : @tel.price FormTemplate.render( "registration/mail", currency: @customer.currency, + price: price, **onboarding_extras ) end diff --git a/test/test_registration.rb b/test/test_registration.rb index 6503c18a9987c680567f57add1d6052d9188ccd8..dc3117c738c3cc0ab1a57adc64e2e1b89b59f93a 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -877,9 +877,10 @@ class RegistrationTest < Minitest::Test class MailTest < Minitest::Test def setup + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") @mail = Registration::Payment::Mail.new( customer(plan_name: "test_cad"), - "+15555550000" + @tel ) end From 6bbf900b348aaa6767cff6703bdf7eee00820235 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Wed, 25 Jun 2025 15:07:22 -0400 Subject: [PATCH 09/16] refactor registration_test to use real Tn's --- test/test_registration.rb | 81 +++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/test/test_registration.rb b/test/test_registration.rb index dc3117c738c3cc0ab1a57adc64e2e1b89b59f93a..3662c3d3ad762549497d022810f5fbb951e5376f 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -155,7 +155,8 @@ class RegistrationTest < Minitest::Test def setup @customer = Minitest::Mock.new(customer) - @activation = Registration::Activation.new(@customer, "+15555550000") + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") + @activation = Registration::Activation.new(@customer, @tel) end def test_write @@ -167,7 +168,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -179,7 +180,7 @@ class RegistrationTest < Minitest::Test Registration::Activation::Payment.expect( :for, EMPromise.reject(:test_result), - [Blather::Stanza::Iq, @customer, "+15555550000"] + [Blather::Stanza::Iq, @customer, @tel] ) assert_equal( :test_result, @@ -200,7 +201,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -216,7 +217,7 @@ class RegistrationTest < Minitest::Test Registration::Activation::Payment.expect( :for, EMPromise.reject(:test_result), - [Blather::Stanza::Iq, @customer, "+15555550000"] + [Blather::Stanza::Iq, @customer, @tel] ) assert_equal( :test_result, @@ -237,7 +238,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -263,7 +264,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -282,7 +283,7 @@ class RegistrationTest < Minitest::Test Registration::Activation::Finish.expect( :new, OpenStruct.new(write: EMPromise.reject(:test_result)), - [@customer, "+15555550000"] + [@customer, @tel] ) assert_equal( :test_result, @@ -307,7 +308,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -325,7 +326,7 @@ class RegistrationTest < Minitest::Test Registration::Activation::Payment.expect( :for, EMPromise.reject(:test_result), - [Blather::Stanza::Iq, @customer, "+15555550000"] + [Blather::Stanza::Iq, @customer, @tel] ) assert_equal( :test_result, @@ -357,7 +358,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -384,7 +385,7 @@ class RegistrationTest < Minitest::Test :new, EMPromise.reject(:test_result) ) do |*args, **| - assert_equal "+15555550000", args[1] + assert_equal @tel, args[1] end assert_equal( :test_result, @@ -411,7 +412,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -452,7 +453,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -486,7 +487,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -514,8 +515,9 @@ class RegistrationTest < Minitest::Test Registration::Activation::Allow::DB = Minitest::Mock.new def test_write_credit_to_nil + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") cust = Minitest::Mock.new(customer("test")) - allow = Registration::Activation::Allow.new(cust, "+15555550000", nil) + allow = Registration::Activation::Allow.new(cust, tel, nil) Command::COMMAND_MANAGER.expect( :write, @@ -525,7 +527,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) assert_equal 1, iq.form.fields.length @@ -550,8 +552,9 @@ class RegistrationTest < Minitest::Test def test_write_credit_to_refercust cust = Minitest::Mock.new(customer("test")) + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") allow = Registration::Activation::Allow.new( - cust, "+15555550000", "refercust" + cust, tel, "refercust" ) Command::COMMAND_MANAGER.expect( @@ -562,7 +565,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) assert_equal 1, iq.form.fields.length @@ -597,10 +600,11 @@ class RegistrationTest < Minitest::Test def setup @customer = customer + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") @google_play = Registration::Activation::GooglePlay.new( @customer, "abcd", - "+15555550000" + @tel ) end @@ -613,7 +617,7 @@ class RegistrationTest < Minitest::Test [Matching.new do |iq| assert_equal :form, iq.form.type assert_equal( - "You've selected +15555550000 as your JMP number.", + "You've selected (555) 555-0000 as your JMP number.", iq.form.instructions.lines.first.chomp ) end] @@ -632,7 +636,7 @@ class RegistrationTest < Minitest::Test Registration::Activation::GooglePlay::Finish.expect( :new, OpenStruct.new(write: EMPromise.reject(:test_result)), - [Customer, "+15555550000"] + [Customer, @tel] ) result = execute_command { @google_play.write.catch { |e| e } } assert_equal :test_result, result @@ -648,22 +652,24 @@ class RegistrationTest < Minitest::Test CustomerFinancials::BRAINTREE = Minitest::Mock.new def test_for_bitcoin + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") iq = Blather::Stanza::Iq::Command.new iq.form.fields = [ { var: "activation_method", value: "bitcoin" }, { var: "plan_name", value: "test_usd" } ] - result = Registration::Payment.for(iq, customer, "+15555550000") + result = Registration::Payment.for(iq, customer, tel) assert_kind_of Registration::Payment::Bitcoin, result end def test_for_bch + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") iq = Blather::Stanza::Iq::Command.new iq.form.fields = [ { var: "activation_method", value: "bch" }, { var: "plan_name", value: "test_usd" } ] - result = Registration::Payment.for(iq, customer, "+15555550000") + result = Registration::Payment.for(iq, customer, tel) assert_kind_of Registration::Payment::BCH, result end @@ -699,7 +705,7 @@ class RegistrationTest < Minitest::Test Registration::Payment.for( iq, cust, - "+15555550000" + TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") ) end assert_kind_of Registration::Payment::InviteCode, result @@ -718,9 +724,10 @@ class RegistrationTest < Minitest::Test :add_btc_address, EMPromise.resolve("testaddr") ) + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") @bitcoin = Registration::Payment::Bitcoin.new( @customer, - "+15555550000" + @tel ) end @@ -767,9 +774,10 @@ class RegistrationTest < Minitest::Test :add_bch_address, EMPromise.resolve("testaddr") ) + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") @bch = Registration::Payment::BCH.new( @customer, - "+15555550000" + @tel ) end @@ -806,9 +814,10 @@ class RegistrationTest < Minitest::Test class CreditCardTest < Minitest::Test def setup + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") @credit_card = Registration::Payment::CreditCard.new( customer, - "+15555550000" + @tel ) end @@ -922,6 +931,10 @@ class RegistrationTest < Minitest::Test Registration::Payment::MaybeBill::BillPlan = Minitest::Mock.new + def setup + @tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") + end + def test_write customer = customer(plan_name: "test_usd") Registration::Payment::InviteCode::DB.expect(:transaction, true, []) @@ -930,7 +943,7 @@ class RegistrationTest < Minitest::Test OpenStruct.new(write: nil), [ customer, - "+15555550000" + @tel ] ) execute_command do @@ -959,7 +972,7 @@ class RegistrationTest < Minitest::Test Registration::Payment::InviteCode.new( customer, - "+15555550000" + @tel ).write end assert_mock Command::COMMAND_MANAGER @@ -1022,7 +1035,7 @@ class RegistrationTest < Minitest::Test Registration::Payment::InviteCode.new( customer, - "+15555550000" + @tel ).write end assert_mock Command::COMMAND_MANAGER @@ -1100,7 +1113,7 @@ class RegistrationTest < Minitest::Test Registration::Payment::InviteCode.new( customer, - "+15555550000" + @tel ).write.catch { |e| e } end assert_equal :test_result, result @@ -1178,7 +1191,7 @@ class RegistrationTest < Minitest::Test Registration::Payment::InviteCode.new( customer, - "+15555550000" + @tel ).write.catch { |e| e } end assert_equal :test_result, result @@ -1223,7 +1236,7 @@ class RegistrationTest < Minitest::Test ) Registration::Payment::InviteCode.new( customer, - "+15555550000" + @tel ).write.catch { |e| e } end assert_equal :test_result, result From a03d9a385e7bfdc9631c15ab34e7d7e9df07e7ca Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Wed, 25 Jun 2025 15:07:24 -0400 Subject: [PATCH 10/16] charge for premium nums --- forms/registration/activate.rb | 2 +- lib/registration.rb | 100 +++++++++++++++++------- test/test_registration.rb | 138 +++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 30 deletions(-) diff --git a/forms/registration/activate.rb b/forms/registration/activate.rb index 28b021de17fda7ac05ffe5040f5ea78a63cc956d..95886767e9ebc5ef6d5d57ce7a59a752fa43d813 100644 --- a/forms/registration/activate.rb +++ b/forms/registration/activate.rb @@ -3,7 +3,7 @@ title "Activate JMP" instructions <<~I You've selected #{@tel} as your JMP number. - To activate your account, you can either deposit $#{CONFIG[:activation_amount]} to your balance or enter your referral code if you have one. + To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one. (If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.) I diff --git a/lib/registration.rb b/lib/registration.rb index 510698e9fc8757883f983c8a4ba11fc99d7a9990..abef0d50b40467f3c68abd6f29da578ed25b2edf 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -16,6 +16,12 @@ require_relative "./proxied_jid" require_relative "./tel_selections" require_relative "./welcome_message" +def reload_customer(customer) + EMPromise.resolve(nil).then do + Command.execution.customer_repo.find(customer.customer_id) + end +end + class Registration def self.for(customer, google_play_userid, tel_selections) if (reg = customer.registered?) @@ -272,10 +278,11 @@ class Registration raise NotImplementedError, "Subclass must implement" end - def initialize(customer, tel, **) + def initialize(customer, tel, maybe_bill: MaybeBill, **) @customer = customer @customer_id = customer.customer_id @tel = tel + @maybe_bill = maybe_bill end def save @@ -285,11 +292,9 @@ class Registration attr_reader :customer_id, :tel def form(rate, addr) - amount = CONFIG[:activation_amount] / rate - FormTemplate.render( reg_form_name, - amount: amount, + amount: @maybe_bill.price(@tel) / rate, addr: addr ) end @@ -372,7 +377,7 @@ class Registration end def call - reload_customer.then do |customer| + reload_customer(@customer).then do |customer| if customer.balance >= CONFIG[:activation_amount_accept] next BillPlan.new(customer, @tel, finish: @finish) end @@ -381,19 +386,19 @@ class Registration end end - def reload_customer - EMPromise.resolve(nil).then do - Command.execution.customer_repo.find(@customer.customer_id) - end - end - def self.bill? true end + + # @return [Float] The price of the number + activation fee + # @param [TelSelection::ChooseTel::Tn] tel The phone number to charge + def self.price(tel) + CONFIG[:activation_amount] + tel.price + end end class JustCharge - def initialize(customer) + def initialize(customer, *, **) @customer = customer end @@ -406,25 +411,24 @@ class Registration end end + def self.price(tel) + tel.price + end + class CreditCard Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } def self.for(in_customer, tel, finish: Finish, maybe_bill: MaybeBill, **) maybe_bill.new(in_customer, tel, finish: finish).call do |customer| - new(customer, tel, finish: finish) + new(customer, tel, finish: finish, maybe_bill: maybe_bill) end end - def self.reload_customer(customer) - EMPromise.resolve(nil).then do - Command.execution.customer_repo.find(customer.customer_id) - end - end - - def initialize(customer, tel, finish: Finish) + def initialize(customer, tel, finish: Finish, maybe_bill: MaybeBill) @customer = customer @tel = tel @finish = finish + @maybe_bill = maybe_bill end def oob(reply) @@ -432,7 +436,7 @@ class Registration oob.url = CONFIG[:credit_card_url].call( reply.to.stripped.to_s.gsub("\\", "%5C"), @customer.customer_id - ) + "&amount=#{CONFIG[:activation_amount]}" + ) + "&amount=#{@maybe_bill.price(@tel).ceil}" oob.desc = "Pay by credit card, save, then next here to continue" oob end @@ -446,9 +450,15 @@ class Registration }.then do |iq| next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev? - CreditCard.for(@customer, @tel, finish: @finish).then(&:write) + try_again end end + + def try_again + CreditCard.for( + @customer, @tel, finish: @finish, maybe_bill: @maybe_bill + ).then(&:write) + end end class InviteCode @@ -537,11 +547,10 @@ class Registration end def form - price = @maybe_bill.bill? ? CONFIG[:activation_amount] + @tel.price : @tel.price FormTemplate.render( "registration/mail", currency: @customer.currency, - price: price, + price: @maybe_bill.price(@tel), **onboarding_extras ) end @@ -579,7 +588,9 @@ class Registration def write @customer.bill_plan(note: "Bill #{@tel} for first month").then do - @finish.new(@customer, @tel).write + updated_customer = + @customer.with_balance(@customer.balance - @customer.monthly_price) + @finish.new(updated_customer, @tel).write end end end @@ -592,12 +603,42 @@ class Registration end def write - @tel.order(DB, @customer).then( - ->(_) { customer_active_tel_purchased }, - method(:number_purchase_error) + if @customer.balance >= @tel.price + @tel.order(DB, @customer).then( + ->(_) { customer_active_tel_purchased }, + method(:number_purchase_error) + ) + else + buy_number.then { + try_again + }.then(&:write) + end + end + + def try_again + reload_customer(@customer).then do |customer| + Finish.new(customer, @tel) + end + end + + def form + FormTemplate.render( + "registration/buy_number", + tel: @tel ) end + def buy_number + Command.reply { |reply| + reply.command << form + }.then { |iq| + Payment.for( + iq, @customer, @tel, + maybe_bill: ::Registration::Payment::JustCharge + ).write + } + end + protected def number_purchase_error(e) @@ -646,7 +687,8 @@ class Registration EMPromise.all([ TEL_SELECTIONS.delete(@customer.jid), put_default_fwd, - use_referral_code + use_referral_code, + @tel.charge(@customer) ]) }.then do FinishOnboarding.for(@customer, @tel).then(&:write) diff --git a/test/test_registration.rb b/test/test_registration.rb index 3662c3d3ad762549497d022810f5fbb951e5376f..cff75a805e7df045fde7593f22d8a9a538a9218a 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -1684,6 +1684,144 @@ class RegistrationTest < Minitest::Test end em :test_write_local_inventory + def test_write_local_inventory_must_pay + low_cust = customer( + sgx: @sgx, + jid: Blather::JID.new("test\\40onboarding.example.com@proxy") + ).with_balance(5.0) + high_cust = customer( + sgx: @sgx, + jid: Blather::JID.new("test\\40onboarding.example.com@proxy") + ).with_balance(100.0) + + stub_request( + :post, + "https://dashboard.bandwidth.com/v1.0/accounts/moveto/moveTns" + ).with( + body: { + CustomerOrderId: "test", + SourceAccountId: "bandwidth_account_id", + SiteId: "test_site", + SipPeerId: "test_peer", + TelephoneNumbers: { TelephoneNumber: "5555550000" } + }.to_xml(indent: 0, root: "MoveTnsOrder") + ).to_return(status: 200, body: "", headers: {}) + + Registration::Finish::REDIS.expect( + :get, + nil, + ["jmp_customer_pending_invite-test"] + ) + Registration::Finish::REDIS.expect( + :del, + nil, + ["jmp_customer_pending_invite-test"] + ) + Registration::Finish::REDIS.expect( + :hget, + nil, + ["jmp_group_codes", nil] + ) + Registration::Finish::DB.expect( + :exec_defer, + EMPromise.resolve(OpenStruct.new(cmd_tuples: 1)), + [String, ["+15555550000"]] + ) + Bwmsgsv2Repo::REDIS.expect( + :get, + EMPromise.resolve(nil), + ["jmp_customer_backend_sgx-test"] + ) + Bwmsgsv2Repo::REDIS.expect( + :set, + nil, + [ + "catapult_fwd-+15555550000", + "xmpp:test\\40onboarding.example.com@proxy" + ] + ) + Bwmsgsv2Repo::REDIS.expect( + :set, + nil, + ["catapult_fwd_timeout-customer_test@component", 25] + ) + + local_tel = TelSelections::ChooseTel::Tn::LocalInventory.new( + TelSelections::ChooseTel::Tn.new("+15555550000"), + "bandwidth_account_id", + price: 10.0 + ) + + Registration::Payment::CreditCard.stub( + :new, + OpenStruct.new( + write: lambda { + # simulates successful try_again + Registration::Payment::CreditCard.for( + high_cust, + local_tel, + # we know maybe_bill will be passed as JustCharge + # since that's hardcoded + maybe_bill: Registration::Payment::JustCharge + ) + } + ) + ) do + result = execute_command do + @sgx.expect( + :register!, + EMPromise.resolve(@sgx.with( + registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr| + ibr.phone = "+15555550000" + end + )), + ["+15555550000"] + ) + + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [ + { var: "activation_method", value: "credit_card" }, + { var: "plan_name", value: "test_usd" } + ] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert iq.form.field("activation_method") + assert iq.form.field("plan_name") + end] + ) + + Command.execution.customer_repo.expect( + :find, + EMPromise.resolve(high_cust), + ["test"] + ) + + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.reject(:test_result), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert iq.form.field("subdomain") + end] + ) + + Registration::Finish.new( + low_cust, + local_tel + ).write.catch { |e| e } + end + assert_equal :test_result, result + assert_mock @sgx + assert_mock Registration::Finish::REDIS + assert_mock Bwmsgsv2Repo::REDIS + assert_mock Command::COMMAND_MANAGER + end + end + em :test_write_local_inventory_must_pay + def test_write_tn_fail create_order = stub_request( :post, From bdc86ed98b1f2c1cc40cc3d878ec529830cf62da Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 10:50:11 -0500 Subject: [PATCH 11/16] Fix test --- test/test_registration.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_registration.rb b/test/test_registration.rb index cff75a805e7df045fde7593f22d8a9a538a9218a..0cd8572fd5e9b39e90fb0f5f056bcf0339291a04 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -1782,14 +1782,12 @@ class RegistrationTest < Minitest::Test :write, EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| iq.form.fields = [ - { var: "activation_method", value: "credit_card" }, - { var: "plan_name", value: "test_usd" } + { var: "activation_method", value: "credit_card" } ] }), [Matching.new do |iq| assert_equal :form, iq.form.type assert iq.form.field("activation_method") - assert iq.form.field("plan_name") end] ) From cafd02f831d644893d857aed231ccaccd83811bd Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 10:50:24 -0500 Subject: [PATCH 12/16] Remove unused method And move price method where it actually goes --- lib/registration.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index abef0d50b40467f3c68abd6f29da578ed25b2edf..368273481520a4e5485118491f8fa292b2ea8dde 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -386,10 +386,6 @@ class Registration end end - def self.bill? - true - end - # @return [Float] The price of the number + activation fee # @param [TelSelection::ChooseTel::Tn] tel The phone number to charge def self.price(tel) @@ -406,15 +402,11 @@ class Registration yield @customer end - def self.bill? - false + def self.price(tel) + tel.price end end - def self.price(tel) - tel.price - end - class CreditCard Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } From 0dd26e5db553c3c8295defec295f78de08879e9e Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 10:50:43 -0500 Subject: [PATCH 13/16] Inline useless methods And use a better transaction id --- lib/tel_selections.rb | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/tel_selections.rb b/lib/tel_selections.rb index 48ea30a5c2b5b68c375cbdafe737b17905734ff1..7e1f3bb17506b5e39fbbd908e2a9396a01e132bd 100644 --- a/lib/tel_selections.rb +++ b/lib/tel_selections.rb @@ -242,22 +242,14 @@ class TelSelections def transaction(customer) Transaction.new( customer_id: customer.customer_id, - transaction_id: transaction_id(customer), + transaction_id: + "#{customer.customer_id}-bill-#{@tel}-at-#{Time.now.to_i}", amount: -price, - note: transaction_note, + note: "One-time charge for number: #{formatted_tel}", ignore_duplicate: false ) end - # @param customer [Customer] the customer to charge - def transaction_id(customer) - "#{customer.customer_id}-bill-#{customer.plan}-at-#{Time.now.to_i}" - end - - def transaction_note - "One-time charge for number: #{formatted_tel}" - end - def initialize(tel) @tel = tel end From c80b2e21e06982b1ea6b346d5503e4a9bd122f47 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 11:47:45 -0500 Subject: [PATCH 14/16] Bill after charge instead of this blind retry --- lib/registration.rb | 29 ++----- test/test_registration.rb | 156 ++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 113 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index 368273481520a4e5485118491f8fa292b2ea8dde..cd2306540e3287ee39705a4df5a706e0208b9f73 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -398,9 +398,7 @@ class Registration @customer = customer end - def call - yield @customer - end + def call; end def self.price(tel) tel.price @@ -408,19 +406,14 @@ class Registration end class CreditCard - Payment.kinds[:credit_card] = ->(*args, **kw) { self.for(*args, **kw) } - - def self.for(in_customer, tel, finish: Finish, maybe_bill: MaybeBill, **) - maybe_bill.new(in_customer, tel, finish: finish).call do |customer| - new(customer, tel, finish: finish, maybe_bill: maybe_bill) - end - end + Payment.kinds[:credit_card] = method(:new) def initialize(customer, tel, finish: Finish, maybe_bill: MaybeBill) @customer = customer @tel = tel @finish = finish - @maybe_bill = maybe_bill + @maybe_bill = maybe_bill.new(customer, tel, finish: finish) + @price = maybe_bill.price(tel) end def oob(reply) @@ -428,7 +421,7 @@ class Registration oob.url = CONFIG[:credit_card_url].call( reply.to.stripped.to_s.gsub("\\", "%5C"), @customer.customer_id - ) + "&amount=#{@maybe_bill.price(@tel).ceil}" + ) + "&amount=#{@price.ceil}" oob.desc = "Pay by credit card, save, then next here to continue" oob end @@ -442,15 +435,9 @@ class Registration }.then do |iq| next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev? - try_again + @maybe_bill.call { self }&.then(&:write) end end - - def try_again - CreditCard.for( - @customer, @tel, finish: @finish, maybe_bill: @maybe_bill - ).then(&:write) - end end class InviteCode @@ -627,8 +614,8 @@ class Registration Payment.for( iq, @customer, @tel, maybe_bill: ::Registration::Payment::JustCharge - ).write - } + ) + }.then(&:write) end protected diff --git a/test/test_registration.rb b/test/test_registration.rb index 0cd8572fd5e9b39e90fb0f5f056bcf0339291a04..79246f852eb013ce841276a2c68e2c4c4f54524b 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -674,6 +674,7 @@ class RegistrationTest < Minitest::Test end def test_for_credit_card + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") iq = Blather::Stanza::Iq::Command.new iq.from = "test@example.com" iq.form.fields = [ @@ -686,8 +687,8 @@ class RegistrationTest < Minitest::Test Registration::Payment.for( iq, cust, - "" - ).sync + tel + ) end assert_kind_of Registration::Payment::CreditCard, result end @@ -821,7 +822,8 @@ class RegistrationTest < Minitest::Test ) end - def test_for + def test_new + tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000") cust = Minitest::Mock.new(customer) cust.expect( :payment_methods, @@ -831,35 +833,11 @@ class RegistrationTest < Minitest::Test Command.execution.customer_repo.expect(:find, cust, ["test"]) assert_kind_of( Registration::Payment::CreditCard, - Registration::Payment::CreditCard.for( - cust, - "+15555550000" - ).sync + Registration::Payment::CreditCard.new(cust, tel) ) end end - em :test_for - - def test_for_has_balance - cust = Minitest::Mock.new(customer) - cust.expect(:balance, 100) - cust.expect(:payment_methods, EMPromise.resolve(nil)) - Registration::Payment::MaybeBill::BillPlan.expect( - :new, - Registration::BillPlan.new(nil, nil) - ) { true } - execute_command do - Command.execution.customer_repo.expect(:find, cust, ["test"]) - assert_kind_of( - Registration::BillPlan, - Registration::Payment::CreditCard.for( - cust, - "+15555550000" - ).sync - ) - end - end - em :test_for_has_balance + em :test_new def test_write result = execute_command do @@ -1746,77 +1724,71 @@ class RegistrationTest < Minitest::Test ["catapult_fwd_timeout-customer_test@component", 25] ) - local_tel = TelSelections::ChooseTel::Tn::LocalInventory.new( - TelSelections::ChooseTel::Tn.new("+15555550000"), - "bandwidth_account_id", - price: 10.0 + local_tel = Minitest::Mock.new( + TelSelections::ChooseTel::Tn::LocalInventory.new( + TelSelections::ChooseTel::Tn.new("+15555550000"), + "bandwidth_account_id", + price: 10.0 + ) ) - Registration::Payment::CreditCard.stub( - :new, - OpenStruct.new( - write: lambda { - # simulates successful try_again - Registration::Payment::CreditCard.for( - high_cust, - local_tel, - # we know maybe_bill will be passed as JustCharge - # since that's hardcoded - maybe_bill: Registration::Payment::JustCharge - ) - } + result = execute_command do + local_tel.expect(:charge, EMPromise.reject(:test_result), [Customer]) + @sgx.expect( + :register!, + EMPromise.resolve(@sgx.with( + registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr| + ibr.phone = "+15555550000" + end + )), + ["+15555550000"] ) - ) do - result = execute_command do - @sgx.expect( - :register!, - EMPromise.resolve(@sgx.with( - registered?: Blather::Stanza::Iq::IBR.new.tap do |ibr| - ibr.phone = "+15555550000" - end - )), - ["+15555550000"] - ) - Command::COMMAND_MANAGER.expect( - :write, - EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| - iq.form.fields = [ - { var: "activation_method", value: "credit_card" } - ] - }), - [Matching.new do |iq| - assert_equal :form, iq.form.type - assert iq.form.field("activation_method") - end] - ) + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| + iq.from = "customer@example.org" + iq.form.fields = [ + { var: "activation_method", value: "credit_card" } + ] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert_equal "Purchase Number", iq.form.title + end] + ) - Command.execution.customer_repo.expect( - :find, - EMPromise.resolve(high_cust), - ["test"] - ) + Command.execution.customer_repo.expect( + :find, + EMPromise.resolve(high_cust), + ["test"] + ) - Command::COMMAND_MANAGER.expect( - :write, - EMPromise.reject(:test_result), - [Matching.new do |iq| - assert_equal :form, iq.form.type - assert iq.form.field("subdomain") - end] - ) + Command::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq| + iq.from = "customer@example.org" + }), + [Matching.new do |iq| + assert_equal( + "Pay by credit card, save, then next here to continue: " \ + "http://creditcard.example.com?&amount=10", + iq.note.text + ) + end] + ) - Registration::Finish.new( - low_cust, - local_tel - ).write.catch { |e| e } - end - assert_equal :test_result, result - assert_mock @sgx - assert_mock Registration::Finish::REDIS - assert_mock Bwmsgsv2Repo::REDIS - assert_mock Command::COMMAND_MANAGER + Registration::Finish.new( + low_cust, + local_tel + ).write.catch { |e| e } end + + assert_equal :test_result, result + assert_mock @sgx + assert_mock Registration::Finish::REDIS + assert_mock Bwmsgsv2Repo::REDIS + assert_mock Command::COMMAND_MANAGER end em :test_write_local_inventory_must_pay From 6675b96cdbb66ea0186969c1f70cdf1d3a75f9c9 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 12:00:21 -0500 Subject: [PATCH 15/16] Inject price Since that's what we actually care about. Also makes JustCharge more generic for future use --- lib/registration.rb | 49 ++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index cd2306540e3287ee39705a4df5a706e0208b9f73..8d0e4b94cf5c89047dba5afdb21134b3931d6caf 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -259,10 +259,16 @@ class Registration @kinds ||= {} end - def self.for(iq, customer, tel, finish: Finish, maybe_bill: MaybeBill) + def self.for( + iq, customer, tel, + finish: Finish, maybe_bill: MaybeBill, + price: CONFIG[:activation_amount] + tel.price + ) kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) { raise "Invalid activation method" - }.call(customer, tel, finish: finish, maybe_bill: maybe_bill) + }.call( + customer, tel, finish: finish, maybe_bill: maybe_bill, price: price + ) end class CryptoPaymentMethod @@ -278,11 +284,14 @@ class Registration raise NotImplementedError, "Subclass must implement" end - def initialize(customer, tel, maybe_bill: MaybeBill, **) + def initialize( + customer, tel, + price: CONFIG[:activation_amount] + tel.price, ** + ) @customer = customer @customer_id = customer.customer_id @tel = tel - @maybe_bill = maybe_bill + @price = price end def save @@ -294,7 +303,7 @@ class Registration def form(rate, addr) FormTemplate.render( reg_form_name, - amount: @maybe_bill.price(@tel) / rate, + amount: @price / rate, addr: addr ) end @@ -385,12 +394,6 @@ class Registration yield customer end end - - # @return [Float] The price of the number + activation fee - # @param [TelSelection::ChooseTel::Tn] tel The phone number to charge - def self.price(tel) - CONFIG[:activation_amount] + tel.price - end end class JustCharge @@ -399,21 +402,21 @@ class Registration end def call; end - - def self.price(tel) - tel.price - end end class CreditCard Payment.kinds[:credit_card] = method(:new) - def initialize(customer, tel, finish: Finish, maybe_bill: MaybeBill) + def initialize( + customer, tel, + finish: Finish, maybe_bill: MaybeBill, + price: CONFIG[:activation_amount] + tel.price + ) @customer = customer @tel = tel @finish = finish @maybe_bill = maybe_bill.new(customer, tel, finish: finish) - @price = maybe_bill.price(tel) + @price = price end def oob(reply) @@ -519,17 +522,20 @@ class Registration class Mail Payment.kinds[:mail] = method(:new) - def initialize(customer, tel, maybe_bill: MaybeBill, **) + def initialize( + customer, tel, + price: CONFIG[:activation_amount] + tel.price, ** + ) @customer = customer @tel = tel - @maybe_bill = maybe_bill + @price = price end def form FormTemplate.render( "registration/mail", currency: @customer.currency, - price: @maybe_bill.price(@tel), + price: @price, **onboarding_extras ) end @@ -613,7 +619,8 @@ class Registration }.then { |iq| Payment.for( iq, @customer, @tel, - maybe_bill: ::Registration::Payment::JustCharge + maybe_bill: ::Registration::Payment::JustCharge, + price: @tel.price ) }.then(&:write) end From d9f0ee55dd0cf169dbb64cc48ad1fd8e14b3ebc3 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 1 Jul 2025 12:09:27 -0500 Subject: [PATCH 16/16] Factor out BuyNumber --- lib/registration.rb | 65 ++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/lib/registration.rb b/lib/registration.rb index 8d0e4b94cf5c89047dba5afdb21134b3931d6caf..274b9ecc578c953ea7bc9066141cf6743651fd39 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -580,53 +580,58 @@ class Registration end end - class Finish + class BuyNumber def initialize(customer, tel) @customer = customer @tel = tel - @invites = InvitesRepo.new(DB, REDIS) end def write - if @customer.balance >= @tel.price - @tel.order(DB, @customer).then( - ->(_) { customer_active_tel_purchased }, - method(:number_purchase_error) + Command.reply { |reply| + reply.command << FormTemplate.render( + "registration/buy_number", + tel: @tel ) - else - buy_number.then { - try_again - }.then(&:write) - end + }.then(&method(:parse)).then(&:write) end - def try_again - reload_customer(@customer).then do |customer| - Finish.new(customer, @tel) - end - end + protected - def form - FormTemplate.render( - "registration/buy_number", - tel: @tel + def parse(iq) + Payment.for( + iq, @customer, @tel, + maybe_bill: ::Registration::Payment::JustCharge, + price: @tel.price ) end + end - def buy_number - Command.reply { |reply| - reply.command << form - }.then { |iq| - Payment.for( - iq, @customer, @tel, - maybe_bill: ::Registration::Payment::JustCharge, - price: @tel.price - ) - }.then(&:write) + class Finish + def initialize(customer, tel) + @customer = customer + @tel = tel + @invites = InvitesRepo.new(DB, REDIS) + end + + def write + return buy_number if @customer.balance < @tel.price + + @tel.order(DB, @customer).then( + ->(_) { customer_active_tel_purchased }, + method(:number_purchase_error) + ) end protected + def buy_number + BuyNumber.new(@customer, @tel).write.then do + reload_customer(@customer).then { |customer| + Finish.new(customer, @tel) + }.then(&:write) + end + end + def number_purchase_error(e) Command.log.error "number_purchase_error", e TEL_SELECTIONS.delete(@customer.jid).then {