diff --git a/Gemfile b/Gemfile index 79f362f548cc8000e1c3b468c1336d7a10e3199d..5152fb80a32e811acf27d3d3de4b4da4dfded5a7 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ gem "em-hiredis" gem "em-http-request" gem "em-pg-client", git: "https://github.com/royaltm/ruby-em-pg-client" gem "em-synchrony" -gem "em_promise.rb" +gem "em_promise.rb", "~> 0.0.2" gem "eventmachine" gem "money-open-exchange-rates" gem "ruby-bandwidth-iris" diff --git a/lib/customer.rb b/lib/customer.rb index d9fc3cabe1edabe6fb9172f2246408b60eb324fd..f0115a195f40d573e2daa6fb51758a04ed0a0816 100644 --- a/lib/customer.rb +++ b/lib/customer.rb @@ -61,6 +61,15 @@ class Customer end end + def activate_plan_starting_now + DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive? + INSERT INTO plan_log + (customer_id, plan_name, date_range) + VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')) + ON CONFLICT DO NOTHING + SQL + end + def payment_methods @payment_methods ||= BRAINTREE @@ -96,15 +105,6 @@ protected SQL end - def activate_plan_starting_now - DB.exec(<<~SQL, [@customer_id, plan_name]).cmd_tuples.positive? - INSERT INTO plan_log - (customer_id, plan_name, date_range) - VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month')) - ON CONFLICT DO NOTHING - SQL - end - def add_one_month_to_current_plan DB.exec(<<~SQL, [@customer_id]) UPDATE plan_log SET date_range=range_merge( diff --git a/lib/registration.rb b/lib/registration.rb index 6ae4ed46200b52f7a8e7fb49f13a05c81d622f34..89355050b140bfcc21f17de057d4c4d9f42b9608 100644 --- a/lib/registration.rb +++ b/lib/registration.rb @@ -73,7 +73,7 @@ class Registration }, { value: "code", - label: "Referral or Activation Code" + label: "Invite Code" } ] }, @@ -286,6 +286,77 @@ class Registration end end end + + class InviteCode + Payment.kinds[:code] = method(:new) + + class Invalid < StandardError; end + + FIELDS = [{ + var: "code", + type: "text-single", + label: "Your invite code", + required: true + }].freeze + + def initialize(iq, customer, tel, error: nil) + @customer = customer + @tel = tel + @reply = iq.reply + @reply.allowed_actions = [:next] + @form = @reply.form + @form.type = :form + @form.title = "Enter Invite Code" + @form.instructions = error + @form.fields = FIELDS + end + + def write + COMMAND_MANAGER.write(@reply).then do |iq| + guard_too_many_tries.then { + verify(iq.form.field("code")&.value&.to_s) + }.then { + Finish.new(iq, @customer, @tel) + }.catch_only(Invalid) { |e| + invalid_code(iq, e) + }.then(&:write) + end + end + + protected + + def guard_too_many_tries + REDIS.get("jmp_invite_tries-#{@customer.customer_id}").then do |t| + raise Invalid, "Too many wrong attempts" if t > 10 + end + end + + def invalid_code(iq, e) + EMPromise.all([ + REDIS.incr("jmp_invite_tries-#{@customer.customer_id}").then do + REDIS.expire("jmp_invite_tries-#{@customer.customer_id}", 60 * 60) + end, + InviteCode.new(iq, @customer, @tel, error: e.message) + ]).then(&:last) + end + + def customer_id + @customer.customer_id + end + + def verify(code) + EM.promise_fiber 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 + raise Invalid, "Not a valid invite code: #{code}" unless valid + @customer.activate_plan_starting_now + end + end + end + end end class Finish diff --git a/schemas b/schemas index e005a4d6b09636d21614be0c513ce9360cef2ccb..1bef640493ff0409838c71e72dd105fb61473cb5 160000 --- a/schemas +++ b/schemas @@ -1 +1 @@ -Subproject commit e005a4d6b09636d21614be0c513ce9360cef2ccb +Subproject commit 1bef640493ff0409838c71e72dd105fb61473cb5 diff --git a/test/test_registration.rb b/test/test_registration.rb index c1a017bb06e550424f4ef54f57ff1c7dede56343..43386545739afac5614e5e70a5988abc13d27c77 100644 --- a/test/test_registration.rb +++ b/test/test_registration.rb @@ -154,14 +154,17 @@ class RegistrationTest < Minitest::Test em :test_for_credit_card def test_for_code - skip "Code not implemented yet" iq = Blather::Stanza::Iq::Command.new iq.form.fields = [ { var: "activation_method", value: "code" }, { var: "plan_name", value: "test_usd" } ] - result = Registration::Payment.for(iq, "test", "+15555550000") - assert_kind_of Registration::Payment::Code, result + result = Registration::Payment.for( + iq, + Customer.new("test"), + "+15555550000" + ) + assert_kind_of Registration::Payment::InviteCode, result end class BitcoinTest < Minitest::Test @@ -335,6 +338,166 @@ class RegistrationTest < Minitest::Test end em :test_write_declines end + + class InviteCodeTest < Minitest::Test + Registration::Payment::InviteCode::DB = + Minitest::Mock.new + Registration::Payment::InviteCode::REDIS = + Minitest::Mock.new + Registration::Payment::InviteCode::COMMAND_MANAGER = + Minitest::Mock.new + Registration::Payment::InviteCode::Finish = + Minitest::Mock.new + + def test_write + customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(0), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve( + Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [{ var: "code", value: "abc" }] + } + ), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_nil reply.form.instructions + end] + ) + Registration::Payment::InviteCode::DB.expect(:transaction, true, []) + Registration::Payment::InviteCode::Finish.expect( + :new, + OpenStruct.new(write: nil), + [ + Blather::Stanza::Iq::Command, + customer, + "+15555550000" + ] + ) + iq = Blather::Stanza::Iq::Command.new + iq.from = "test@example.com" + Registration::Payment::InviteCode.new( + iq, + customer, + "+15555550000" + ).write.sync + Registration::Payment::InviteCode::COMMAND_MANAGER.verify + Registration::Payment::InviteCode::DB.verify + Registration::Payment::InviteCode::REDIS.verify + Registration::Payment::InviteCode::Finish.verify + end + em :test_write + + def test_write_bad_code + customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(0), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve( + Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [{ var: "code", value: "abc" }] + } + ), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_nil reply.form.instructions + end] + ) + Registration::Payment::InviteCode::DB.expect(:transaction, []) do + raise Registration::Payment::InviteCode::Invalid, "wut" + end + Registration::Payment::InviteCode::REDIS.expect( + :incr, + EMPromise.resolve(nil), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::REDIS.expect( + :expire, + EMPromise.resolve(nil), + ["jmp_invite_tries-test", 60 * 60] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.reject(Promise::Error.new), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_equal "wut", reply.form.instructions + end] + ) + iq = Blather::Stanza::Iq::Command.new + iq.from = "test@example.com" + assert_raises Promise::Error do + Registration::Payment::InviteCode.new( + iq, + customer, + "+15555550000" + ).write.sync + end + Registration::Payment::InviteCode::COMMAND_MANAGER.verify + Registration::Payment::InviteCode::DB.verify + Registration::Payment::InviteCode::REDIS.verify + end + em :test_write_bad_code + + def test_write_bad_code_over_limit + customer = Customer.new("test", plan_name: "test_usd") + Registration::Payment::InviteCode::REDIS.expect( + :get, + EMPromise.resolve(11), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.resolve( + Blather::Stanza::Iq::Command.new.tap { |iq| + iq.form.fields = [{ var: "code", value: "abc" }] + } + ), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_nil reply.form.instructions + end] + ) + Registration::Payment::InviteCode::REDIS.expect( + :incr, + EMPromise.resolve(nil), + ["jmp_invite_tries-test"] + ) + Registration::Payment::InviteCode::REDIS.expect( + :expire, + EMPromise.resolve(nil), + ["jmp_invite_tries-test", 60 * 60] + ) + Registration::Payment::InviteCode::COMMAND_MANAGER.expect( + :write, + EMPromise.reject(Promise::Error.new), + [Matching.new do |reply| + assert_equal :form, reply.form.type + assert_equal "Too many wrong attempts", reply.form.instructions + end] + ) + iq = Blather::Stanza::Iq::Command.new + iq.from = "test@example.com" + assert_raises Promise::Error do + Registration::Payment::InviteCode.new( + iq, + customer, + "+15555550000" + ).write.sync + end + Registration::Payment::InviteCode::COMMAND_MANAGER.verify + Registration::Payment::InviteCode::REDIS.verify + end + em :test_write_bad_code_over_limit + end end class FinishTest < Minitest::Test