sim_order.rb

  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