.rubocop.yml 🔗
@@ -11,6 +11,8 @@ Metrics/ClassLength:
Metrics/MethodLength:
Exclude:
- test/*
+ CountAsOne:
+ - array
Metrics/BlockLength:
ExcludedMethods:
Phillip Davis created
name can be set at registration and bulk edited later as well
.rubocop.yml | 2
forms/edit_sim_nicknames.rb | 14 ++
forms/sim_details.rb | 3
lib/edit_sim_nicknames.rb | 30 ++++
lib/sim.rb | 2
lib/sim_order.rb | 74 +++++++++--
lib/sim_repo.rb | 11 +
lib/utils.rb | 17 ++
sgx_jmp.rb | 10 +
test/test_sim_order.rb | 237 +++++++++++++++++++++++++++++++++++++++
10 files changed, 380 insertions(+), 20 deletions(-)
@@ -11,6 +11,8 @@ Metrics/ClassLength:
Metrics/MethodLength:
Exclude:
- test/*
+ CountAsOne:
+ - array
Metrics/BlockLength:
ExcludedMethods:
@@ -0,0 +1,14 @@
+form!
+
+if @sims.empty?
+ instructions "You have no (e)SIMs."
+else
+ @sims.map { |sim|
+ field(
+ var: "sim-#{sim.iccid}",
+ label: sim.iccid,
+ type: "text-single",
+ value: sim.nickname
+ )
+ }
+end
@@ -19,7 +19,8 @@ field(
options: [
{ label: "Done", value: "complete" },
{ label: "Order new Physical SIM", value: "order-sim" },
- { label: "Order new eSIM", value: "order-esim" }
+ { label: "Order new eSIM", value: "order-esim" },
+ { label: "Edit (e)SIM nicknames", value: "edit-nicknames" }
],
value: "complete"
)
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class EditSimNicknames
+ # @param [Customer] customer the customer editing the sims
+ # @param [Array<Sim>] sims the sims whose nicknames
+ # are up for editing
+ def initialize(customer, sims, sim_repo: SIMRepo.new(DB))
+ @customer = customer
+ @sims = sims
+ @sim_repo = sim_repo
+ end
+
+ def form
+ FormTemplate.render(
+ "edit_sim_nicknames",
+ sims: @sims
+ )
+ end
+
+ def complete(iq)
+ EMPromise.resolve(nil).then {
+ sims = @sims.map { |sim|
+ sim.with(nickname: iq.form.field("sim-#{sim.iccid}").value)
+ }
+ @sim_repo.update_nicknames(sims)
+ }.then {
+ Command.finish("Successfully updated SIM nicknames.")
+ }
+ end
+end
@@ -15,7 +15,7 @@ class SIM
def self.extract(kwargs)
kwargs = kwargs&.transform_keys(&:to_sym) || {}
new(kwargs.slice(
- :iccid, :lpa_code, :remaining_usage_kb, :remaining_days, :notes
+ :iccid, :lpa_code, :remaining_usage_kb, :remaining_days, :notes, :nickname
))
end
@@ -30,6 +30,12 @@ class SIMOrder
label: "Shipping Address",
var: "addr",
required: true
+ },
+ {
+ type: "text-single",
+ var: "nickname",
+ label: "Nickname",
+ required: false
}
]
end
@@ -52,11 +58,29 @@ class SIMOrder
end
def complete(iq)
- addr = Array(iq.form.field("addr").value).join("\n")
- EMPromise.resolve(nil).then { commit }.then do |sim|
+ form = iq.form
+ EMPromise.resolve(nil).then {
+ commit(form.field("nickname").value.presence || self.class.label)
+ }.then do |sim|
+ Ack.new(
+ @customer,
+ sim,
+ Array(form.field("addr").value).join("\n")
+ ).complete
+ end
+ end
+
+ class Ack
+ def initialize(customer, sim, addr)
+ @customer = customer
+ @sim = sim
+ @addr = addr
+ end
+
+ def complete
@customer.stanza_from(Blather::Stanza::Message.new(
Blather::JID.new(""), # Doesn't matter, sgx is set to direct target
- "SIM ORDER: #{sim.iccid}\n#{addr}"
+ "SIM ORDER: #{@sim.iccid}\n#{@addr}"
))
Command.finish(
"You will receive an notice from support when your SIM ships."
@@ -66,10 +90,12 @@ class SIMOrder
protected
- def commit
+ # @param [String, nil] nickname the nickname, if any, assigned to
+ # the sim by the customer
+ def commit(nickname)
DB.transaction do
sim = @sim_repo.available.sync
- @sim_repo.put_owner(sim, @customer, self.class.label)
+ @sim_repo.put_owner(sim, @customer, nickname)
keepgo_tx = @sim_repo.refill(sim, amount_mb: 1024).sync
raise "SIM activation failed" unless keepgo_tx["ack"] == "success"
@@ -96,16 +122,34 @@ protected
[]
end
- def complete(_)
- EMPromise.resolve(nil).then { commit }.then do |sim|
- Command.finish do |reply|
- oob = OOB.find_or_create(reply.command)
- oob.url = sim.lpa_code
- oob.desc = "LPA Activation Code"
- reply.command << FormTemplate.render(
- "order_sim/esim_complete", sim: sim
- )
- end
+ # @param [Blather::Stanza::Iq] iq the stanza
+ # containing a filled out `order_sim/with_balance`
+ def complete(iq)
+ EMPromise.resolve(nil).then {
+ commit(
+ nickname: iq.form.field("nickname").value.presence || self.class.label
+ )
+ }.then do |sim|
+ ActivationCode.new(sim).complete
+ end
+ end
+ end
+
+ class ActivationCode
+ # @param [Sim] sim the sim which the customer
+ # just ordered
+ def initialize(sim)
+ @sim = sim
+ end
+
+ def complete
+ Command.finish do |reply|
+ oob = OOB.find_or_create(reply.command)
+ oob.url = @sim.lpa_code
+ oob.desc = "LPA Activation Code"
+ reply.command << FormTemplate.render(
+ "order_sim/esim_complete", sim: @sim
+ )
end
end
end
@@ -48,6 +48,17 @@ class SIMRepo
).then { |req| JSON.parse(req.response) }
end
+ def update_nicknames(sims)
+ iccids = PG::TextEncoder::Array.new.encode(sims.map(&:iccid))
+ nicknames = PG::TextEncoder::Array.new.encode(sims.map(&:nickname))
+ db.query_defer(<<~SQL, [iccids, nicknames])
+ UPDATE sims SET nickname = data_table.nickname
+ FROM
+ (SELECT UNNEST($1::text[]) AS iccid, UNNEST($2::text[]) AS nickname) AS data_table
+ WHERE sims.iccid = data_table.iccid
+ SQL
+ end
+
def owned_by(customer)
customer = customer.customer_id if customer.respond_to?(:customer_id)
promise = db.query_defer(<<~SQL, [customer])
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class String
+ def presence
+ if empty?
+ nil
+ else
+ self
+ end
+ end
+end
+
+class NilClass
+ def presence
+ nil
+ end
+end
@@ -14,6 +14,7 @@ require "sentry-ruby"
require "statsd-instrument"
require_relative "lib/background_log"
+require_relative "lib/utils"
$stdout.sync = true
LOG = Ougai::Logger.new(BackgroundLog.new($stdout))
@@ -103,6 +104,7 @@ require_relative "lib/transaction"
require_relative "lib/tel_selections"
require_relative "lib/sim_repo"
require_relative "lib/sim_order"
+require_relative "lib/edit_sim_nicknames"
require_relative "lib/snikket"
require_relative "lib/welcome_message"
require_relative "web"
@@ -778,14 +780,16 @@ Command.new(
SIMOrder::ESIM.for(
customer, **CONFIG.dig(:sims, :esim, customer.currency)
)
+ when "edit-nicknames"
+ EditSimNicknames.new(customer, sims)
else
Command.finish
end
- }.then { |order|
+ }.then { |action|
Command.reply { |reply|
reply.allowed_actions = [:complete]
- reply.command << order.form
- }.then(&order.method(:complete))
+ reply.command << action.form
+ }.then(&action.method(:complete))
}
end
}.register(self).then(&CommandList.method(:register))
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+require "ostruct"
+
+require_relative "../lib/sim_order"
+
+class SIMOrderTest < Minitest::Test
+ SIMOrder::DB = Minitest::Mock.new
+ Transaction::DB = Minitest::Mock.new
+
+ def setup
+ @price = 5000
+ @plan_name = "better than no plan"
+ @sim_repo = Minitest::Mock.new
+ @sim = SIM.new(
+ iccid: "123",
+ nickname: nil,
+ lpa_code: "LPA:abc",
+ remaining_usage_kb: 1025,
+ remaining_days: 3,
+ notes: "no notes"
+ )
+ end
+
+ def test_for_enough_balance
+ customer = Minitest::Mock.new
+ customer.expect(:balance, 60)
+
+ result = execute_command do
+ SIMOrder.for(customer, price: @price, plan: @plan_name)
+ end
+ assert_kind_of SIMOrder, result
+ assert_mock customer
+ end
+ em :test_for_enough_balance
+
+ def test_for_insufficient_balance_with_top_up
+ customer = Minitest::Mock.new
+ customer.expect(:balance, 40)
+
+ LowBalance::AutoTopUp.stub(
+ :for,
+ OpenStruct.new(can_top_up?: true),
+ [customer, @price]
+ ) do
+ result = execute_command do
+ SIMOrder.for(customer, price: @price, plan: @plan_name)
+ end
+ assert_kind_of SIMOrder::WithTopUp, result
+ assert_mock customer
+ end
+ end
+ em :test_for_insufficient_balance_with_top_up
+
+ def test_for_insufficient_balance_please_top_up
+ customer = Minitest::Mock.new
+ customer.expect(:balance, 40)
+
+ LowBalance::AutoTopUp.stub(
+ :for,
+ OpenStruct.new(can_top_up?: false),
+ [customer, @price]
+ ) do
+ result = execute_command do
+ SIMOrder.for(customer, price: @price, plan: @plan_name)
+ end
+ assert_kind_of SIMOrder::PleaseTopUp, result
+ assert_mock customer
+ end
+ end
+ em :test_for_insufficient_balance_please_top_up
+
+ def test_complete_nil_nick
+ customer = Minitest::Mock.new
+ customer.expect(:balance, 100)
+ customer.expect(
+ :stanza_from,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Message]
+ )
+ customer.expect(:customer_id, "123")
+
+ SIMOrder::DB.expect(
+ :transaction,
+ EMPromise.resolve(@sim)
+ ) do |&blk|
+ blk.call
+ end
+
+ Transaction::DB.expect(
+ :exec,
+ nil,
+ [
+ String,
+ Matching.new { |params|
+ assert_equal "123", params[0]
+ assert_equal "tx123", params[1]
+ assert_kind_of Time, params[2]
+ assert_kind_of Time, params[3]
+ assert_in_delta(-(@price / 100).to_d, params[4], 0.05)
+ assert_equal "SIM Activation 123", params[5]
+ }
+ ]
+ )
+
+ @sim_repo.expect(:available, EMPromise.resolve(@sim), [])
+ @sim_repo.expect(:put_owner, nil, [@sim, customer, "SIM"])
+ @sim_repo.expect(
+ :refill,
+ EMPromise.resolve(OpenStruct.new(ack: "success", transaction_id: "tx123"))
+ ) do |refill_sim, amount_mb:|
+ @sim == refill_sim && amount_mb == 1024
+ end
+
+ order_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [
+ { var: "addr", value: "123 Main St" },
+ { var: "nickname", value: nil }
+ ]
+ }
+
+ blather = Minitest::Mock.new
+ blather.expect(
+ :<<,
+ EMPromise.resolve(:test_result),
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal(
+ "You will receive an notice from support when your SIM ships.",
+ reply.note.content
+ )
+ end]
+ )
+ sim_order = SIMOrder.for(customer, price: @price, plan: @plan_name)
+ sim_order.instance_variable_set(:@sim_repo, @sim_repo)
+
+ assert_equal(
+ :test_result,
+ execute_command(blather: blather) { sim_order.complete(order_form) }
+ )
+
+ assert_mock Transaction::DB
+ assert_mock SIMOrder::DB
+ assert_mock customer
+ assert_mock @sim_repo
+ assert_mock blather
+ end
+ em :test_complete_nil_nick
+
+ def test_complete_nick_present
+ customer = Minitest::Mock.new
+ customer.expect(:balance, 100)
+ customer.expect(
+ :stanza_from,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Message]
+ )
+ customer.expect(
+ :customer_id,
+ "123",
+ []
+ )
+ Transaction::DB.expect(
+ :exec,
+ nil,
+ [
+ String,
+ Matching.new { |params|
+ assert_equal "123", params[0]
+ assert_equal "tx123", params[1]
+ assert_kind_of Time, params[2]
+ assert_kind_of Time, params[3]
+ assert_in_delta(-(@price / 100).to_d, params[4], 0.05)
+ assert_equal "SIM Activation 123", params[5]
+ }
+ ]
+ )
+
+ SIMOrder::DB.expect(
+ :transaction,
+ @sim
+ ) do |&blk|
+ blk.call
+ end
+
+ @sim_repo.expect(:available, EMPromise.resolve(@sim), [])
+ @sim_repo.expect(
+ :put_owner,
+ nil,
+ [@sim, customer, "test_nick"]
+ )
+ @sim_repo.expect(
+ :refill,
+ EMPromise.resolve(OpenStruct.new(ack: "success", transaction_id: "tx123"))
+ ) do |refill_sim, amount_mb:|
+ @sim == refill_sim && amount_mb == 1024
+ end
+
+ order_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [
+ { var: "addr", value: "123 Main St" },
+ { var: "nickname", value: "test_nick" }
+ ]
+ }
+
+ blather = Minitest::Mock.new
+ blather.expect(
+ :<<,
+ EMPromise.resolve(:test_result),
+ [Matching.new do |reply|
+ assert_equal :completed, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal(
+ "You will receive an notice from support when your SIM ships.",
+ reply.note.content
+ )
+ end]
+ )
+ sim_order = SIMOrder.for(customer, price: @price, plan: @plan_name)
+ sim_order.instance_variable_set(:@sim_repo, @sim_repo)
+
+ assert_equal(
+ :test_result,
+ execute_command(blather: blather) { sim_order.complete(order_form) }
+ )
+
+ assert_mock Transaction::DB
+ assert_mock SIMOrder::DB
+ assert_mock customer
+ assert_mock @sim_repo
+ assert_mock blather
+ end
+ em :test_complete_nick_present
+end