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