clover_sandbox_simulator 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc73877a0d3dbbce2dfe5b038167864972933519265d341665ee23646f37f5c1
4
- data.tar.gz: 808ede5d13382014d7c2ab5f58379160c0983b7db8ead184032b70a5f39b453a
3
+ metadata.gz: 72dfd2be038d2febf2b311a2b5a076060141e3d3d32fd8030c60b7c5a96b2560
4
+ data.tar.gz: 6eb8ef64591c7fc9296f9839961d1865d2dcb98e27ea9165f020cda7cc2940ed
5
5
  SHA512:
6
- metadata.gz: afdb2f5ddd9026721f63328fb2efd4cc97799d952fea3288d8b090ca4daab696719df353eb8d515aafdfe5b62b165798cbde650cb9ffabaf7e827fae1487a321
7
- data.tar.gz: 1218800960726b410b98d5b8d0ca7cfed1aed9a1f916214a257d456ca08373123724764ec04680d1eabde7ebd9f25d98d77a43301d54ee5d0fa2ca74000b7873
6
+ metadata.gz: 124c9d70cf3b8c993ad7f5f348099ab4a8fcab54b26ddcd301191d8705a9168b3fdeef8473cf500f006430d87952d74ba2ff9e787424041014245e44983ae657
7
+ data.tar.gz: 74734957ee1a1a8c0e935d554039b836d9e5990218095c66c3c64166848f85006dfbcbf242b47c526649fb3bf3d465594d6c49525955acb31be6ed9ede5ce7ec
data/bin/simulate CHANGED
@@ -26,13 +26,15 @@ module CloverSandboxSimulator
26
26
 
27
27
  desc "generate", "Generate orders for today"
28
28
  option :count, type: :numeric, aliases: "-n", desc: "Number of orders to generate"
29
+ option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
30
+ desc: "Percentage of orders to refund (0-100, default 5)"
29
31
  def generate
30
32
  configure_logging
31
33
 
32
34
  puts "šŸ½ļø Clover Sandbox Simulator - Order Generation"
33
35
  puts "=" * 50
34
36
 
35
- generator = Generators::OrderGenerator.new
37
+ generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
36
38
  count = options[:count]
37
39
 
38
40
  orders = generator.generate_today(count: count)
@@ -42,13 +44,15 @@ module CloverSandboxSimulator
42
44
 
43
45
  desc "day", "Generate a realistic full day of restaurant operations"
44
46
  option :multiplier, type: :numeric, aliases: "-m", default: 1.0, desc: "Order multiplier (0.5 = slow day, 2.0 = busy day)"
47
+ option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
48
+ desc: "Percentage of orders to refund (0-100, default 5)"
45
49
  def day
46
50
  configure_logging
47
51
 
48
52
  puts "šŸ½ļø Clover Sandbox Simulator - Realistic Restaurant Day"
49
53
  puts "=" * 50
50
54
 
51
- generator = Generators::OrderGenerator.new
55
+ generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
52
56
  orders = generator.generate_realistic_day(multiplier: options[:multiplier])
53
57
 
54
58
  puts "\nāœ… Generated #{orders.size} orders!"
@@ -96,6 +100,8 @@ module CloverSandboxSimulator
96
100
  desc "full", "Run full simulation (setup + generate orders)"
97
101
  option :count, type: :numeric, aliases: "-n", desc: "Number of orders to generate"
98
102
  option :business_type, type: :string, default: "restaurant", desc: "Business type"
103
+ option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
104
+ desc: "Percentage of orders to refund (0-100, default 5)"
99
105
  def full
100
106
  configure_logging
101
107
 
@@ -109,7 +115,7 @@ module CloverSandboxSimulator
109
115
  puts "\n"
110
116
 
111
117
  # Generate orders
112
- order_gen = Generators::OrderGenerator.new
118
+ order_gen = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
113
119
  orders = order_gen.generate_today(count: options[:count])
114
120
 
115
121
  puts "\nāœ… Full simulation complete!"
@@ -150,6 +156,7 @@ module CloverSandboxSimulator
150
156
  employees = services.employee.get_employees
151
157
  customers = services.customer.get_customers
152
158
  discounts = services.discount.get_discounts
159
+ refunds = services.refund.fetch_refunds
153
160
 
154
161
  puts "Categories: #{categories.size}"
155
162
  puts "Menu Items: #{items.size}"
@@ -157,6 +164,7 @@ module CloverSandboxSimulator
157
164
  puts "Employees: #{employees.size}"
158
165
  puts "Customers: #{customers.size}"
159
166
  puts "Discounts: #{discounts.size}"
167
+ puts "Refunds: #{refunds.size}"
160
168
 
161
169
  puts "\nšŸ“‹ Categories:"
162
170
  categories.each { |c| puts " - #{c['name']}" }
@@ -165,13 +173,245 @@ module CloverSandboxSimulator
165
173
  tenders.each { |t| puts " - #{t['label']}" }
166
174
  end
167
175
 
176
+ desc "refunds", "List all refunds"
177
+ option :limit, type: :numeric, aliases: "-l", desc: "Maximum number of refunds to show"
178
+ def refunds
179
+ configure_logging
180
+
181
+ puts "šŸ½ļø Clover Sandbox Simulator - Refunds"
182
+ puts "=" * 50
183
+
184
+ services = Services::Clover::ServicesManager.new
185
+ refunds = services.refund.fetch_refunds(limit: options[:limit])
186
+
187
+ if refunds.empty?
188
+ puts "No refunds found."
189
+ return
190
+ end
191
+
192
+ puts "Found #{refunds.size} refunds:\n\n"
193
+
194
+ total_amount = 0
195
+ refunds.each do |refund|
196
+ amount = refund["amount"] || 0
197
+ total_amount += amount
198
+ payment_id = refund.dig("payment", "id") || "N/A"
199
+ created_at = refund["createdTime"] ? Time.at(refund["createdTime"] / 1000).strftime("%Y-%m-%d %H:%M") : "N/A"
200
+
201
+ puts " ID: #{refund['id']}"
202
+ puts " Amount: $#{'%.2f' % (amount / 100.0)}"
203
+ puts " Payment: #{payment_id}"
204
+ puts " Created: #{created_at}"
205
+ puts ""
206
+ end
207
+
208
+ puts "-" * 40
209
+ puts "Total refunded: $#{'%.2f' % (total_amount / 100.0)}"
210
+ end
211
+
212
+ desc "refund", "Create a refund for a payment"
213
+ option :payment_id, type: :string, aliases: "-p", required: true, desc: "Payment ID to refund"
214
+ option :amount, type: :numeric, aliases: "-a", desc: "Amount in cents (omit for full refund)"
215
+ option :reason, type: :string, aliases: "-r", default: "customer_request",
216
+ desc: "Refund reason (customer_request, quality_issue, wrong_order, duplicate_charge)"
217
+ def refund
218
+ configure_logging
219
+
220
+ puts "šŸ½ļø Clover Sandbox Simulator - Create Refund"
221
+ puts "=" * 50
222
+
223
+ services = Services::Clover::ServicesManager.new
224
+
225
+ if options[:amount]
226
+ puts "Creating partial refund of $#{'%.2f' % (options[:amount] / 100.0)}..."
227
+ result = services.refund.create_partial_refund(
228
+ payment_id: options[:payment_id],
229
+ amount: options[:amount],
230
+ reason: options[:reason]
231
+ )
232
+ else
233
+ puts "Creating full refund..."
234
+ result = services.refund.create_full_refund(
235
+ payment_id: options[:payment_id],
236
+ reason: options[:reason]
237
+ )
238
+ end
239
+
240
+ if result && result["id"]
241
+ puts "\nāœ… Refund created successfully!"
242
+ puts " Refund ID: #{result['id']}"
243
+ puts " Amount: $#{'%.2f' % ((result['amount'] || 0) / 100.0)}"
244
+ else
245
+ puts "\nāŒ Failed to create refund."
246
+ end
247
+ end
248
+
168
249
  desc "version", "Show version"
169
250
  def version
170
251
  puts "Clover Sandbox Simulator v1.0.0"
171
252
  end
172
253
 
254
+ # ============================================
255
+ # Gift Card Commands
256
+ # ============================================
257
+
258
+ desc "gift_cards", "List all gift cards and their balances"
259
+ def gift_cards
260
+ configure_logging
261
+
262
+ puts "šŸŽ Clover Sandbox Simulator - Gift Cards"
263
+ puts "=" * 50
264
+
265
+ services = Services::Clover::ServicesManager.new
266
+ cards = services.gift_card.fetch_gift_cards
267
+
268
+ if cards.empty?
269
+ puts "No gift cards found."
270
+ puts "\nCreate one with: simulate gift_card_create --amount 5000"
271
+ return
272
+ end
273
+
274
+ puts "\n#{'ID'.ljust(20)} #{'Card Number'.ljust(20)} #{'Balance'.rjust(12)} #{'Status'.ljust(10)}"
275
+ puts "-" * 65
276
+
277
+ total_balance = 0
278
+ cards.each do |card|
279
+ id = card["id"] || "N/A"
280
+ number = mask_card_number(card["cardNumber"] || "Unknown")
281
+ balance = card["balance"] || 0
282
+ status = card["status"] || "UNKNOWN"
283
+
284
+ total_balance += balance if status == "ACTIVE"
285
+
286
+ balance_str = "$#{'%.2f' % (balance / 100.0)}"
287
+ puts "#{id.ljust(20)} #{number.ljust(20)} #{balance_str.rjust(12)} #{status.ljust(10)}"
288
+ end
289
+
290
+ puts "-" * 65
291
+ puts "Total Active Balance: $#{'%.2f' % (total_balance / 100.0)}"
292
+ puts "\nāœ… Found #{cards.size} gift cards"
293
+ end
294
+
295
+ desc "gift_card_create", "Create a new gift card"
296
+ option :amount, type: :numeric, aliases: "-a", required: true, desc: "Initial balance in cents (e.g., 5000 for $50)"
297
+ option :card_number, type: :string, aliases: "-n", desc: "Optional 16-digit card number"
298
+ def gift_card_create
299
+ configure_logging
300
+
301
+ puts "šŸŽ Clover Sandbox Simulator - Create Gift Card"
302
+ puts "=" * 50
303
+
304
+ services = Services::Clover::ServicesManager.new
305
+
306
+ amount = options[:amount]
307
+ card_number = options[:card_number]
308
+
309
+ puts "Creating gift card with balance: $#{'%.2f' % (amount / 100.0)}"
310
+
311
+ result = services.gift_card.create_gift_card(
312
+ amount: amount,
313
+ card_number: card_number
314
+ )
315
+
316
+ if result && result["id"]
317
+ puts "\nāœ… Gift card created successfully!"
318
+ puts " ID: #{result['id']}"
319
+ puts " Card Number: #{mask_card_number(result['cardNumber'])}"
320
+ puts " Balance: $#{'%.2f' % ((result['balance'] || amount) / 100.0)}"
321
+ puts " Status: #{result['status']}"
322
+ else
323
+ puts "\nāŒ Failed to create gift card"
324
+ end
325
+ end
326
+
327
+ desc "gift_card_balance", "Check gift card balance"
328
+ option :id, type: :string, aliases: "-i", required: true, desc: "Gift card ID"
329
+ def gift_card_balance
330
+ configure_logging
331
+
332
+ puts "šŸŽ Clover Sandbox Simulator - Gift Card Balance"
333
+ puts "=" * 50
334
+
335
+ services = Services::Clover::ServicesManager.new
336
+
337
+ card = services.gift_card.get_gift_card(options[:id])
338
+
339
+ if card
340
+ puts "\nGift Card Details:"
341
+ puts " ID: #{card['id']}"
342
+ puts " Card Number: #{mask_card_number(card['cardNumber'])}"
343
+ puts " Balance: $#{'%.2f' % ((card['balance'] || 0) / 100.0)}"
344
+ puts " Status: #{card['status']}"
345
+ else
346
+ puts "\nāŒ Gift card not found: #{options[:id]}"
347
+ end
348
+ end
349
+
350
+ desc "gift_card_reload", "Add balance to a gift card"
351
+ option :id, type: :string, aliases: "-i", required: true, desc: "Gift card ID"
352
+ option :amount, type: :numeric, aliases: "-a", required: true, desc: "Amount to add in cents"
353
+ def gift_card_reload
354
+ configure_logging
355
+
356
+ puts "šŸŽ Clover Sandbox Simulator - Reload Gift Card"
357
+ puts "=" * 50
358
+
359
+ services = Services::Clover::ServicesManager.new
360
+
361
+ gc_id = options[:id]
362
+ amount = options[:amount]
363
+
364
+ puts "Adding $#{'%.2f' % (amount / 100.0)} to gift card #{gc_id}"
365
+
366
+ result = services.gift_card.reload_gift_card(gc_id, amount: amount)
367
+
368
+ if result
369
+ puts "\nāœ… Gift card reloaded successfully!"
370
+ puts " New Balance: $#{'%.2f' % ((result['balance'] || 0) / 100.0)}"
371
+ else
372
+ puts "\nāŒ Failed to reload gift card"
373
+ end
374
+ end
375
+
376
+ desc "gift_card_redeem", "Redeem/use balance from 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 redeem in cents"
379
+ def gift_card_redeem
380
+ configure_logging
381
+
382
+ puts "šŸŽ Clover Sandbox Simulator - Redeem 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 "Redeeming $#{'%.2f' % (amount / 100.0)} from gift card #{gc_id}"
391
+
392
+ result = services.gift_card.redeem_gift_card(gc_id, amount: amount)
393
+
394
+ if result[:success]
395
+ puts "\nāœ… Redemption successful!"
396
+ puts " Amount Redeemed: $#{'%.2f' % (result[:amount_redeemed] / 100.0)}"
397
+ puts " Remaining Balance: $#{'%.2f' % (result[:remaining_balance] / 100.0)}"
398
+
399
+ if result[:shortfall] > 0
400
+ puts " āš ļø Shortfall: $#{'%.2f' % (result[:shortfall] / 100.0)} (insufficient balance)"
401
+ end
402
+ else
403
+ puts "\nāŒ Redemption failed: #{result[:message]}"
404
+ end
405
+ end
406
+
173
407
  private
174
408
 
409
+ def mask_card_number(card_number)
410
+ return card_number if card_number.nil? || card_number.length < 8
411
+
412
+ "#{card_number[0..3]}********#{card_number[-4..]}"
413
+ end
414
+
175
415
  def configure_logging
176
416
  if options[:verbose]
177
417
  CloverSandboxSimulator.configuration.logger.level = Logger::DEBUG
@@ -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
- raise ConfigurationError, "CLOVER_API_TOKEN is required" if api_token.nil? || api_token.empty?
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,32 @@ 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
+ @api_token = merchant["CLOVER_API_TOKEN"] if merchant["CLOVER_API_TOKEN"].to_s.length > 10
150
+ @refresh_token = merchant["CLOVER_REFRESH_TOKEN"] if merchant["CLOVER_REFRESH_TOKEN"].to_s.length > 10
151
+ @public_token = merchant["PUBLIC_TOKEN"] unless merchant["PUBLIC_TOKEN"].to_s.empty?
152
+ @private_token = merchant["PRIVATE_TOKEN"] unless merchant["PRIVATE_TOKEN"].to_s.empty?
153
+ end
154
+
35
155
  def normalize_url(url)
36
156
  url = url.strip
37
157
  url.end_with?("/") ? url : "#{url}/"