Detailed changes
@@ -81,10 +81,10 @@ class Customer
)
end
- def with_plan(plan_name)
+ def with_plan(plan_name, **kwargs)
self.class.new(
@customer_id, @jid,
- plan: @plan.with_plan_name(plan_name),
+ plan: @plan.with(plan_name: plan_name, **kwargs),
balance: @balance, tndetails: @tndetails, sgx: @sgx
)
end
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "forwardable"
+require "value_semantics/monkey_patched"
require_relative "em"
require_relative "plan"
@@ -8,21 +9,24 @@ require_relative "plan"
class CustomerPlan
extend Forwardable
- attr_reader :expires_at, :auto_top_up_amount, :monthly_overage_limit,
- :parent_customer_id
-
- def_delegator :@plan, :name, :plan_name
- def_delegators :@plan, :currency, :merchant_account, :monthly_price,
+ def_delegator :plan, :name, :plan_name
+ def_delegators :plan, :currency, :merchant_account, :monthly_price,
:minute_limit, :message_limit
- def self.for(customer_id, plan_name: nil, **kwargs)
- new(customer_id, plan: plan_name&.then(&Plan.method(:for)), **kwargs)
+ value_semantics do
+ customer_id String
+ plan Anything(), default: nil, coerce: true
+ expires_at Either(Time, nil), default_generator: -> { Time.now }
+ auto_top_up_amount Integer, default: 0
+ monthly_overage_limit Integer, default: 0
+ pending Bool(), default: false
+ parent_customer_id Either(String, nil), default: nil
end
def self.default(customer_id, jid)
config = CONFIG[:parented_domains][Blather::JID.new(jid).domain]
if config
- self.for(
+ new(
customer_id,
plan_name: config[:plan_name],
parent_customer_id: config[:customer_id]
@@ -33,7 +37,7 @@ class CustomerPlan
end
def self.extract(customer_id, **kwargs)
- self.for(
+ new(
customer_id,
**kwargs.slice(
:plan_name, :expires_at, :parent_customer_id, :pending,
@@ -42,44 +46,44 @@ class CustomerPlan
)
end
- def initialize(
- customer_id,
- plan: nil,
- expires_at: Time.now,
- auto_top_up_amount: 0,
- monthly_overage_limit: 0,
- pending: false, parent_customer_id: nil
- )
- @customer_id = customer_id
- @plan = plan || OpenStruct.new
- @expires_at = expires_at
- @auto_top_up_amount = auto_top_up_amount || 0
- @monthly_overage_limit = monthly_overage_limit || 0
- @pending = pending
- @parent_customer_id = parent_customer_id
+ def self.coerce_plan(plan_or_name_or_nil)
+ return OpenStruct.new unless plan_or_name_or_nil
+
+ Plan.for(plan_or_name_or_nil)
+ end
+
+ def initialize(customer_id=nil, **kwargs)
+ kwargs[:plan] = kwargs.delete(:plan_name) if kwargs.key?(:plan_name)
+ super(customer_id ? kwargs.merge(customer_id: customer_id) : kwargs)
end
def active?
- plan_name && @expires_at > Time.now
+ plan_name && expires_at > Time.now
end
def status
return :active if active?
- return :pending if @pending
+ return :pending if pending
:expired
end
- def with_plan_name(plan_name)
- self.class.new(
- @customer_id,
- plan: Plan.for(plan_name),
- expires_at: @expires_at
- )
+ def verify_parent!
+ return unless parent_customer_id
+
+ result = DB.query(<<~SQL, [parent_customer_id])
+ SELECT plan_name FROM customer_plans WHERE customer_id=$1
+ SQL
+
+ raise "Invalid parent account" if !result || !result.first
+
+ plan = Plan.for(result.first["plan_name"])
+ raise "Parent currency mismatch" unless plan.currency == currency
end
def save_plan!
- DB.exec_defer(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
+ verify_parent!
+ DB.exec_defer(<<~SQL, [customer_id, plan_name, parent_customer_id])
INSERT INTO plan_log
(customer_id, plan_name, parent_customer_id, date_range)
VALUES (
@@ -107,7 +111,8 @@ class CustomerPlan
end
def activate_plan_starting_now
- activated = DB.exec(<<~SQL, [@customer_id, plan_name, @parent_customer_id])
+ verify_parent!
+ activated = DB.exec(<<~SQL, [customer_id, plan_name, parent_customer_id])
INSERT INTO plan_log (customer_id, plan_name, date_range, parent_customer_id)
VALUES ($1, $2, tsrange(LOCALTIMESTAMP, LOCALTIMESTAMP + '1 month'), $3)
ON CONFLICT DO NOTHING
@@ -115,7 +120,7 @@ class CustomerPlan
activated = activated.cmd_tuples.positive?
return false unless activated
- DB.exec(<<~SQL, [@customer_id])
+ DB.exec(<<~SQL, [customer_id])
DELETE FROM plan_log WHERE customer_id=$1 AND date_range << '[now,now]'
AND upper(date_range) - lower(date_range) < '2 seconds'
SQL
@@ -126,22 +131,24 @@ class CustomerPlan
end
def activation_date
- DB.query_one(<<~SQL, @customer_id).then { |r| r[:start_date] }
+ DB.query_one(<<~SQL, customer_id).then { |r| r[:start_date] }
SELECT
MIN(LOWER(date_range)) AS start_date
FROM plan_log WHERE customer_id = $1;
SQL
end
+ protected :customer_id, :plan, :pending, :[]
+
protected
def charge_for_plan(note)
- raise "No plan setup" unless @plan
+ raise "No plan setup" unless plan
params = [
- @customer_id,
- "#{@customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
- -@plan.monthly_price,
+ customer_id,
+ "#{customer_id}-bill-#{plan_name}-at-#{Time.now.to_i}",
+ -plan.monthly_price,
note
]
DB.exec(<<~SQL, params)
@@ -152,7 +159,7 @@ protected
end
def add_one_month_to_current_plan
- DB.exec(<<~SQL, [@customer_id])
+ DB.exec(<<~SQL, [customer_id])
UPDATE plan_log SET date_range=range_merge(
date_range,
tsrange(
@@ -2,6 +2,8 @@
class Plan
def self.for(plan_name)
+ return plan_name if plan_name.is_a?(Plan)
+
plan = CONFIG[:plans].find { |p| p[:name] == plan_name }
raise "No plan by that name" unless plan
@@ -11,6 +11,7 @@ require_relative "./command"
require_relative "./em"
require_relative "./invites_repo"
require_relative "./oob"
+require_relative "./parent_code_repo"
require_relative "./proxied_jid"
require_relative "./tel_selections"
require_relative "./welcome_message"
@@ -105,7 +106,7 @@ class Registration
def next_step(iq)
code = iq.form.field("code")&.value&.to_s
- save_customer_plan(iq).then {
+ save_customer_plan(iq, code).then {
finish_if_valid_invite(code)
}.catch_only(InvitesRepo::Invalid) do
@invites.stash_code(customer.customer_id, code).then do
@@ -124,10 +125,12 @@ class Registration
end
end
- def save_customer_plan(iq)
- plan_name = iq.form.field("plan_name").value.to_s
- @customer = @customer.with_plan(plan_name)
- @customer.save_plan!
+ def save_customer_plan(iq, code)
+ ParentCodeRepo.new(REDIS).find(code).then do |parent|
+ plan_name = iq.form.field("plan_name").value.to_s
+ @customer = @customer.with_plan(plan_name, parent_customer_id: parent)
+ @customer.save_plan!
+ end
end
class GooglePlay
@@ -136,6 +139,7 @@ class Registration
@google_play_userid = google_play_userid
@tel = tel
@invites = InvitesRepo.new(DB, REDIS)
+ @parent_code_repo = ParentCodeRepo.new(REDIS)
end
def used
@@ -163,17 +167,25 @@ class Registration
end
def activate(iq)
- REDIS.sadd("google_play_userids", @google_play_userid).then {
- plan_name = iq.form.field("plan_name").value.to_s
- @customer = @customer.with_plan(plan_name)
- @customer.activate_plan_starting_now
+ plan_name = iq.form.field("plan_name").value
+ code = iq.form.field("code")&.value
+ EMPromise.all([
+ @parent_code_repo.find(code),
+ REDIS.sadd("google_play_userids", @google_play_userid)
+ ]).then { |(parent, _)|
+ save_active_plan(plan_name, parent)
}.then do
- use_referral_code(iq.form.field("code")&.value&.to_s)
+ use_referral_code(code)
end
end
protected
+ def save_active_plan(plan_name, parent)
+ @customer = @customer.with_plan(plan_name, parent_customer_id: parent)
+ @customer.activate_plan_starting_now
+ end
+
def use_referral_code(code)
EMPromise.resolve(nil).then {
@invites.claim_code(@customer.customer_id, code) {
@@ -406,7 +418,24 @@ class Registration
end
class InviteCode
- Payment.kinds[:code] = method(:new)
+ Payment.kinds[:code] = ->(*args, **kw) { self.for(*args, **kw) }
+
+ def self.for(in_customer, tel, finish: Finish, **)
+ reload_customer(in_customer).then do |customer|
+ if customer.balance >= CONFIG[:activation_amount_accept]
+ next BillPlan.new(customer, tel, finish: finish)
+ end
+
+ msg = if customer.balance.positive?
+ "Account balance not enough to cover the activation"
+ end
+ new(customer, tel, error: msg)
+ end
+ end
+
+ def self.reload_customer(customer)
+ Command.execution.customer_repo.find(customer.customer_id)
+ end
FIELDS = [{
var: "code",
@@ -49,6 +49,11 @@ class CustomerTest < Minitest::Test
em :test_bill_plan_activate
def test_bill_plan_reactivate_child
+ CustomerPlan::DB.expect(
+ :query,
+ [{ "plan_name" => "test_usd" }],
+ [String, ["parent"]]
+ )
CustomerPlan::DB.expect(:transaction, nil) do |&block|
block.call
true
@@ -194,6 +194,11 @@ class CustomerRepoTest < Minitest::Test
EMPromise.resolve([]),
["jmp_customer_feature_flags-testp"]
)
+ CustomerPlan::DB.expect(
+ :query,
+ [{ "plan_name" => "test_usd" }],
+ [String, ["1234"]]
+ )
CustomerPlan::DB.expect(
:exec_defer,
EMPromise.resolve(nil),
@@ -112,7 +112,9 @@ class RegistrationTest < Minitest::Test
class ActivationTest < Minitest::Test
Registration::Activation::DB = Minitest::Mock.new
- Registration::Activation::REDIS = FakeRedis.new
+ Registration::Activation::REDIS = FakeRedis.new(
+ "jmp_parent_codes" => { "PARENT_CODE" => 1 }
+ )
Registration::Activation::Payment = Minitest::Mock.new
Registration::Activation::Finish = Minitest::Mock.new
Command::COMMAND_MANAGER = Minitest::Mock.new
@@ -136,7 +138,9 @@ class RegistrationTest < Minitest::Test
)
end]
)
- @customer.expect(:with_plan, @customer, ["test_usd"])
+ @customer.expect(:with_plan, @customer) do |*args, **|
+ assert_equal ["test_usd"], args
+ end
@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
Registration::Activation::Payment.expect(
:for,
@@ -170,7 +174,9 @@ class RegistrationTest < Minitest::Test
)
end]
)
- @customer.expect(:with_plan, @customer, ["test_usd"])
+ @customer.expect(:with_plan, @customer) do |*args, **|
+ assert_equal ["test_usd"], args
+ end
@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
@customer.expect(:activate_plan_starting_now, EMPromise.resolve(nil), [])
Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
@@ -212,7 +218,9 @@ class RegistrationTest < Minitest::Test
)
end]
)
- @customer.expect(:with_plan, @customer, ["test_usd"])
+ @customer.expect(:with_plan, @customer) do |*args, **|
+ assert_equal ["test_usd"], args
+ end
@customer.expect(:save_plan!, EMPromise.resolve(nil), [])
Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
Registration::Activation::DB.expect(
@@ -241,6 +249,50 @@ class RegistrationTest < Minitest::Test
assert_mock Registration::Activation::DB
end
em :test_write_with_group_code
+
+ def test_write_with_parent_code
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [
+ { var: "plan_name", value: "test_usd" },
+ { var: "code", value: "PARENT_CODE" }
+ ]
+ }),
+ [Matching.new do |iq|
+ assert_equal :form, iq.form.type
+ assert_equal(
+ "You've selected +15555550000 as your JMP number.",
+ iq.form.instructions.lines.first.chomp
+ )
+ end]
+ )
+ @customer.expect(:with_plan, @customer) do |*args, **kwargs|
+ assert_equal ["test_usd"], args
+ assert_equal({ parent_customer_id: 1 }, kwargs)
+ end
+ @customer.expect(:save_plan!, EMPromise.resolve(nil), [])
+ Registration::Activation::DB.expect(:transaction, []) { |&blk| blk.call }
+ Registration::Activation::DB.expect(
+ :exec,
+ OpenStruct.new(cmd_tuples: 0),
+ [String, ["test", "PARENT_CODE"]]
+ )
+ Registration::Activation::Payment.expect(
+ :for,
+ EMPromise.reject(:test_result),
+ [Blather::Stanza::Iq, @customer, "+15555550000"]
+ )
+ assert_equal(
+ :test_result,
+ execute_command { @activation.write.catch { |e| e } }
+ )
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock @customer
+ assert_mock Registration::Activation::Payment
+ assert_mock Registration::Activation::DB
+ end
+ em :test_write_with_parent_code
end
class AllowTest < Minitest::Test
@@ -426,13 +478,18 @@ class RegistrationTest < Minitest::Test
{ var: "activation_method", value: "code" },
{ var: "plan_name", value: "test_usd" }
]
- result = Registration::Payment.for(
- iq,
- customer,
- "+15555550000"
- )
+ cust = customer
+ result = execute_command do
+ Command.execution.customer_repo.expect(:find, cust, ["test"])
+ Registration::Payment.for(
+ iq,
+ cust,
+ "+15555550000"
+ )
+ end
assert_kind_of Registration::Payment::InviteCode, result
end
+ em :test_for_code
class BitcoinTest < Minitest::Test
Registration::Payment::Bitcoin::BTC_SELL_PRICES = Minitest::Mock.new