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