Detailed changes
@@ -7,6 +7,7 @@ gem "bandwidth-sdk", "<= 6.1.0"
gem "blather", git: "https://github.com/singpolyma/blather", branch: "better-ids"
gem "braintree"
gem "countries"
+gem "csv"
gem "dhall", ">= 0.5.3.fixed"
gem "em-hiredis"
gem "em-http-request", git: "https://github.com/singpolyma/em-http-request", branch: "fix-letsencrypt"
@@ -88,6 +88,7 @@
, sip_host : Text
, snikket_hosting_api : Text
, support_link : forall (customer_jid : Text) -> Text
+, public_onboarding_url : Text
, upstream_domain : Text
, web : < Inet : { interface : Text, port : Natural } | Unix : Text >
, web_register : { from : Text, to : Text }
@@ -115,6 +115,7 @@ in
reachability_senders = [ "+14445556666" ],
support_link = \(customer_jid: Text) ->
"http://localhost:3002/app/accounts/2/contacts/custom_attributes/jid/${customer_jid}",
+ public_onboarding_url = "xmpp:example.com?register",
churnbuster = {
api_key = "",
account_id = ""
@@ -1,30 +0,0 @@
-form!
-
-title "Purchase Number"
-
-instructions <<~I
- You've selected #{@tel} as your JMP number.
- This number has a one-time price of $#{'%.2f' % @tel.price}.
- (If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
-I
-
-field(
- var: "activation_method",
- type: "list-single",
- label: "Activate using",
- required: true,
- options: [
- {
- value: "credit_card",
- label: "Credit Card"
- },
- {
- value: "bitcoin",
- label: "Bitcoin"
- },
- {
- value: "bch",
- label: "Bitcoin Cash"
- }
- ]
-)
@@ -0,0 +1,12 @@
+form!
+title "Choose SIM Kind"
+
+field(
+ label: "SIM or eSIM Adapter",
+ var: "sim_kind",
+ type: "list-single",
+ options: [
+ { label: "SIM", value: "sim" },
+ { label: "eSIM Adapter", value: "esim" }
+ ]
+)
@@ -0,0 +1,28 @@
+form!
+
+title @title
+
+instructions @instructions
+
+field(
+ var: "activation_method",
+ type: "list-single",
+ label: "Activate using",
+ required: true,
+ options: [
+ {
+ value: "credit_card",
+ label: "Credit Card"
+ },
+ {
+ value: "bitcoin",
+ label: "Bitcoin"
+ },
+ {
+ value: "bch",
+ label: "Bitcoin Cash"
+ }
+ ]
+)
+
+instance_eval File.read("#{__dir__}/plan_name.rb") if @currency_required
@@ -26,7 +26,8 @@ field(
var: "http://jabber.org/protocol/commands#actions",
options: [
{ label: "Search", value: "next" },
- { label: "Just Show Me Some Numbers", value: "feelinglucky" }
+ { label: "Just Show Me Some Numbers", value: "feelinglucky" },
+ { label: "Just Data, Please", value: "data_only" }
],
value: "next"
)
@@ -1,6 +1,10 @@
# frozen_string_literal: true
+require_relative "sim_order"
+
class EditSimNicknames
+ include SIMAction
+
# @param [Customer] customer the customer editing the sims
# @param [Array<Sim>] sims the sims whose nicknames
# are up for editing
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "blather"
+
+require_relative "proxied_jid"
+
+Blather::JID.class_eval do
+ def onboarding?
+ ProxiedJID.new(self).unproxied.domain == CONFIG[:onboarding_domain]
+ end
+end
@@ -15,6 +15,8 @@ require_relative "./parent_code_repo"
require_relative "./proxied_jid"
require_relative "./tel_selections"
require_relative "./welcome_message"
+require_relative "./sim_kind"
+require_relative "./onboarding"
def reload_customer(customer)
EMPromise.resolve(nil).then do
@@ -22,14 +24,111 @@ def reload_customer(customer)
end
end
+def handle_prev(customer, googleplay_user_id, product)
+ if product.is_a?(SIMKind)
+ Registration::DataOnly.for(customer, product)
+ else
+ Registration::Activation.for(customer, googleplay_user_id, product)
+ end
+end
+
class Registration
+ class PayForSim
+ def initialize(customer, sim_kind)
+ @customer = customer
+ @sim_kind = sim_kind
+ end
+
+ def write
+ Command.reply { |reply|
+ reply.command << FormTemplate.render(
+ "registration/pay_without_code",
+ product: @sim_kind,
+ instructions: instructions,
+ currency_required: !@customer.currency,
+ title: "Pay for #{@sim_kind.klass.label}"
+ )
+ }.then(&method(:parse)).then(&:write)
+ end
+
+ def instructions
+ cfg = @sim_kind.cfg(@customer.currency)
+ return nil unless cfg
+
+ <<~I
+ To activate your data SIM, you need to deposit $#{'%.2f' % (cfg[:price] / 100.0)} to your balance.
+ (If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
+ I
+ end
+
+ def parse(iq)
+ unless @customer.currency
+ plan = Plan.for_registration(iq.form.field("plan_name").value.to_s)
+ (@customer = @customer.with_plan(plan.name)).save_plan!
+ end.then { process_payment(iq) }
+ end
+
+ def process_payment(iq)
+ Payment.for(
+ iq, @customer, @sim_kind,
+ price: @sim_kind.cfg(@customer.currency)[:price],
+ maybe_bill: Registration::Payment::JustCharge
+ ).write.then { reload_customer(@customer) }.then { |customer|
+ DataOnly.for(customer, @sim_kind)
+ }
+ end
+ end
+
+ class DataOnly
+ def self.for(customer, sim_kind)
+ cfg = sim_kind.cfg(customer.currency)
+ unless cfg && customer.balance > cfg[:price]
+ return PayForSim.new(customer, sim_kind)
+ end
+
+ new(customer, sim_kind)
+ end
+
+ def initialize(customer, sim_kind)
+ @customer = customer
+ @sim_kind = sim_kind
+ end
+
+ def write
+ @sim_kind.klass.for(
+ @customer,
+ **@sim_kind.cfg(@customer.currency)
+ ).then { |order|
+ # NOTE: cheogram will swallow any stanza
+ # with type `completed`
+ # `can_complete: false` is needed to prevent customer
+ # from (possibly) permanently losing their eSIM QR code
+ order.process(can_complete: false)
+ }
+ end
+ end
+
+ class RegistrationType
+ def self.for(customer, google_play_userid, product)
+ if product.is_a?(SIMKind)
+ return Registration::DataOnly.for(
+ customer, product
+ )
+ end
+
+ Registration::FinishOrStartActivation.for(
+ customer, google_play_userid, product
+ )
+ end
+ end
+
def self.for(customer, google_play_userid, tel_selections)
if (reg = customer.registered?)
Registered.for(customer, reg.phone)
else
- tel_selections[customer.jid].then(&:choose_tel).then do |tel|
- reserve_and_continue(tel_selections, customer, tel).then do
- FinishOrStartActivation.for(customer, google_play_userid, tel)
+ tel_selections[customer.jid].then(&:choose_tel_or_data).then do |product|
+ reserve_and_continue(tel_selections, customer, product).then do
+ RegistrationType.for(customer, google_play_userid, product)
end
end
end
@@ -40,7 +139,7 @@ class Registration
tel_selections.delete(customer.jid).then {
tel_selections[customer.jid]
}.then { |choose|
- choose.choose_tel(
+ choose.choose_tel_or_data(
error: "The JMP number #{tel} is no longer available."
)
}.then { |n_tel| reserve_and_continue(tel_selections, customer, n_tel) }
@@ -49,8 +148,7 @@ class Registration
class Registered
def self.for(customer, tel)
- jid = ProxiedJID.new(customer.jid).unproxied
- if jid.domain == CONFIG[:onboarding_domain]
+ if customer.jid.onboarding?
FinishOnboarding.for(customer, tel)
else
new(tel)
@@ -260,14 +358,14 @@ class Registration
end
def self.for(
- iq, customer, tel,
+ iq, customer, product,
finish: Finish, maybe_bill: MaybeBill,
- price: CONFIG[:activation_amount] + tel.price
+ price: CONFIG[:activation_amount] + product.price
)
kinds.fetch(iq.form.field("activation_method")&.value&.to_s&.to_sym) {
raise "Invalid activation method"
}.call(
- customer, tel, finish: finish, maybe_bill: maybe_bill, price: price
+ customer, product, finish: finish, maybe_bill: maybe_bill, price: price
)
end
@@ -285,20 +383,22 @@ class Registration
end
def initialize(
- customer, tel,
- price: CONFIG[:activation_amount] + tel.price, **
+ customer, product,
+ price: CONFIG[:activation_amount] + product.price, **
)
@customer = customer
@customer_id = customer.customer_id
- @tel = tel
+ @product = product
@price = price
end
def save
- TEL_SELECTIONS.set(@customer.jid, @tel)
+ return if product.is_a?(SIMKind)
+
+ TEL_SELECTIONS.set(@customer.jid, @product)
end
- attr_reader :customer_id, :tel
+ attr_reader :customer_id, :product
def form(rate, addr)
FormTemplate.render(
@@ -323,7 +423,7 @@ class Registration
def handle_possible_prev(iq)
raise "Action not allowed" unless iq.prev?
- Activation.for(@customer, nil, @tel).then(&:write)
+ handle_prev(@customer, nil, @product)
end
def addr_and_rate
@@ -408,14 +508,14 @@ class Registration
Payment.kinds[:credit_card] = method(:new)
def initialize(
- customer, tel,
+ customer, product,
finish: Finish, maybe_bill: MaybeBill,
- price: CONFIG[:activation_amount] + tel.price
+ price: CONFIG[:activation_amount] + product.price
)
@customer = customer
- @tel = tel
+ @product = product
@finish = finish
- @maybe_bill = maybe_bill.new(customer, tel, finish: finish)
+ @maybe_bill = maybe_bill.new(customer, product, finish: finish)
@price = price
end
@@ -436,7 +536,7 @@ class Registration
reply.note_type = :info
reply.note_text = "#{toob.desc}: #{toob.url}"
}.then do |iq|
- next Activation.for(@customer, nil, @tel).then(&:write) if iq.prev?
+ handle_prev(@customer, nil, @product) if iq.prev?
@maybe_bill.call { self }&.then(&:write)
end
@@ -586,11 +686,21 @@ class Registration
@tel = tel
end
+ def instructions
+ <<~I
+ You've selected #{@tel} as your JMP number.
+ To activate your account, you can either deposit $#{'%.2f' % (CONFIG[:activation_amount] + @tel.price)} to your balance or enter your referral code if you have one.
+ (If you'd like to pay in another cryptocurrency, currently we recommend using a service like simpleswap.io, morphtoken.com, changenow.io, or godex.io.)
+ I
+ end
+
def write
Command.reply { |reply|
reply.command << FormTemplate.render(
- "registration/buy_number",
- tel: @tel
+ "registration/pay_without_code",
+ product: @tel,
+ instructions: instructions,
+ title: "Purchase Number"
)
}.then(&method(:parse)).then(&:write)
end
@@ -637,7 +747,7 @@ class Registration
TEL_SELECTIONS.delete(@customer.jid).then {
TEL_SELECTIONS[@customer.jid]
}.then { |choose|
- choose.choose_tel(
+ choose.choose_tel_or_data(
error: "The JMP number #{@tel} is no longer available."
)
}.then { |tel| Finish.new(@customer, tel).write }
@@ -903,4 +1013,13 @@ class Registration
end
end
end
+
+ def self.public_onboarding_invite
+ Command.finish { |reply|
+ reply.allowed_actions = [:prev]
+ oob = OOB.find_or_create(reply.command)
+ oob.url = CONFIG[:public_onboarding_url]
+ oob.desc = "Get your XMPP account"
+ }
+ end
end
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class SIMKind
+ attr_reader :klass
+
+ def initialize(variant)
+ @klass =
+ case variant
+ when "sim"
+ SIMOrder
+ when "esim"
+ SIMOrder::ESIM
+ end
+ @variant = variant
+ end
+
+ def self.from_form(form)
+ new(form.field("sim_kind")&.value)
+ end
+
+ def cfg(currency)
+ CONFIG.dig(:sims, @variant.to_sym, currency)
+ end
+end
@@ -3,9 +3,39 @@
require "bigdecimal/util"
require_relative "low_balance"
+require_relative "registration"
require_relative "transaction"
+require_relative "onboarding"
+
+module SIMAction
+ # @param can_complete [TrueClass, FalseClass]
+ def process(can_complete: true)
+ Command.reply { |reply|
+ # Users shouldn't be allowed to abort in the middle
+ # of any flow that ends in them getting onboarded
+ if can_complete && !@customer.jid.onboarding?
+ reply.allowed_actions = [:complete]
+ end
+ reply.command << form
+ }.then { |iq| complete(iq) }
+ end
+end
+
+module SIMFinish
+ def finish
+ complete.then { |_iq|
+ if @customer.jid.onboarding?
+ Registration.public_onboarding_invite
+ else
+ Command.finish
+ end
+ }
+ end
+end
class SIMOrder
+ include SIMAction
+
def self.for(customer, price:, **kwargs)
price = price.to_i / 100.to_d
return new(customer, price: price, **kwargs) if customer.balance >= price
@@ -66,25 +96,36 @@ class SIMOrder
@customer,
sim,
Array(form.field("addr").value).join("\n")
- ).complete
+ ).write
end
end
class Ack
+ include SIMFinish
+
def initialize(customer, sim, addr)
@customer = customer
@sim = sim
@addr = addr
end
- def complete
+ def write
+ notify.then { finish }
+ end
+
+ def notify
@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}"
))
- Command.finish(
- "You will receive an notice from support when your SIM ships."
- )
+ end
+
+ def complete
+ Command.reply { |iq|
+ iq.note_type = :info
+ iq.note_text =
+ "You will receive a notice from support when your SIM ships."
+ }
end
end
@@ -137,31 +178,37 @@ protected
iq.form.field("nickname")&.value.presence || self.class.label
)
}.then do |sim|
- ActivationCode.new(sim).complete
+ ActivationCode.new(@customer, sim).finish
end
end
end
class ActivationCode
+ include SIMFinish
+
+ # @param [Customer] customer the customer who ordered
# @param [Sim] sim the sim which the customer
# just ordered
- def initialize(sim)
+ def initialize(customer, sim)
+ @customer = customer
@sim = sim
end
def complete
- Command.finish do |reply|
+ Command.reply { |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
class WithTopUp
+ include SIMAction
+
def initialize(customer, continue, price:, plan:, top_up:)
@customer = customer
@price = price
@@ -193,6 +240,8 @@ protected
end
class PleaseTopUp
+ include SIMAction
+
def initialize(price:, plan:)
@price = price
@plan = plan
@@ -8,6 +8,17 @@ require "countries"
require_relative "area_code_repo"
require_relative "form_template"
+require_relative "sim_kind"
+
+module NullReserve
+ def reserve(*)
+ EMPromise.resolve(nil)
+ end
+end
+
+SIMKind.class_eval do
+ include NullReserve
+end
class TelSelections
THIRTY_DAYS = 60 * 60 * 24 * 30
@@ -46,7 +57,7 @@ class TelSelections
@tel = ChooseTel::Tn.for_pending_value(tel)
end
- def choose_tel
+ def choose_tel_or_data
EMPromise.resolve(@tel)
end
end
@@ -59,18 +70,32 @@ class TelSelections
@memcache = memcache
end
- def choose_tel(error: nil)
+ def choose_tel_or_data(error: nil)
Command.reply { |reply|
reply.allowed_actions = [:next]
reply.command << FormTemplate.render("tn_search", error: error)
- }.then do |iq|
- available = AvailableNumber.for(iq.form, db: @db, memcache: @memcache)
- next available if available.is_a?(Tn::Bandwidth)
+ }.then { |iq|
+ response = iq.form.field(
+ "http://jabber.org/protocol/commands#actions"
+ )&.value.to_s.strip
+ response == "data_only" ? choose_sim_kind : choose_tel(iq)
+ }
+ end
- choose_from_list(available.tns)
- rescue Fail
- choose_tel(error: $!.to_s)
- end
+ def choose_sim_kind
+ Command.reply { |reply|
+ reply.command << FormTemplate.render("registration/choose_sim_kind")
+ reply.allowed_actions = [:cancel, :next]
+ }.then { |iq| SIMKind.from_form(iq.form) }
+ end
+
+ def choose_tel(iq)
+ available = AvailableNumber.for(iq.form, db: @db, memcache: @memcache)
+ return available if available.is_a?(Tn::Bandwidth)
+
+ choose_from_list(available.tns)
+ rescue Fail
+ choose_tel_or_data(error: $!.to_s)
end
def choose_from_list(tns)
@@ -86,7 +111,7 @@ class TelSelections
def choose_from_list_result(tns, iq)
tel = iq.form.field("tel")&.value
- return choose_tel if iq.prev? || !tel
+ return choose_tel_or_data if iq.prev? || !tel
tns.find { |tn| tn.tel == tel } || Tn::Bandwidth.new(Tn.new(tel))
end
@@ -296,6 +321,8 @@ class TelSelections
end
class LocalInventory < SimpleDelegator
+ include NullReserve
+
attr_reader :price
def initialize(tn, bandwidth_account_id, price: 0)
@@ -352,10 +379,6 @@ class TelSelections
"LocalInventory/#{tel}/#{@bandwidth_account_id}/#{price}"
end
- def reserve(*)
- EMPromise.resolve(nil)
- end
-
def order(db, _customer)
# Move always moves to wrong account, oops
# Also probably can't move from/to same account
@@ -784,12 +784,7 @@ Command.new(
else
Command.finish
end
- }.then { |action|
- Command.reply { |reply|
- reply.allowed_actions = [:complete]
- reply.command << action.form
- }.then(&action.method(:complete))
- }
+ }.then(&:process)
end
}.register(self).then(&CommandList.method(:register))
@@ -139,6 +139,16 @@ CONFIG = {
],
credit_card_url: ->(*) { "http://creditcard.example.com?" },
electrum_notify_url: ->(*) { "http://notify.example.com" },
+ sims: {
+ sim: {
+ USD: { price: 500, plan: "1GB" },
+ CAD: { price: 600, plan: "1GB" }
+ },
+ esim: {
+ USD: { price: 300, plan: "500MB" },
+ CAD: { price: 400, plan: "500MB" }
+ }
+ },
keep_area_codes: ["556"],
keep_area_codes_in: {
account: "moveto",
@@ -167,7 +177,8 @@ CONFIG = {
bulk_order_tokens: {
sometoken: { customer_id: "bulkcustomer", peer_id: "bulkpeer" },
lowtoken: { customer_id: "customerid_low", peer_id: "lowpeer" }
- }
+ },
+ public_onboarding_url: "xmpp:example.com?register"
}.freeze
def panic(e)
@@ -2197,4 +2197,215 @@ class RegistrationTest < Minitest::Test
end
em :test_write_needs_dns
end
+
+ class RegistrationTypeTest < Minitest::Test
+ def test_for_with_sim_kind
+ cust = customer(plan_name: "test_usd").with_balance(1000)
+ sim_kind = SIMKind.new("sim")
+ result = Registration::RegistrationType.for(cust, nil, sim_kind)
+ assert_kind_of Registration::DataOnly, result
+ end
+
+ def test_for_with_esim_kind
+ cust = customer(plan_name: "test_usd").with_balance(1000)
+ sim_kind = SIMKind.new("esim")
+ result = Registration::RegistrationType.for(cust, nil, sim_kind)
+ assert_kind_of Registration::DataOnly, result
+ end
+
+ def test_for_with_telephone_number
+ cust = customer(plan_name: "test_usd", expires_at: Time.now + 999)
+ tel = TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
+ result = Registration::RegistrationType.for(cust, nil, tel)
+ assert_kind_of Registration::Finish, result
+ end
+ end
+
+ class DataOnlyTest < Minitest::Test
+ def setup
+ @customer = customer(plan_name: "test_usd").with_balance(1000)
+ @sim_kind = SIMKind.new("sim")
+ end
+
+ def test_for_returns_pay_for_sim_when_balance_insufficient
+ low_balance_customer = @customer.with_balance(0.50)
+ result = Registration::DataOnly.for(low_balance_customer, @sim_kind)
+ assert_kind_of Registration::PayForSim, result
+ end
+
+ def test_for_returns_pay_for_sim_when_config_missing
+ no_currency_customer = customer(currency: nil)
+ result = Registration::DataOnly.for(no_currency_customer, @sim_kind)
+ assert_kind_of Registration::PayForSim, result
+ end
+
+ def test_for_returns_data_only_when_balance_sufficient
+ result =
+ Registration::DataOnly.for(@customer, @sim_kind)
+ assert_kind_of Registration::DataOnly, result
+ end
+
+ def test_write_calls_sim_kind_for_and_process
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |iq|
+ assert_equal iq.form.title, "Order SIM"
+ assert(
+ iq.form.instructions.start_with?("Our SIMs provide a plan")
+ )
+ end]
+ )
+ Registration::DataOnly.new(
+ @customer,
+ @sim_kind
+ ).write.catch { |e| e }.sync
+ end
+ assert_equal result, :test_result
+ assert_mock Command::COMMAND_MANAGER
+ end
+ em :test_write_calls_sim_kind_for_and_process
+ end
+
+ class PayForSimTest < Minitest::Test
+ Command::COMMAND_MANAGER = Minitest::Mock.new
+ Registration::PayForSim::Payment = Minitest::Mock.new
+
+ def setup
+ @customer = customer(plan_name: "test_usd")
+ @sim_kind = SIMKind.new("sim")
+ @pay_for_sim = Registration::PayForSim.new(@customer, @sim_kind)
+ end
+
+ def test_write_displays_payment_form
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "credit_card" }
+ ]
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(iq),
+ [Matching.new do |reply|
+ assert_equal :form, reply.form.type
+ assert_equal "Pay for SIM", reply.form.title
+ end]
+ )
+
+ ejector_mock = Minitest::Mock.new
+ ejector_mock.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ []
+ )
+ Registration::PayForSim::Payment.expect(
+ :for,
+ ejector_mock
+ ) do |*, price:, maybe_bill:|
+ assert_equal 500, price
+ assert_equal(
+ Registration::Payment::JustCharge,
+ maybe_bill
+ )
+ end
+ @pay_for_sim.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ end
+ em :test_write_displays_payment_form
+
+ def test_write_with_no_currency_shows_plan_selection
+ no_currency_customer = customer(currency: nil)
+ pay_for_sim = Registration::PayForSim.new(no_currency_customer, @sim_kind)
+
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "plan_name", value: "test_usd" },
+ { var: "activation_method", value: "credit_card" }
+ ]
+
+ result = execute_command do
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.resolve(iq),
+ [Matching.new do |reply|
+ assert_equal :form, reply.form.type
+ assert_equal "Pay for SIM", reply.form.title
+ end]
+ )
+
+ CustomerPlan::DB.expect(
+ :exec_defer,
+ EMPromise.resolve(nil),
+ [String, ["test", "test_usd", nil]]
+ )
+ ejector_mock = Minitest::Mock.new
+ ejector_mock.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ []
+ )
+ Registration::PayForSim::Payment.expect(
+ :for,
+ ejector_mock
+ ) do |*, price:, maybe_bill:|
+ assert_equal 500, price
+ assert_equal(
+ Registration::Payment::JustCharge,
+ maybe_bill
+ )
+ end
+ pay_for_sim.write.catch { |e| e }
+ end
+
+ assert_equal :test_result, result
+ assert_mock Command::COMMAND_MANAGER
+ assert_mock CustomerPlan::DB
+ end
+ em :test_write_with_no_currency_shows_plan_selection
+
+ def test_process_payment_uses_just_charge_strategy
+ iq = Blather::Stanza::Iq::Command.new
+ iq.form.fields = [
+ { var: "activation_method", value: "credit_card" }
+ ]
+
+ reloaded_customer = @customer.with_balance(100).with_plan("test_usd")
+
+ result = execute_command do |exe|
+ payment = Minitest::Mock.new
+ payment.expect(
+ :write,
+ EMPromise.resolve(nil),
+ []
+ )
+ Registration::PayForSim::Payment.expect(
+ :for,
+ payment
+ ) do |_, customer, sim_kind, price:, maybe_bill:|
+ assert_equal @customer, customer
+ assert_equal @sim_kind, sim_kind
+ assert_equal Registration::Payment::JustCharge, maybe_bill
+ assert_equal 500, price
+ end
+ exe.customer_repo.expect(
+ :find,
+ EMPromise.resolve(reloaded_customer),
+ [@customer.customer_id]
+ )
+
+ result_ = @pay_for_sim.process_payment(iq).sync
+ assert_mock payment
+ assert_mock Command.execution.customer_repo
+ assert_mock Registration::PayForSim::Payment
+ result_
+ end
+ assert_kind_of Registration::PayForSim, result
+ end
+ em :test_process_payment_uses_just_charge_strategy
+ end
end
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+require "ostruct"
+require "customer"
+
+require_relative "../lib/sim_order"
+require_relative "../lib/sim_kind"
+
+class SIMKindTest < Minitest::Test
+ def setup
+ @sim_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [{ var: "sim_kind", value: "sim" }]
+ }
+
+ @esim_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [{ var: "sim_kind", value: "esim" }]
+ }
+
+ @invalid_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = [{ var: "sim_kind", value: "invalid" }]
+ }
+
+ @missing_form = Blather::Stanza::Iq::Command.new.tap { |iq|
+ iq.form.fields = []
+ }
+ end
+
+ def test_initialize_with_sim
+ kind = SIMKind.new("sim")
+ assert_equal SIMOrder, kind.klass
+ end
+
+ def test_initialize_with_esim
+ kind = SIMKind.new("esim")
+ assert_equal SIMOrder::ESIM, kind.klass
+ end
+
+ def test_initialize_with_invalid_variant
+ kind = SIMKind.new("invalid")
+ assert_nil kind.klass
+ end
+
+ def test_initialize_with_nil
+ kind = SIMKind.new(nil)
+ assert_nil kind.klass
+ end
+
+ def test_from_form_with_sim
+ kind = SIMKind.from_form(@sim_form.form)
+ assert_equal SIMOrder, kind.klass
+ end
+
+ def test_from_form_with_esim
+ kind = SIMKind.from_form(@esim_form.form)
+ assert_equal SIMOrder::ESIM, kind.klass
+ end
+
+ def test_cfg_returns_nil_when_customer_currency_nil
+ kind = SIMKind.new("sim")
+ cust = customer(currency: nil)
+ assert_nil kind.cfg(cust.currency)
+ end
+
+ def test_cfg_returns_nil_when_config_missing_variant
+ kind = SIMKind.new("invalid")
+ cust = customer(currency: :USD)
+ assert_nil kind.cfg(cust.currency)
+ end
+
+ def test_cfg_returns_nil_when_config_missing_currency
+ kind = SIMKind.new("sim")
+ cust = customer(currency: :EUR)
+ assert_nil kind.cfg(cust.currency)
+ end
+end
@@ -62,157 +62,164 @@ class SIMOrderTest < Minitest::Test
end
def test_complete_nil_nick
- customer = Minitest::Mock.new(customer("123", balance: 100))
- customer.expect(
- :stanza_from,
- EMPromise.resolve(nil),
- [Blather::Stanza::Message]
- )
-
- 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
+ execute_command {
+ customer = Minitest::Mock.new(customer("123", balance: 100))
+ customer.expect(
+ :stanza_from,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Message]
+ )
- order_form = Blather::Stanza::Iq::Command.new.tap { |iq|
- iq.form.fields = [
- { var: "addr", value: "123 Main St" },
- { var: "nickname", value: nil }
- ]
- }
+ 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]
+ }
+ ]
+ )
- 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
+ @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")
)
- end]
- )
- sim_order = SIMOrder.for(customer, price: @price, plan: @plan_name)
- sim_order.instance_variable_set(:@sim_repo, @sim_repo)
+ ) 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 }
+ ]
+ }
+
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |reply|
+ assert_equal :executing, reply.status
+ assert_equal :info, reply.note_type
+ assert_equal(
+ "You will receive a notice from support when your SIM ships.",
+ reply.note.content
+ )
+ end]
+ )
- assert_equal(
- :test_result,
- execute_command(blather: blather) { sim_order.complete(order_form) }
- )
+ sim_order = SIMOrder.for(customer, price: @price, plan: @plan_name)
+ sim_order.instance_variable_set(:@sim_repo, @sim_repo)
+
+ assert_equal(
+ :test_result,
+ sim_order.complete(order_form).catch { |e| e }.sync
+ )
- assert_mock Transaction::DB
- assert_mock SIMOrder::DB
- assert_mock customer
- assert_mock @sim_repo
- assert_mock blather
+ assert_mock Transaction::DB
+ assert_mock SIMOrder::DB
+ assert_mock customer
+ assert_mock @sim_repo
+ }
end
em :test_complete_nil_nick
def test_complete_nick_present
- customer = Minitest::Mock.new(customer("123", balance: 100))
- customer.expect(
- :stanza_from,
- EMPromise.resolve(nil),
- [Blather::Stanza::Message]
- )
- 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]
- }
- ]
- )
+ execute_command {
+ Command::COMMAND_MANAGER.expect(
+ :write,
+ EMPromise.reject(:test_result),
+ [Matching.new do |reply|
+ assert_equal :info, reply.note_type
+ assert_equal :executing, reply.status
+ assert_equal(
+ "You will receive a notice from support when your SIM ships.",
+ reply.note.content
+ )
+ end]
+ )
- SIMOrder::DB.expect(
- :transaction,
- @sim
- ) do |&blk|
- blk.call
- end
+ customer = Minitest::Mock.new(customer("123", balance: 100))
+ customer.expect(
+ :stanza_from,
+ EMPromise.resolve(nil),
+ [Blather::Stanza::Message]
+ )
+ 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, "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
+ 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" }
+ ]
+ }
+
+ sim_order =
+ SIMOrder.for(customer, price: @price, plan: @plan_name)
+ sim_order.instance_variable_set(:@sim_repo, @sim_repo)
- order_form = Blather::Stanza::Iq::Command.new.tap { |iq|
- iq.form.fields = [
- { var: "addr", value: "123 Main St" },
- { var: "nickname", value: "test_nick" }
- ]
- }
+ assert_equal(
+ :test_result,
+ sim_order.complete(order_form).catch { |e| e }.sync
+ )
- 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
+ assert_mock Transaction::DB
+ assert_mock SIMOrder::DB
+ assert_mock customer
+ assert_mock @sim_repo
+ }
end
em :test_complete_nick_present
end
@@ -28,7 +28,10 @@ class TelSelectionsTest < Minitest::Test
jid,
TelSelections::ChooseTel::Tn.for_pending_value("+15555550000")
).sync
- assert_equal "+15555550000", @manager[jid].then(&:choose_tel).sync.tel
+ assert_equal(
+ "+15555550000",
+ @manager[jid].then(&:choose_tel_or_data).sync.tel
+ )
end
em :test_choose_tel_have_tel