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		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