From 9a3023317c1f716a54fc813a378d6c3598fc127a Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 18 May 2021 09:48:37 -0500 Subject: [PATCH] Allow user to activate using invite code Checks if the code is available and marks it used, then activates. Tracks who invited and who used in the table for later reward or punishment. --- Gemfile | 2 +- lib/customer.rb | 18 ++++---- lib/registration.rb | 56 ++++++++++++++++++++++- schemas | 2 +- test/test_registration.rb | 95 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 158 insertions(+), 15 deletions(-) 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..487636de3262c08ab259edeb9c98ae29594cbbee 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,60 @@ 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| + verify(iq.form.field("code")&.value&.to_s).then { + Finish.new(iq, @customer, @tel) + }.catch_only(Invalid) { |e| + InviteCode.new(iq, @customer, @tel, error: e.message) + }.then(&:write) + end + end + + protected + + 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..4f079d9191c25c0b8c61417a0322c67dd4b1b995 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,92 @@ class RegistrationTest < Minitest::Test end em :test_write_declines end + + class InviteCodeTest < Minitest::Test + Registration::Payment::InviteCode::DB = + 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::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::Finish.verify + end + em :test_write + + def test_write_bad_code + customer = Customer.new("test", plan_name: "test_usd") + 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::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::Finish.verify + end + em :test_write_bad_code + end end class FinishTest < Minitest::Test