~singpolyma/sgx-jmp

9a3023317c1f716a54fc813a378d6c3598fc127a — Stephen Paul Weber 1 year, 8 months ago 8b93d5a
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.
5 files changed, 158 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 +55 -1
@@ 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

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

M test/test_registration.rb => test/test_registration.rb +92 -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,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