~singpolyma/sgx-jmp

829a57bfd2145e060948c0104f8d9cc79015c894 — Stephen Paul Weber 3 years ago 8b93d5a + e2b5bdf
Merge branch 'invites'

* invites:
  Block repeated invite code tries by customer id
  Allow user to activate using invite code
5 files changed, 249 insertions(+), 15 deletions(-)

M Gemfile
M lib/customer.rb
M lib/registration.rb
M schemas
M test/test_registration.rb
M Gemfile => Gemfile +1 -1
@@ 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"

M lib/customer.rb => lib/customer.rb +9 -9
@@ 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(

M lib/registration.rb => lib/registration.rb +72 -1
@@ 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

M schemas => schemas +1 -1
@@ 1,1 1,1 @@
Subproject commit e005a4d6b09636d21614be0c513ce9360cef2ccb
Subproject commit 1bef640493ff0409838c71e72dd105fb61473cb5

M test/test_registration.rb => test/test_registration.rb +166 -3
@@ 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