clover_sandbox_simulator 1.0.0 ā 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/simulate +283 -6
- data/lib/clover_sandbox_simulator/configuration.rb +129 -2
- data/lib/clover_sandbox_simulator/data/restaurant/combos.json +242 -0
- data/lib/clover_sandbox_simulator/data/restaurant/coupon_codes.json +266 -0
- data/lib/clover_sandbox_simulator/data/restaurant/discounts.json +166 -7
- data/lib/clover_sandbox_simulator/data/restaurant/gift_cards.json +82 -0
- data/lib/clover_sandbox_simulator/generators/data_loader.rb +59 -0
- data/lib/clover_sandbox_simulator/generators/order_generator.rb +583 -54
- data/lib/clover_sandbox_simulator/parallel_executor.rb +319 -0
- data/lib/clover_sandbox_simulator/services/clover/discount_service.rb +786 -1
- data/lib/clover_sandbox_simulator/services/clover/ecommerce_service.rb +282 -0
- data/lib/clover_sandbox_simulator/services/clover/gift_card_service.rb +224 -0
- data/lib/clover_sandbox_simulator/services/clover/oauth_service.rb +265 -0
- data/lib/clover_sandbox_simulator/services/clover/order_service.rb +60 -3
- data/lib/clover_sandbox_simulator/services/clover/payment_service.rb +95 -2
- data/lib/clover_sandbox_simulator/services/clover/refund_service.rb +367 -0
- data/lib/clover_sandbox_simulator/services/clover/services_manager.rb +31 -0
- metadata +38 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15537c1595b19f0f0c351e4650b65ac23316538b7fbc5e3b55bb71c798cba25a
|
|
4
|
+
data.tar.gz: 6205e71eb9af6a625e5b95024156e2579fe8a687c274178f7a3aa255dc4a13ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c3407b953637dc1ddf428f5795befc91969b8c0fb99c67b86c15e93cc6c02a7a9567367991135724e4920426cbd1cd0dcf4c65227aef0fa9600e9510246c838
|
|
7
|
+
data.tar.gz: 6602102b44527baa4aca0181174d5732407c207bb6139588b010f0c06775b61c80421b31bf0ea6b4b089e1cfa1f74379a151effcc76d384c2fd6d51ec5678729
|
data/bin/simulate
CHANGED
|
@@ -9,6 +9,32 @@ module CloverSandboxSimulator
|
|
|
9
9
|
# Command-line interface for Clover Sandbox Simulator
|
|
10
10
|
class CLI < Thor
|
|
11
11
|
class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose logging"
|
|
12
|
+
class_option :merchant, type: :string, aliases: "-m", desc: "Merchant ID to use (from .env.json)"
|
|
13
|
+
class_option :merchant_index, type: :numeric, aliases: "-i", desc: "Merchant index to use (0-based, from .env.json)"
|
|
14
|
+
|
|
15
|
+
desc "merchants", "List available merchants from .env.json"
|
|
16
|
+
def merchants
|
|
17
|
+
puts "š½ļø Clover Sandbox Simulator - Available Merchants"
|
|
18
|
+
puts "=" * 60
|
|
19
|
+
|
|
20
|
+
config = CloverSandboxSimulator.configuration
|
|
21
|
+
merchants = config.available_merchants
|
|
22
|
+
|
|
23
|
+
if merchants.empty?
|
|
24
|
+
puts "No merchants found in .env.json"
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "\n#{'Index'.ljust(6)} #{'Merchant ID'.ljust(16)} #{'Name'.ljust(25)} #{'eComm'}"
|
|
29
|
+
puts "-" * 60
|
|
30
|
+
|
|
31
|
+
merchants.each_with_index do |m, idx|
|
|
32
|
+
ecomm = m[:has_ecommerce] ? "ā" : "-"
|
|
33
|
+
puts "#{idx.to_s.ljust(6)} #{m[:id].ljust(16)} #{m[:name][0..23].ljust(25)} #{ecomm}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
puts "\nUse: simulate <command> -i <index> or -m <merchant_id>"
|
|
37
|
+
end
|
|
12
38
|
|
|
13
39
|
desc "setup", "Set up restaurant entities (categories, items, discounts, employees, customers)"
|
|
14
40
|
option :business_type, type: :string, default: "restaurant", desc: "Business type (restaurant, retail, salon)"
|
|
@@ -26,13 +52,15 @@ module CloverSandboxSimulator
|
|
|
26
52
|
|
|
27
53
|
desc "generate", "Generate orders for today"
|
|
28
54
|
option :count, type: :numeric, aliases: "-n", desc: "Number of orders to generate"
|
|
55
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
|
|
56
|
+
desc: "Percentage of orders to refund (0-100, default 5)"
|
|
29
57
|
def generate
|
|
30
58
|
configure_logging
|
|
31
59
|
|
|
32
60
|
puts "š½ļø Clover Sandbox Simulator - Order Generation"
|
|
33
61
|
puts "=" * 50
|
|
34
62
|
|
|
35
|
-
generator = Generators::OrderGenerator.new
|
|
63
|
+
generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
|
|
36
64
|
count = options[:count]
|
|
37
65
|
|
|
38
66
|
orders = generator.generate_today(count: count)
|
|
@@ -41,14 +69,16 @@ module CloverSandboxSimulator
|
|
|
41
69
|
end
|
|
42
70
|
|
|
43
71
|
desc "day", "Generate a realistic full day of restaurant operations"
|
|
44
|
-
option :multiplier, type: :numeric, aliases: "-
|
|
72
|
+
option :multiplier, type: :numeric, aliases: "-x", default: 1.0, desc: "Order multiplier (0.5 = slow day, 2.0 = busy day)"
|
|
73
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
|
|
74
|
+
desc: "Percentage of orders to refund (0-100, default 5)"
|
|
45
75
|
def day
|
|
46
76
|
configure_logging
|
|
47
77
|
|
|
48
78
|
puts "š½ļø Clover Sandbox Simulator - Realistic Restaurant Day"
|
|
49
79
|
puts "=" * 50
|
|
50
80
|
|
|
51
|
-
generator = Generators::OrderGenerator.new
|
|
81
|
+
generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
|
|
52
82
|
orders = generator.generate_realistic_day(multiplier: options[:multiplier])
|
|
53
83
|
|
|
54
84
|
puts "\nā
Generated #{orders.size} orders!"
|
|
@@ -96,6 +126,8 @@ module CloverSandboxSimulator
|
|
|
96
126
|
desc "full", "Run full simulation (setup + generate orders)"
|
|
97
127
|
option :count, type: :numeric, aliases: "-n", desc: "Number of orders to generate"
|
|
98
128
|
option :business_type, type: :string, default: "restaurant", desc: "Business type"
|
|
129
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
|
|
130
|
+
desc: "Percentage of orders to refund (0-100, default 5)"
|
|
99
131
|
def full
|
|
100
132
|
configure_logging
|
|
101
133
|
|
|
@@ -109,7 +141,7 @@ module CloverSandboxSimulator
|
|
|
109
141
|
puts "\n"
|
|
110
142
|
|
|
111
143
|
# Generate orders
|
|
112
|
-
order_gen = Generators::OrderGenerator.new
|
|
144
|
+
order_gen = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
|
|
113
145
|
orders = order_gen.generate_today(count: options[:count])
|
|
114
146
|
|
|
115
147
|
puts "\nā
Full simulation complete!"
|
|
@@ -150,6 +182,7 @@ module CloverSandboxSimulator
|
|
|
150
182
|
employees = services.employee.get_employees
|
|
151
183
|
customers = services.customer.get_customers
|
|
152
184
|
discounts = services.discount.get_discounts
|
|
185
|
+
refunds = services.refund.fetch_refunds
|
|
153
186
|
|
|
154
187
|
puts "Categories: #{categories.size}"
|
|
155
188
|
puts "Menu Items: #{items.size}"
|
|
@@ -157,6 +190,7 @@ module CloverSandboxSimulator
|
|
|
157
190
|
puts "Employees: #{employees.size}"
|
|
158
191
|
puts "Customers: #{customers.size}"
|
|
159
192
|
puts "Discounts: #{discounts.size}"
|
|
193
|
+
puts "Refunds: #{refunds.size}"
|
|
160
194
|
|
|
161
195
|
puts "\nš Categories:"
|
|
162
196
|
categories.each { |c| puts " - #{c['name']}" }
|
|
@@ -165,19 +199,262 @@ module CloverSandboxSimulator
|
|
|
165
199
|
tenders.each { |t| puts " - #{t['label']}" }
|
|
166
200
|
end
|
|
167
201
|
|
|
202
|
+
desc "refunds", "List all refunds"
|
|
203
|
+
option :limit, type: :numeric, aliases: "-l", desc: "Maximum number of refunds to show"
|
|
204
|
+
def refunds
|
|
205
|
+
configure_logging
|
|
206
|
+
|
|
207
|
+
puts "š½ļø Clover Sandbox Simulator - Refunds"
|
|
208
|
+
puts "=" * 50
|
|
209
|
+
|
|
210
|
+
services = Services::Clover::ServicesManager.new
|
|
211
|
+
refunds = services.refund.fetch_refunds(limit: options[:limit])
|
|
212
|
+
|
|
213
|
+
if refunds.empty?
|
|
214
|
+
puts "No refunds found."
|
|
215
|
+
return
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
puts "Found #{refunds.size} refunds:\n\n"
|
|
219
|
+
|
|
220
|
+
total_amount = 0
|
|
221
|
+
refunds.each do |refund|
|
|
222
|
+
amount = refund["amount"] || 0
|
|
223
|
+
total_amount += amount
|
|
224
|
+
payment_id = refund.dig("payment", "id") || "N/A"
|
|
225
|
+
created_at = refund["createdTime"] ? Time.at(refund["createdTime"] / 1000).strftime("%Y-%m-%d %H:%M") : "N/A"
|
|
226
|
+
|
|
227
|
+
puts " ID: #{refund['id']}"
|
|
228
|
+
puts " Amount: $#{'%.2f' % (amount / 100.0)}"
|
|
229
|
+
puts " Payment: #{payment_id}"
|
|
230
|
+
puts " Created: #{created_at}"
|
|
231
|
+
puts ""
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
puts "-" * 40
|
|
235
|
+
puts "Total refunded: $#{'%.2f' % (total_amount / 100.0)}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
desc "refund", "Create a refund for a payment"
|
|
239
|
+
option :payment_id, type: :string, aliases: "-p", required: true, desc: "Payment ID to refund"
|
|
240
|
+
option :amount, type: :numeric, aliases: "-a", desc: "Amount in cents (omit for full refund)"
|
|
241
|
+
option :reason, type: :string, aliases: "-r", default: "customer_request",
|
|
242
|
+
desc: "Refund reason (customer_request, quality_issue, wrong_order, duplicate_charge)"
|
|
243
|
+
def refund
|
|
244
|
+
configure_logging
|
|
245
|
+
|
|
246
|
+
puts "š½ļø Clover Sandbox Simulator - Create Refund"
|
|
247
|
+
puts "=" * 50
|
|
248
|
+
|
|
249
|
+
services = Services::Clover::ServicesManager.new
|
|
250
|
+
|
|
251
|
+
if options[:amount]
|
|
252
|
+
puts "Creating partial refund of $#{'%.2f' % (options[:amount] / 100.0)}..."
|
|
253
|
+
result = services.refund.create_partial_refund(
|
|
254
|
+
payment_id: options[:payment_id],
|
|
255
|
+
amount: options[:amount],
|
|
256
|
+
reason: options[:reason]
|
|
257
|
+
)
|
|
258
|
+
else
|
|
259
|
+
puts "Creating full refund..."
|
|
260
|
+
result = services.refund.create_full_refund(
|
|
261
|
+
payment_id: options[:payment_id],
|
|
262
|
+
reason: options[:reason]
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
if result && result["id"]
|
|
267
|
+
puts "\nā
Refund created successfully!"
|
|
268
|
+
puts " Refund ID: #{result['id']}"
|
|
269
|
+
puts " Amount: $#{'%.2f' % ((result['amount'] || 0) / 100.0)}"
|
|
270
|
+
else
|
|
271
|
+
puts "\nā Failed to create refund."
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
168
275
|
desc "version", "Show version"
|
|
169
276
|
def version
|
|
170
277
|
puts "Clover Sandbox Simulator v1.0.0"
|
|
171
278
|
end
|
|
172
279
|
|
|
280
|
+
# ============================================
|
|
281
|
+
# Gift Card Commands
|
|
282
|
+
# ============================================
|
|
283
|
+
|
|
284
|
+
desc "gift_cards", "List all gift cards and their balances"
|
|
285
|
+
def gift_cards
|
|
286
|
+
configure_logging
|
|
287
|
+
|
|
288
|
+
puts "š Clover Sandbox Simulator - Gift Cards"
|
|
289
|
+
puts "=" * 50
|
|
290
|
+
|
|
291
|
+
services = Services::Clover::ServicesManager.new
|
|
292
|
+
cards = services.gift_card.fetch_gift_cards
|
|
293
|
+
|
|
294
|
+
if cards.empty?
|
|
295
|
+
puts "No gift cards found."
|
|
296
|
+
puts "\nCreate one with: simulate gift_card_create --amount 5000"
|
|
297
|
+
return
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
puts "\n#{'ID'.ljust(20)} #{'Card Number'.ljust(20)} #{'Balance'.rjust(12)} #{'Status'.ljust(10)}"
|
|
301
|
+
puts "-" * 65
|
|
302
|
+
|
|
303
|
+
total_balance = 0
|
|
304
|
+
cards.each do |card|
|
|
305
|
+
id = card["id"] || "N/A"
|
|
306
|
+
number = mask_card_number(card["cardNumber"] || "Unknown")
|
|
307
|
+
balance = card["balance"] || 0
|
|
308
|
+
status = card["status"] || "UNKNOWN"
|
|
309
|
+
|
|
310
|
+
total_balance += balance if status == "ACTIVE"
|
|
311
|
+
|
|
312
|
+
balance_str = "$#{'%.2f' % (balance / 100.0)}"
|
|
313
|
+
puts "#{id.ljust(20)} #{number.ljust(20)} #{balance_str.rjust(12)} #{status.ljust(10)}"
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
puts "-" * 65
|
|
317
|
+
puts "Total Active Balance: $#{'%.2f' % (total_balance / 100.0)}"
|
|
318
|
+
puts "\nā
Found #{cards.size} gift cards"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
desc "gift_card_create", "Create a new gift card"
|
|
322
|
+
option :amount, type: :numeric, aliases: "-a", required: true, desc: "Initial balance in cents (e.g., 5000 for $50)"
|
|
323
|
+
option :card_number, type: :string, aliases: "-n", desc: "Optional 16-digit card number"
|
|
324
|
+
def gift_card_create
|
|
325
|
+
configure_logging
|
|
326
|
+
|
|
327
|
+
puts "š Clover Sandbox Simulator - Create Gift Card"
|
|
328
|
+
puts "=" * 50
|
|
329
|
+
|
|
330
|
+
services = Services::Clover::ServicesManager.new
|
|
331
|
+
|
|
332
|
+
amount = options[:amount]
|
|
333
|
+
card_number = options[:card_number]
|
|
334
|
+
|
|
335
|
+
puts "Creating gift card with balance: $#{'%.2f' % (amount / 100.0)}"
|
|
336
|
+
|
|
337
|
+
result = services.gift_card.create_gift_card(
|
|
338
|
+
amount: amount,
|
|
339
|
+
card_number: card_number
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if result && result["id"]
|
|
343
|
+
puts "\nā
Gift card created successfully!"
|
|
344
|
+
puts " ID: #{result['id']}"
|
|
345
|
+
puts " Card Number: #{mask_card_number(result['cardNumber'])}"
|
|
346
|
+
puts " Balance: $#{'%.2f' % ((result['balance'] || amount) / 100.0)}"
|
|
347
|
+
puts " Status: #{result['status']}"
|
|
348
|
+
else
|
|
349
|
+
puts "\nā Failed to create gift card"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
desc "gift_card_balance", "Check gift card balance"
|
|
354
|
+
option :id, type: :string, aliases: "-i", required: true, desc: "Gift card ID"
|
|
355
|
+
def gift_card_balance
|
|
356
|
+
configure_logging
|
|
357
|
+
|
|
358
|
+
puts "š Clover Sandbox Simulator - Gift Card Balance"
|
|
359
|
+
puts "=" * 50
|
|
360
|
+
|
|
361
|
+
services = Services::Clover::ServicesManager.new
|
|
362
|
+
|
|
363
|
+
card = services.gift_card.get_gift_card(options[:id])
|
|
364
|
+
|
|
365
|
+
if card
|
|
366
|
+
puts "\nGift Card Details:"
|
|
367
|
+
puts " ID: #{card['id']}"
|
|
368
|
+
puts " Card Number: #{mask_card_number(card['cardNumber'])}"
|
|
369
|
+
puts " Balance: $#{'%.2f' % ((card['balance'] || 0) / 100.0)}"
|
|
370
|
+
puts " Status: #{card['status']}"
|
|
371
|
+
else
|
|
372
|
+
puts "\nā Gift card not found: #{options[:id]}"
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
desc "gift_card_reload", "Add balance to a gift card"
|
|
377
|
+
option :id, type: :string, aliases: "-i", required: true, desc: "Gift card ID"
|
|
378
|
+
option :amount, type: :numeric, aliases: "-a", required: true, desc: "Amount to add in cents"
|
|
379
|
+
def gift_card_reload
|
|
380
|
+
configure_logging
|
|
381
|
+
|
|
382
|
+
puts "š Clover Sandbox Simulator - Reload Gift Card"
|
|
383
|
+
puts "=" * 50
|
|
384
|
+
|
|
385
|
+
services = Services::Clover::ServicesManager.new
|
|
386
|
+
|
|
387
|
+
gc_id = options[:id]
|
|
388
|
+
amount = options[:amount]
|
|
389
|
+
|
|
390
|
+
puts "Adding $#{'%.2f' % (amount / 100.0)} to gift card #{gc_id}"
|
|
391
|
+
|
|
392
|
+
result = services.gift_card.reload_gift_card(gc_id, amount: amount)
|
|
393
|
+
|
|
394
|
+
if result
|
|
395
|
+
puts "\nā
Gift card reloaded successfully!"
|
|
396
|
+
puts " New Balance: $#{'%.2f' % ((result['balance'] || 0) / 100.0)}"
|
|
397
|
+
else
|
|
398
|
+
puts "\nā Failed to reload gift card"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
desc "gift_card_redeem", "Redeem/use balance from a gift card"
|
|
403
|
+
option :id, type: :string, aliases: "-i", required: true, desc: "Gift card ID"
|
|
404
|
+
option :amount, type: :numeric, aliases: "-a", required: true, desc: "Amount to redeem in cents"
|
|
405
|
+
def gift_card_redeem
|
|
406
|
+
configure_logging
|
|
407
|
+
|
|
408
|
+
puts "š Clover Sandbox Simulator - Redeem Gift Card"
|
|
409
|
+
puts "=" * 50
|
|
410
|
+
|
|
411
|
+
services = Services::Clover::ServicesManager.new
|
|
412
|
+
|
|
413
|
+
gc_id = options[:id]
|
|
414
|
+
amount = options[:amount]
|
|
415
|
+
|
|
416
|
+
puts "Redeeming $#{'%.2f' % (amount / 100.0)} from gift card #{gc_id}"
|
|
417
|
+
|
|
418
|
+
result = services.gift_card.redeem_gift_card(gc_id, amount: amount)
|
|
419
|
+
|
|
420
|
+
if result[:success]
|
|
421
|
+
puts "\nā
Redemption successful!"
|
|
422
|
+
puts " Amount Redeemed: $#{'%.2f' % (result[:amount_redeemed] / 100.0)}"
|
|
423
|
+
puts " Remaining Balance: $#{'%.2f' % (result[:remaining_balance] / 100.0)}"
|
|
424
|
+
|
|
425
|
+
if result[:shortfall] > 0
|
|
426
|
+
puts " ā ļø Shortfall: $#{'%.2f' % (result[:shortfall] / 100.0)} (insufficient balance)"
|
|
427
|
+
end
|
|
428
|
+
else
|
|
429
|
+
puts "\nā Redemption failed: #{result[:message]}"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
173
433
|
private
|
|
174
434
|
|
|
435
|
+
def mask_card_number(card_number)
|
|
436
|
+
return card_number if card_number.nil? || card_number.length < 8
|
|
437
|
+
|
|
438
|
+
"#{card_number[0..3]}********#{card_number[-4..]}"
|
|
439
|
+
end
|
|
440
|
+
|
|
175
441
|
def configure_logging
|
|
442
|
+
config = CloverSandboxSimulator.configuration
|
|
443
|
+
|
|
176
444
|
if options[:verbose]
|
|
177
|
-
|
|
445
|
+
config.logger.level = Logger::DEBUG
|
|
178
446
|
else
|
|
179
|
-
|
|
447
|
+
config.logger.level = Logger::INFO
|
|
180
448
|
end
|
|
449
|
+
|
|
450
|
+
# Load specific merchant if specified
|
|
451
|
+
if options[:merchant]
|
|
452
|
+
config.load_merchant(merchant_id: options[:merchant])
|
|
453
|
+
elsif options[:merchant_index]
|
|
454
|
+
config.load_merchant(index: options[:merchant_index])
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
puts "Using merchant: #{config.merchant_name} (#{config.merchant_id})"
|
|
181
458
|
end
|
|
182
459
|
|
|
183
460
|
def print_order_summary(orders)
|
|
@@ -2,20 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
module CloverSandboxSimulator
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :merchant_id, :api_token, :environment, :log_level, :tax_rate, :business_type
|
|
5
|
+
attr_accessor :merchant_id, :merchant_name, :api_token, :environment, :log_level, :tax_rate, :business_type,
|
|
6
|
+
:public_token, :private_token, :ecommerce_environment, :tokenizer_environment,
|
|
7
|
+
:app_id, :app_secret, :refresh_token
|
|
8
|
+
|
|
9
|
+
# Path to merchants JSON file
|
|
10
|
+
MERCHANTS_FILE = File.join(File.dirname(__FILE__), "..", "..", ".env.json")
|
|
6
11
|
|
|
7
12
|
def initialize
|
|
8
13
|
@merchant_id = ENV.fetch("CLOVER_MERCHANT_ID", nil)
|
|
14
|
+
@merchant_name = ENV.fetch("CLOVER_MERCHANT_NAME", nil)
|
|
9
15
|
@api_token = ENV.fetch("CLOVER_API_TOKEN", nil)
|
|
10
16
|
@environment = normalize_url(ENV.fetch("CLOVER_ENVIRONMENT", "https://sandbox.dev.clover.com/"))
|
|
11
17
|
@log_level = parse_log_level(ENV.fetch("LOG_LEVEL", "INFO"))
|
|
12
18
|
@tax_rate = ENV.fetch("TAX_RATE", "8.25").to_f
|
|
13
19
|
@business_type = ENV.fetch("BUSINESS_TYPE", "restaurant").to_sym
|
|
20
|
+
|
|
21
|
+
# Ecommerce API tokens (for card payments and refunds)
|
|
22
|
+
@public_token = ENV.fetch("PUBLIC_TOKEN", nil)
|
|
23
|
+
@private_token = ENV.fetch("PRIVATE_TOKEN", nil)
|
|
24
|
+
@ecommerce_environment = normalize_url(ENV.fetch("ECOMMERCE_ENVIRONMENT", "https://scl-sandbox.dev.clover.com/"))
|
|
25
|
+
@tokenizer_environment = normalize_url(ENV.fetch("TOKENIZER_ENVIRONMENT", "https://token-sandbox.dev.clover.com/"))
|
|
26
|
+
|
|
27
|
+
# OAuth credentials (for token refresh)
|
|
28
|
+
@app_id = ENV.fetch("CLOVER_APP_ID", nil)
|
|
29
|
+
@app_secret = ENV.fetch("CLOVER_APP_SECRET", nil)
|
|
30
|
+
@refresh_token = ENV.fetch("CLOVER_REFRESH_TOKEN", nil)
|
|
31
|
+
|
|
32
|
+
# Load from .env.json if merchant_id not set in ENV
|
|
33
|
+
load_from_merchants_file if @merchant_id.nil? || @merchant_id.empty?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if OAuth is configured for token refresh
|
|
37
|
+
def oauth_enabled?
|
|
38
|
+
!app_id.nil? && !app_id.empty? &&
|
|
39
|
+
!app_secret.nil? && !app_secret.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Load configuration for a specific merchant from .env.json
|
|
43
|
+
#
|
|
44
|
+
# @param merchant_id [String, nil] Merchant ID to load (nil for first merchant)
|
|
45
|
+
# @param index [Integer, nil] Index of merchant in the list (0-based)
|
|
46
|
+
# @return [self]
|
|
47
|
+
def load_merchant(merchant_id: nil, index: nil)
|
|
48
|
+
merchants = load_merchants_file
|
|
49
|
+
return self if merchants.empty?
|
|
50
|
+
|
|
51
|
+
merchant = if merchant_id
|
|
52
|
+
merchants.find { |m| m["CLOVER_MERCHANT_ID"] == merchant_id }
|
|
53
|
+
elsif index
|
|
54
|
+
merchants[index]
|
|
55
|
+
else
|
|
56
|
+
merchants.first
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if merchant
|
|
60
|
+
apply_merchant_config(merchant)
|
|
61
|
+
logger.info "Loaded merchant: #{@merchant_name} (#{@merchant_id})"
|
|
62
|
+
else
|
|
63
|
+
logger.warn "Merchant not found: #{merchant_id || "index #{index}"}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# List all available merchants from .env.json
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Hash>] Array of merchant configs
|
|
72
|
+
def available_merchants
|
|
73
|
+
load_merchants_file.map do |m|
|
|
74
|
+
{
|
|
75
|
+
id: m["CLOVER_MERCHANT_ID"],
|
|
76
|
+
name: m["CLOVER_MERCHANT_NAME"],
|
|
77
|
+
has_ecommerce: !m["PUBLIC_TOKEN"].to_s.empty? && !m["PRIVATE_TOKEN"].to_s.empty?
|
|
78
|
+
}
|
|
79
|
+
end
|
|
14
80
|
end
|
|
15
81
|
|
|
16
82
|
def validate!
|
|
17
83
|
raise ConfigurationError, "CLOVER_MERCHANT_ID is required" if merchant_id.nil? || merchant_id.empty?
|
|
18
|
-
|
|
84
|
+
|
|
85
|
+
# API token is only required for Platform API operations
|
|
86
|
+
# Ecommerce-only operations can work without it
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Validate for Platform API operations (requires OAuth token)
|
|
91
|
+
def validate_platform!
|
|
92
|
+
validate!
|
|
93
|
+
raise ConfigurationError, "CLOVER_API_TOKEN is required for Platform API" if api_token.nil? || api_token.empty?
|
|
94
|
+
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if Platform API is configured (has OAuth token)
|
|
99
|
+
def platform_enabled?
|
|
100
|
+
!api_token.nil? && !api_token.empty? && api_token != "NEEDS_REFRESH"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if Ecommerce API is configured
|
|
104
|
+
def ecommerce_enabled?
|
|
105
|
+
!public_token.nil? && !public_token.empty? &&
|
|
106
|
+
!private_token.nil? && !private_token.empty?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate Ecommerce configuration
|
|
110
|
+
def validate_ecommerce!
|
|
111
|
+
raise ConfigurationError, "PUBLIC_TOKEN is required for Ecommerce API" if public_token.nil? || public_token.empty?
|
|
112
|
+
raise ConfigurationError, "PRIVATE_TOKEN is required for Ecommerce API" if private_token.nil? || private_token.empty?
|
|
19
113
|
|
|
20
114
|
true
|
|
21
115
|
end
|
|
@@ -32,6 +126,39 @@ module CloverSandboxSimulator
|
|
|
32
126
|
|
|
33
127
|
private
|
|
34
128
|
|
|
129
|
+
def load_from_merchants_file
|
|
130
|
+
merchants = load_merchants_file
|
|
131
|
+
return if merchants.empty?
|
|
132
|
+
|
|
133
|
+
# Use first merchant by default
|
|
134
|
+
apply_merchant_config(merchants.first)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def load_merchants_file
|
|
138
|
+
return [] unless File.exist?(MERCHANTS_FILE)
|
|
139
|
+
|
|
140
|
+
JSON.parse(File.read(MERCHANTS_FILE))
|
|
141
|
+
rescue JSON::ParserError => e
|
|
142
|
+
warn "Failed to parse #{MERCHANTS_FILE}: #{e.message}"
|
|
143
|
+
[]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def apply_merchant_config(merchant)
|
|
147
|
+
@merchant_id = merchant["CLOVER_MERCHANT_ID"]
|
|
148
|
+
@merchant_name = merchant["CLOVER_MERCHANT_NAME"]
|
|
149
|
+
# CLOVER_API_TOKEN = static API token (never expires)
|
|
150
|
+
# CLOVER_ACCESS_TOKEN = OAuth JWT token (expires, can be refreshed)
|
|
151
|
+
# Prefer static API token if available, fall back to OAuth token
|
|
152
|
+
if merchant["CLOVER_API_TOKEN"].to_s.length > 10
|
|
153
|
+
@api_token = merchant["CLOVER_API_TOKEN"]
|
|
154
|
+
elsif merchant["CLOVER_ACCESS_TOKEN"].to_s.length > 10
|
|
155
|
+
@api_token = merchant["CLOVER_ACCESS_TOKEN"]
|
|
156
|
+
end
|
|
157
|
+
@refresh_token = merchant["CLOVER_REFRESH_TOKEN"] if merchant["CLOVER_REFRESH_TOKEN"].to_s.length > 10
|
|
158
|
+
@public_token = merchant["PUBLIC_TOKEN"] unless merchant["PUBLIC_TOKEN"].to_s.empty?
|
|
159
|
+
@private_token = merchant["PRIVATE_TOKEN"] unless merchant["PRIVATE_TOKEN"].to_s.empty?
|
|
160
|
+
end
|
|
161
|
+
|
|
35
162
|
def normalize_url(url)
|
|
36
163
|
url = url.strip
|
|
37
164
|
url.end_with?("/") ? url : "#{url}/"
|