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 79f362f..5152fb8 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 d9fc3ca..f0115a1 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 6ae4ed4..487636d 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 e005a4d..1bef640 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 c1a017b..4f079d9 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 -- 2.38.5