test_sim_job.rb

  1# frozen_string_literal: true
  2
  3require "bigdecimal"
  4require "em-http"
  5require "test_helper"
  6require "customer"
  7require "low_balance"
  8require "sim"
  9
 10load File.expand_path("../bin/sim_job", __dir__)
 11
 12class SimJobTest < Minitest::Test
 13	class Ledger
 14		attr_reader :charges
 15
 16		def initialize(balances)
 17			@balances = balances
 18			@charges = []
 19		end
 20
 21		def balance_for(customer_id)
 22			@balances.fetch(customer_id, BigDecimal("0"))
 23		end
 24
 25		def charge(customer_id, amount)
 26			@balances[customer_id] = balance_for(customer_id) - amount
 27			@charges << [customer_id, amount]
 28		end
 29	end
 30
 31	class CustomerRepo
 32		def initialize(customers, ledger)
 33			@customers = customers
 34			@ledger = ledger
 35		end
 36
 37		def find(customer_id)
 38			customer = @customers.fetch(customer_id)
 39			EMPromise.resolve(customer.with(balance: balance_for(customer)))
 40		end
 41
 42		def balance_for(customer)
 43			@ledger.balance_for(customer.billing_customer_id)
 44		end
 45	end
 46
 47	class RecordingAction
 48		attr_accessor :customer
 49		attr_reader :balances_seen, :iccid
 50
 51		def initialize(iccid, customer, ledger:, price: BigDecimal("5"), error: nil)
 52			@iccid = iccid
 53			@customer = customer
 54			@ledger = ledger
 55			@price = price
 56			@error = error
 57			@balances_seen = []
 58		end
 59
 60		def call
 61			@balances_seen << customer.balance
 62			return EMPromise.reject(@error) if @error
 63
 64			if customer.balance >= @price
 65				@ledger.charge(customer.billing_customer_id, @price)
 66			end
 67			EMPromise.resolve(nil)
 68		end
 69	end
 70
 71	class SuccessfulLowBalance
 72		def initialize(amount)
 73			@amount = amount
 74		end
 75
 76		def notify!
 77			EMPromise.resolve(@amount)
 78		end
 79	end
 80
 81	def test_process_sim_actions_reloads_balance_between_actions
 82		ledger = Ledger.new("parent" => BigDecimal("5"))
 83		repo = CustomerRepo.new(
 84			{
 85				"parent" => customer("parent"),
 86				"first" => customer("first", parent_customer_id: "parent"),
 87				"second" => customer("second", parent_customer_id: "parent")
 88			},
 89			ledger
 90		)
 91		first = RecordingAction.new(
 92			"first", customer("first", parent_customer_id: "parent"), ledger: ledger
 93		)
 94		second = RecordingAction.new(
 95			"second", customer("second", parent_customer_id: "parent"), ledger: ledger
 96		)
 97
 98		process_sim_actions([first, second], customer_repo: repo).sync
 99
100		assert_equal [BigDecimal("5")], first.balances_seen
101		assert_equal [BigDecimal("0")], second.balances_seen
102		assert_equal [["parent", BigDecimal("5")]], ledger.charges
103	end
104	em :test_process_sim_actions_reloads_balance_between_actions
105
106	def test_process_sim_actions_continues_after_failure
107		ledger = Ledger.new("first" => BigDecimal("5"), "second" => BigDecimal("5"))
108		repo = CustomerRepo.new(
109			{
110				"first" => customer("first"),
111				"second" => customer("second")
112			},
113			ledger
114		)
115		error = RuntimeError.new("boom")
116		first = RecordingAction.new(
117			"first", customer("first"), ledger: ledger, error: error
118		)
119		second = RecordingAction.new("second", customer("second"), ledger: ledger)
120
121		process_sim_actions([first, second], customer_repo: repo).sync
122
123		assert_equal [BigDecimal("5")], first.balances_seen
124		assert_equal [BigDecimal("5")], second.balances_seen
125		assert_equal [["second", BigDecimal("5")]], ledger.charges
126	end
127	em :test_process_sim_actions_continues_after_failure
128
129	def test_job_customer_preserves_billing_customer_when_balance_changes
130		parent = customer(
131			"parent",
132			balance: BigDecimal("45"),
133			auto_top_up_amount: 15
134		)
135		child = customer(
136			"child",
137			parent_customer_id: "parent",
138			balance: BigDecimal("0")
139		)
140
141		updated = JobCustomer.new(child, billing_customer: parent)
142			.with(balance: BigDecimal("40"))
143		billing_customer = updated.billing_customer.sync
144
145		assert_kind_of JobCustomer, updated
146		assert_equal "child", updated.customer_id
147		assert_equal BigDecimal("40"), updated.balance
148		assert_kind_of JobCustomer, billing_customer
149		assert_equal "parent", billing_customer.customer_id
150		assert_equal BigDecimal("40"), billing_customer.balance
151	end
152	em :test_job_customer_preserves_billing_customer_when_balance_changes
153
154	def test_sim_top_up_refills_when_refill_exactly_fits_monthly_limit
155		top_up = SimTopUp.new(sim, customer: customer(balance: BigDecimal("100")))
156		refill = nil
157
158		top_up.stub(:amount_spent, EMPromise.resolve(BigDecimal("1"))) do
159			top_up.stub(:monthly_limit, EMPromise.resolve(BigDecimal("46"))) do
160				top_up.stub(:refill_price, BigDecimal("45")) do
161					top_up.stub(:refill_and_bill, lambda { |*args|
162						refill = args
163						EMPromise.resolve(nil)
164					}) do
165						top_up.call.sync
166					end
167				end
168			end
169		end
170
171		assert_equal(
172			[5120, BigDecimal("45"), "5GB Data Topup for 123"],
173			refill
174		)
175	end
176	em :test_sim_top_up_refills_when_refill_exactly_fits_monthly_limit
177
178	def test_sim_top_up_warns_when_refill_would_exceed_monthly_limit
179		top_up = SimTopUp.new(sim, customer: customer(balance: BigDecimal("100")))
180		warned = false
181		refilled = false
182
183		top_up.stub(:amount_spent, EMPromise.resolve(BigDecimal("45"))) do
184			top_up.stub(:monthly_limit, EMPromise.resolve(BigDecimal("46"))) do
185				top_up.stub(:refill_price, BigDecimal("45")) do
186					top_up.stub(:refill_and_bill, lambda { |*|
187						refilled = true
188						EMPromise.resolve(nil)
189					}) do
190						top_up.stub(:warn, lambda {
191							warned = true
192							EMPromise.resolve(nil)
193						}) do
194							top_up.call.sync
195						end
196					end
197				end
198			end
199		end
200
201		assert warned
202		refute refilled
203	end
204	em :test_sim_top_up_warns_when_refill_would_exceed_monthly_limit
205
206	def test_sim_annual_retries_with_low_balance_result_added_to_balance
207		annual = SimAnnual.new(sim, customer: customer(balance: BigDecimal("0")))
208		balances_billed = []
209		low_balance = SuccessfulLowBalance.new(BigDecimal("10"))
210
211		LowBalance.stub(:for, EMPromise.resolve(low_balance)) do
212			annual.stub(:annual_price, BigDecimal("10")) do
213				annual.stub(:refill_and_bill, lambda { |*|
214					balances_billed << annual.customer.balance
215					EMPromise.resolve(nil)
216				}) do
217					annual.call.sync
218				end
219			end
220		end
221
222		assert_equal [BigDecimal("10")], balances_billed
223	end
224	em :test_sim_annual_retries_with_low_balance_result_added_to_balance
225
226	def sim
227		SIM.new(
228			iccid: "123",
229			remaining_usage_kb: 1,
230			remaining_days: 365,
231			notes: ""
232		)
233	end
234end