diff --git a/forms/registration/activate.rb b/forms/registration/activate.rb index cbd8e542190bce3309fc20067c9abc4bb97cea62..0cf8a0acc73bb935d1c5f8e1ac5f647ed4bbfe9f 100644 --- a/forms/registration/activate.rb +++ b/forms/registration/activate.rb @@ -34,3 +34,9 @@ field( ) instance_eval File.read("#{__dir__}/plan_name.rb") + +field( + var: "code", + type: "text-single", + label: "Optional referral code" +) diff --git a/lib/invites_repo.rb b/lib/invites_repo.rb index fd4ba71f50367fdf728ef60366538147dc8dc97e..1dc4b71712dfbe824e5eaedf25bfc5ef858591ad 100644 --- a/lib/invites_repo.rb +++ b/lib/invites_repo.rb @@ -15,13 +15,23 @@ class InvitesRepo promise.then { |result| result.map { |row| row["code"] } } end + def stash_code(customer_id, code) + return EMPromise.resolve(nil) if code.to_s.strip == "" + + @redis.set("jmp_customer_pending_invite-#{customer_id}", code) + end + + CLAIM_SQL = <<~SQL + UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP + WHERE code=$2 AND used_by_id IS NULL + SQL + def claim_code(customer_id, code, &blk) + raise Invalid, "No code provided" if code.to_s.strip == "" + guard_too_many_tries(customer_id).then do @db.transaction do - valid = @db.exec(<<~SQL, [customer_id, code]).cmd_tuples.positive? - UPDATE invites SET used_by_id=$1, used_at=LOCALTIMESTAMP - WHERE code=$2 AND used_by_id IS NULL - SQL + valid = @db.exec(CLAIM_SQL, [customer_id, code]).cmd_tuples.positive? invalid_code(customer_id, code).sync unless valid blk.call diff --git a/lib/registration.rb b/lib/registration.rb index 5b26ac77f58842516c59d633c5c38ba0415bf93e..22d078f333ce2a5a36ca3a442893182c15adf14c 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -87,6 +87,7 @@ class Registration def initialize(customer, tel) @customer = customer @tel = tel + @invites = InvitesRepo.new(DB, REDIS) end attr_reader :customer, :tel @@ -109,13 +110,32 @@ class Registration end def next_step(iq) - EMPromise.resolve(nil).then { - Payment.for(iq, customer, tel) - }.then(&:write) + code = iq.form.field("code")&.value&.to_s + save_customer_plan(iq).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) + plan_name = iq.form.field("plan_name").value.to_s + @customer = @customer.with_plan(plan_name) + @customer.save_plan! + end + def rate_center EM.promise_fiber { center = BandwidthIris::Tn.get(tel).get_rate_center @@ -128,6 +148,7 @@ class Registration @customer = customer @google_play_userid = google_play_userid @tel = tel + @invites = InvitesRepo.new(DB, REDIS) end def used @@ -160,18 +181,18 @@ class Registration @customer = @customer.with_plan(plan_name) @customer.activate_plan_starting_now }.then do - if iq.form.field("code") - use_referral_code(iq.form.field("code").value.to_s) - end + use_referral_code(iq.form.field("code")&.value&.to_s) end end protected def use_referral_code(code) - InvitesRepo.new.claim_code(@customer.customer_id, code) { + @invites.claim_code(@customer.customer_id, code) { @customer.extend_plan - }.catch_only(InvitesRepo::Invalid) { nil } + }.catch_only(InvitesRepo::Invalid) do + @invites.stash_code(customer.customer_id, code) + end end end @@ -225,13 +246,9 @@ class Registration end def self.for(iq, customer, tel, final_message: nil, finish: Finish) - plan_name = iq.form.field("plan_name").value.to_s - customer = customer.with_plan(plan_name) - customer.save_plan!.then do - 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) - end + 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) end class Bitcoin diff --git a/test/test_registration.rb b/test/test_registration.rb index 7e739206e310b3643bd04e674cf80a23b4600888..1bf384d0c81cfce31bdfb3405c4978f792bdd965 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -103,9 +103,15 @@ class RegistrationTest < Minitest::Test em :test_for_not_activated_with_customer_id class ActivationTest < Minitest::Test + Registration::Activation::DB = Minitest::Mock.new + Registration::Activation::REDIS = FakeRedis.new + Registration::Activation::Payment = Minitest::Mock.new + Registration::Activation::Finish = Minitest::Mock.new Command::COMMAND_MANAGER = Minitest::Mock.new + def setup - @activation = Registration::Activation.new("test", "+15555550000") + @customer = Minitest::Mock.new(customer) + @activation = Registration::Activation.new(@customer, "+15555550000") end def test_write @@ -130,7 +136,9 @@ class RegistrationTest < Minitest::Test RESPONSE Command::COMMAND_MANAGER.expect( :write, - EMPromise.reject(:test_result), + 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( @@ -139,13 +147,149 @@ class RegistrationTest < Minitest::Test ) end] ) + @customer.expect(:with_plan, @customer, ["test_usd"]) + @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 + + def test_write_with_code + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/+15555550000" + ).to_return(status: 201, body: <<~RESPONSE) + + 5555550000 + + RESPONSE + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/5555550000/ratecenter" + ).to_return(status: 201, body: <<~RESPONSE) + + + KE + FA + + + RESPONSE + 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: "123" } + ] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert_equal( + "You've selected +15555550000 (FA, KE) as your JMP number.", + iq.form.instructions.lines.first.chomp + ) + end] + ) + @customer.expect(:with_plan, @customer, ["test_usd"]) + @customer.expect(:save_plan!, EMPromise.resolve(nil), []) + @customer.expect(:activate_plan_starting_now, EMPromise.resolve(nil), []) + Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call } + Registration::Activation::DB.expect( + :exec, + OpenStruct.new(cmd_tuples: 1), + [String, ["test", "123"]] + ) + Registration::Activation::Finish.expect( + :new, + OpenStruct.new(write: EMPromise.reject(:test_result)), + [@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 + assert_mock Registration::Activation::DB + end + em :test_write_with_code + + def test_write_with_group_code + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/+15555550000" + ).to_return(status: 201, body: <<~RESPONSE) + + 5555550000 + + RESPONSE + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/5555550000/ratecenter" + ).to_return(status: 201, body: <<~RESPONSE) + + + KE + FA + + + RESPONSE + 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: "123" } + ] + }), + [Matching.new do |iq| + assert_equal :form, iq.form.type + assert_equal( + "You've selected +15555550000 (FA, KE) as your JMP number.", + iq.form.instructions.lines.first.chomp + ) + end] + ) + @customer.expect(:with_plan, @customer, ["test_usd"]) + @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", "123"]] + ) + 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_equal( + "123", + Registration::Activation::REDIS.get( + "jmp_customer_pending_invite-test" + ).sync + ) + assert_mock Command::COMMAND_MANAGER + assert_mock @customer + assert_mock Registration::Activation::Payment + assert_mock Registration::Activation::DB + end + em :test_write_with_group_code end class AllowTest < Minitest::Test @@ -272,23 +416,16 @@ class RegistrationTest < Minitest::Test CustomerFinancials::BRAINTREE = Minitest::Mock.new def test_for_bitcoin - cust = Minitest::Mock.new(customer) - cust.expect(:with_plan, cust, ["test_usd"]) - cust.expect(:save_plan!, nil) 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, cust, "+15555550000") + result = Registration::Payment.for(iq, customer, "+15555550000") assert_kind_of Registration::Payment::Bitcoin, result - assert_mock cust end def test_for_credit_card - cust = Minitest::Mock.new(customer) - cust.expect(:with_plan, cust, ["test_usd"]) - cust.expect(:save_plan!, nil) braintree_customer = Minitest::Mock.new CustomerFinancials::BRAINTREE.expect( :customer, @@ -306,6 +443,7 @@ class RegistrationTest < Minitest::Test { var: "activation_method", value: "credit_card" }, { var: "plan_name", value: "test_usd" } ] + cust = customer result = execute_command do Command.execution.customer_repo.expect(:find, cust, ["test"]) Registration::Payment.for( @@ -315,14 +453,10 @@ class RegistrationTest < Minitest::Test ).sync end assert_kind_of Registration::Payment::CreditCard, result - assert_mock cust end em :test_for_credit_card def test_for_code - cust = Minitest::Mock.new(customer) - cust.expect(:with_plan, cust, ["test_usd"]) - cust.expect(:save_plan!, nil) iq = Blather::Stanza::Iq::Command.new iq.form.fields = [ { var: "activation_method", value: "code" }, @@ -330,11 +464,10 @@ class RegistrationTest < Minitest::Test ] result = Registration::Payment.for( iq, - cust, + customer, "+15555550000" ) assert_kind_of Registration::Payment::InviteCode, result - assert_mock cust end class BitcoinTest < Minitest::Test