1# frozen_string_literal: true
2
3require "bigdecimal/util"
4
5require_relative "low_balance"
6require_relative "registration"
7require_relative "transaction"
8require_relative "onboarding"
9
10module SIMAction
11 # @param can_complete [TrueClass, FalseClass]
12 def process(can_complete: true)
13 Command.reply { |reply|
14 # Users shouldn't be allowed to abort in the middle
15 # of any flow that ends in them getting onboarded
16 if can_complete && !@customer.jid.onboarding?
17 reply.allowed_actions = [:complete]
18 end
19 reply.command << form
20 }.then { |iq| complete(iq) }
21 end
22end
23
24module SIMFinish
25 def finish
26 complete.then { |_iq|
27 if @customer.jid.onboarding?
28 Registration.public_onboarding_invite
29 else
30 Command.finish
31 end
32 }
33 end
34end
35
36class SIMOrder
37 include SIMAction
38
39 def self.for(customer, price:, **kwargs)
40 price = price.to_i / 100.to_d
41 log.debug("SIMOrder.for", balance: customer.balance, price: price)
42 return new(customer, price: price, **kwargs) if customer.balance >= price
43
44 LowBalance::AutoTopUp.for(customer, price).then do |top_up|
45 if top_up.can_top_up?
46 WithTopUp.new(customer, self, price: price, top_up: top_up, **kwargs)
47 else
48 PleaseTopUp.new(price: price, customer: customer, **kwargs)
49 end
50 end
51 end
52
53 def self.label
54 "SIM"
55 end
56
57 def self.fillable_fields
58 [
59 {
60 type: "text-multi",
61 label: "Shipping Address",
62 var: "addr",
63 required: true
64 },
65 {
66 type: "text-single",
67 var: "nickname",
68 label: "Nickname",
69 required: false
70 }
71 ]
72 end
73
74 def initialize(customer, price:, plan:)
75 @customer = customer
76 @price = price
77 @plan = plan
78 @sim_repo = SIMRepo.new(db: DB)
79 end
80
81 def form
82 FormTemplate.render(
83 "order_sim/with_balance",
84 price: @price,
85 plan: @plan,
86 label: self.class.label,
87 fillable_fields: self.class.fillable_fields
88 )
89 end
90
91 def complete(iq)
92 form = iq.form
93 EMPromise.resolve(nil).then {
94 commit(form.field("nickname")&.value.presence || self.class.label)
95 }.then do |sim|
96 Ack.new(
97 @customer,
98 sim,
99 Array(form.field("addr").value).join("\n")
100 ).write
101 end
102 end
103
104 class Ack
105 include SIMFinish
106
107 def initialize(customer, sim, addr)
108 @customer = customer
109 @sim = sim
110 @addr = addr
111 end
112
113 def write
114 notify.then { finish }
115 end
116
117 def notify
118 @customer.stanza_from(Blather::Stanza::Message.new(
119 Blather::JID.new(""), # Doesn't matter, sgx is set to direct target
120 "SIM ORDER: #{@sim.iccid}\n#{@addr}"
121 ))
122 end
123
124 def complete
125 Command.reply { |iq|
126 iq.note_type = :info
127 iq.note_text =
128 "You will receive a notice from support when your SIM ships."
129 }
130 end
131 end
132
133protected
134
135 # @param [String, nil] nickname the nickname, if any, assigned to
136 # the sim by the customer
137 def commit(nickname)
138 DB.transaction do
139 sim = @sim_repo.available.sync
140 @sim_repo.put_owner(sim, @customer, nickname)
141 keepgo_tx = @sim_repo.refill(sim, amount_mb: 1024).sync
142 raise "SIM activation failed" unless keepgo_tx["ack"] == "success"
143
144 transaction(sim, keepgo_tx).insert_tx
145 sim
146 end
147 end
148
149 def transaction(sim, keepgo_tx)
150 Transaction.new(
151 customer_id: @customer.customer_id,
152 transaction_id: keepgo_tx["transaction_id"],
153 amount: -@price,
154 note: "#{self.class.label} Activation #{sim.iccid}"
155 )
156 end
157
158 class ESIM < SIMOrder
159 def self.label
160 "eSIM"
161 end
162
163 def self.fillable_fields
164 [
165 {
166 type: "text-single",
167 var: "nickname",
168 label: "Nickname",
169 required: false
170 }
171 ]
172 end
173
174 # @param [Blather::Stanza::Iq] iq the stanza
175 # containing a filled out `order_sim/with_balance`
176 def complete(iq)
177 EMPromise.resolve(nil).then {
178 commit(
179 iq.form.field("nickname")&.value.presence || self.class.label
180 )
181 }.then do |sim|
182 ActivationCode.new(@customer, sim).finish
183 end
184 end
185 end
186
187 class ActivationCode
188 include SIMFinish
189
190 # @param [Customer] customer the customer who ordered
191 # @param [Sim] sim the sim which the customer
192 # just ordered
193 def initialize(customer, sim)
194 @customer = customer
195 @sim = sim
196 end
197
198 def complete
199 Command.reply { |reply|
200 oob = OOB.find_or_create(reply.command)
201 oob.url = @sim.lpa_code
202 oob.desc = "LPA Activation Code"
203 reply.command << FormTemplate.render(
204 "order_sim/esim_complete", sim: @sim
205 )
206 }
207 end
208 end
209
210 class WithTopUp
211 include SIMAction
212
213 def initialize(customer, continue, price:, plan:, top_up:)
214 @customer = customer
215 @price = price
216 @plan = plan
217 @top_up = top_up
218 @continue = continue
219 end
220
221 def form
222 FormTemplate.render(
223 "order_sim/with_top_up",
224 price: @price,
225 plan: @plan,
226 top_up_amount: @top_up.top_up_amount,
227 label: @continue.label,
228 fillable_fields: @continue.fillable_fields
229 )
230 end
231
232 def complete(iq)
233 @top_up.notify!.then do |amount|
234 if amount.positive?
235 @continue.new(@customer, price: @price, plan: @plan).complete(iq)
236 else
237 Command.finish("Could not top up", type: :error)
238 end
239 end
240 end
241 end
242
243 class PleaseTopUp
244 include SIMAction
245
246 def initialize(price:, plan:, customer:)
247 @price = price
248 @plan = plan
249 @customer = customer
250 end
251
252 def form
253 FormTemplate.render(
254 "order_sim/please_top_up",
255 price: @price,
256 plan: @plan
257 )
258 end
259
260 def complete(_)
261 Command.finish
262 end
263 end
264end