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