clover_sandbox_simulator 1.4.0 → 1.5.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/Gemfile +3 -0
- data/bin/simulate +245 -2
- data/lib/clover_sandbox_simulator/configuration.rb +22 -1
- data/lib/clover_sandbox_simulator/data/restaurant/categories.json +21 -6
- data/lib/clover_sandbox_simulator/data/restaurant/combos.json +31 -8
- data/lib/clover_sandbox_simulator/data/restaurant/items.json +150 -0
- data/lib/clover_sandbox_simulator/data/restaurant/tax_rates.json +3 -0
- data/lib/clover_sandbox_simulator/database.rb +228 -0
- data/lib/clover_sandbox_simulator/db/factories/api_requests.rb +94 -0
- data/lib/clover_sandbox_simulator/db/factories/business_types.rb +178 -0
- data/lib/clover_sandbox_simulator/db/factories/categories.rb +331 -0
- data/lib/clover_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
- data/lib/clover_sandbox_simulator/db/factories/items.rb +1363 -0
- data/lib/clover_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
- data/lib/clover_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000000_enable_pgcrypto.rb +7 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000001_create_business_types.rb +18 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000002_create_categories.rb +18 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000003_create_items.rb +23 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000004_create_simulated_orders.rb +36 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000005_create_simulated_payments.rb +26 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000006_create_api_requests.rb +25 -0
- data/lib/clover_sandbox_simulator/db/migrate/20260206000007_create_daily_summaries.rb +24 -0
- data/lib/clover_sandbox_simulator/generators/data_loader.rb +129 -34
- data/lib/clover_sandbox_simulator/generators/order_generator.rb +312 -48
- data/lib/clover_sandbox_simulator/models/api_request.rb +43 -0
- data/lib/clover_sandbox_simulator/models/business_type.rb +25 -0
- data/lib/clover_sandbox_simulator/models/category.rb +18 -0
- data/lib/clover_sandbox_simulator/models/daily_summary.rb +68 -0
- data/lib/clover_sandbox_simulator/models/item.rb +33 -0
- data/lib/clover_sandbox_simulator/models/record.rb +16 -0
- data/lib/clover_sandbox_simulator/models/simulated_order.rb +42 -0
- data/lib/clover_sandbox_simulator/models/simulated_payment.rb +28 -0
- data/lib/clover_sandbox_simulator/seeder.rb +197 -0
- data/lib/clover_sandbox_simulator/services/base_service.rb +68 -3
- data/lib/clover_sandbox_simulator/services/clover/discount_service.rb +33 -21
- data/lib/clover_sandbox_simulator/services/clover/order_service.rb +49 -0
- data/lib/clover_sandbox_simulator/services/clover/refund_service.rb +15 -2
- data/lib/clover_sandbox_simulator/services/clover/tender_service.rb +43 -12
- data/lib/clover_sandbox_simulator.rb +4 -0
- metadata +83 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 71bf747b5954c1ed19674690e6bd87b50565e6134d271578d0ca555107c5768b
|
|
4
|
+
data.tar.gz: a6e887ab824caa87d013d98fefc58040d0668849b05f60694375d14202792ad5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 01fbdfe1c3deb2506c2c49b8f800e4636b9c34150047276f034b85a4f51a60681feb0d6374abe2682481de2d44410e337a44b4f5b9a84ef93ebc0003e396d801
|
|
7
|
+
data.tar.gz: 845682838c47593dbfff5face1418d6a1359224bfbd1c167c314527d7ca7a1c4a3a9c39a6ac2d10a8b81bcb3a9180a17014f4881c2be139b55ba3947b22e31ef
|
data/Gemfile
CHANGED
data/bin/simulate
CHANGED
|
@@ -386,7 +386,230 @@ module CloverSandboxSimulator
|
|
|
386
386
|
|
|
387
387
|
desc "version", "Show version"
|
|
388
388
|
def version
|
|
389
|
-
puts "Clover Sandbox Simulator
|
|
389
|
+
puts "Clover Sandbox Simulator v#{CloverSandboxSimulator::VERSION}"
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# ============================================
|
|
393
|
+
# Database Management Commands (db: namespace)
|
|
394
|
+
# ============================================
|
|
395
|
+
|
|
396
|
+
desc "db SUBCOMMAND", "Database management commands"
|
|
397
|
+
subcommand "db", Class.new(Thor) {
|
|
398
|
+
def self.banner(task, namespace = true, subcommand = true)
|
|
399
|
+
"simulate db #{task.usage}"
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
namespace "db"
|
|
403
|
+
|
|
404
|
+
desc "create", "Create the PostgreSQL database"
|
|
405
|
+
def create
|
|
406
|
+
db = CloverSandboxSimulator::Database
|
|
407
|
+
url = db.database_url
|
|
408
|
+
puts "Creating database..."
|
|
409
|
+
db.create!(url)
|
|
410
|
+
puts "Done."
|
|
411
|
+
rescue CloverSandboxSimulator::Error => e
|
|
412
|
+
puts "Error: #{e.message}"
|
|
413
|
+
exit 1
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
desc "migrate", "Run pending migrations"
|
|
417
|
+
def migrate
|
|
418
|
+
db = CloverSandboxSimulator::Database
|
|
419
|
+
url = db.database_url
|
|
420
|
+
puts "Connecting and running migrations..."
|
|
421
|
+
db.connect!(url)
|
|
422
|
+
db.migrate!
|
|
423
|
+
puts "Done."
|
|
424
|
+
rescue CloverSandboxSimulator::Error => e
|
|
425
|
+
puts "Error: #{e.message}"
|
|
426
|
+
exit 1
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
desc "seed", "Seed the database with realistic data via FactoryBot"
|
|
430
|
+
option :type, type: :string, desc: "Business type to seed (e.g. restaurant, retail_clothing). Seeds all if omitted."
|
|
431
|
+
def seed
|
|
432
|
+
db = CloverSandboxSimulator::Database
|
|
433
|
+
url = db.database_url
|
|
434
|
+
puts "Connecting and seeding..."
|
|
435
|
+
db.connect!(url)
|
|
436
|
+
bt = options[:type]&.to_sym
|
|
437
|
+
result = db.seed!(business_type: bt)
|
|
438
|
+
puts "Seeded: #{result[:business_types]} business types, #{result[:categories]} categories, #{result[:items]} items"
|
|
439
|
+
puts " (#{result[:created]} created, #{result[:found]} already existed)"
|
|
440
|
+
rescue CloverSandboxSimulator::Error => e
|
|
441
|
+
puts "Error: #{e.message}"
|
|
442
|
+
exit 1
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
desc "reset", "Drop, create, migrate, and seed the database"
|
|
446
|
+
option :type, type: :string, desc: "Business type to seed (e.g. restaurant). Seeds all if omitted."
|
|
447
|
+
option :confirm, type: :boolean, desc: "Confirm destructive operation"
|
|
448
|
+
def reset
|
|
449
|
+
unless options[:confirm]
|
|
450
|
+
puts "This will DROP and recreate the database. All data will be lost."
|
|
451
|
+
puts "Run with --confirm to proceed."
|
|
452
|
+
return
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
db = CloverSandboxSimulator::Database
|
|
456
|
+
url = db.database_url
|
|
457
|
+
|
|
458
|
+
puts "Dropping database..."
|
|
459
|
+
db.drop!(url)
|
|
460
|
+
|
|
461
|
+
puts "Creating database..."
|
|
462
|
+
db.create!(url)
|
|
463
|
+
|
|
464
|
+
puts "Connecting and running migrations..."
|
|
465
|
+
db.connect!(url)
|
|
466
|
+
db.migrate!
|
|
467
|
+
|
|
468
|
+
puts "Seeding..."
|
|
469
|
+
bt = options[:type]&.to_sym
|
|
470
|
+
result = db.seed!(business_type: bt)
|
|
471
|
+
puts "Seeded: #{result[:business_types]} business types, #{result[:categories]} categories, #{result[:items]} items"
|
|
472
|
+
puts "Done."
|
|
473
|
+
rescue CloverSandboxSimulator::Error => e
|
|
474
|
+
puts "Error: #{e.message}"
|
|
475
|
+
exit 1
|
|
476
|
+
end
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# ============================================
|
|
480
|
+
# Audit & Reporting Commands
|
|
481
|
+
# ============================================
|
|
482
|
+
|
|
483
|
+
desc "summary", "Show daily summary for current merchant"
|
|
484
|
+
option :date, type: :string, aliases: "-d", desc: "Date (YYYY-MM-DD, default: today)"
|
|
485
|
+
def summary
|
|
486
|
+
configure_logging
|
|
487
|
+
require_db_connection!
|
|
488
|
+
|
|
489
|
+
date = options[:date] ? Date.parse(options[:date]) : Date.today
|
|
490
|
+
merchant_id = CloverSandboxSimulator.configuration.merchant_id
|
|
491
|
+
|
|
492
|
+
s = Models::DailySummary.for_merchant(merchant_id).on_date(date).first
|
|
493
|
+
|
|
494
|
+
unless s
|
|
495
|
+
puts "No summary found for merchant #{merchant_id} on #{date}."
|
|
496
|
+
puts "Run 'simulate generate' to create orders first."
|
|
497
|
+
return
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
puts "Daily Summary: #{date}"
|
|
501
|
+
puts "=" * 50
|
|
502
|
+
puts " Merchant: #{merchant_id}"
|
|
503
|
+
puts " Orders: #{s.order_count}"
|
|
504
|
+
puts " Payments: #{s.payment_count}"
|
|
505
|
+
puts " Refunds: #{s.refund_count}"
|
|
506
|
+
puts ""
|
|
507
|
+
puts " Revenue: #{format_cents(s.total_revenue)}"
|
|
508
|
+
puts " Tax: #{format_cents(s.total_tax)}"
|
|
509
|
+
puts " Tips: #{format_cents(s.total_tips)}"
|
|
510
|
+
puts " Discounts: #{format_cents(s.total_discounts)}"
|
|
511
|
+
|
|
512
|
+
breakdown = s.breakdown || {}
|
|
513
|
+
|
|
514
|
+
if breakdown["by_meal_period"]&.any?
|
|
515
|
+
puts ""
|
|
516
|
+
puts " By Meal Period:"
|
|
517
|
+
breakdown["by_meal_period"].each do |period, count|
|
|
518
|
+
rev = breakdown.dig("revenue_by_meal_period", period) || 0
|
|
519
|
+
puts " #{period.ljust(15)} #{count.to_s.rjust(3)} orders #{format_cents(rev)}"
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
if breakdown["by_dining_option"]&.any?
|
|
524
|
+
puts ""
|
|
525
|
+
puts " By Dining Option:"
|
|
526
|
+
breakdown["by_dining_option"].each do |opt, count|
|
|
527
|
+
rev = breakdown.dig("revenue_by_dining_option", opt) || 0
|
|
528
|
+
puts " #{opt.ljust(15)} #{count.to_s.rjust(3)} orders #{format_cents(rev)}"
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
if breakdown["by_tender"]&.any?
|
|
533
|
+
puts ""
|
|
534
|
+
puts " By Tender:"
|
|
535
|
+
breakdown["by_tender"].each do |tender, count|
|
|
536
|
+
puts " #{tender.ljust(20)} #{count} payments"
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
desc "audit", "Show recent API requests"
|
|
542
|
+
option :limit, type: :numeric, aliases: "-l", default: 20, desc: "Number of requests to show (default: 20)"
|
|
543
|
+
option :errors, type: :boolean, aliases: "-e", desc: "Show only error requests"
|
|
544
|
+
def audit
|
|
545
|
+
configure_logging
|
|
546
|
+
require_db_connection!
|
|
547
|
+
|
|
548
|
+
scope = Models::ApiRequest.order(created_at: :desc)
|
|
549
|
+
scope = scope.errors if options[:errors]
|
|
550
|
+
requests = scope.limit(options[:limit])
|
|
551
|
+
|
|
552
|
+
if requests.empty?
|
|
553
|
+
label = options[:errors] ? "error " : ""
|
|
554
|
+
puts "No #{label}API requests found."
|
|
555
|
+
return
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
label = options[:errors] ? "Error " : ""
|
|
559
|
+
puts "Recent #{label}API Requests (#{requests.size})"
|
|
560
|
+
puts "=" * 80
|
|
561
|
+
|
|
562
|
+
requests.each do |req|
|
|
563
|
+
status_indicator = req.error_message ? "ERR" : "OK "
|
|
564
|
+
ts = req.created_at.strftime("%H:%M:%S")
|
|
565
|
+
duration = req.duration_ms ? "#{req.duration_ms}ms" : "N/A"
|
|
566
|
+
|
|
567
|
+
puts "#{status_indicator} #{ts} #{req.http_method.ljust(6)} #{req.response_status || '---'} " \
|
|
568
|
+
"#{duration.rjust(7)} #{truncate_url(req.url, 50)}"
|
|
569
|
+
|
|
570
|
+
if req.error_message
|
|
571
|
+
puts " Error: #{req.error_message[0..80]}"
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
total = Models::ApiRequest.count
|
|
576
|
+
errors = Models::ApiRequest.errors.count
|
|
577
|
+
puts "\nTotal: #{total} requests, #{errors} errors"
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
desc "business_types", "List business types with category and item counts"
|
|
581
|
+
def business_types
|
|
582
|
+
configure_logging
|
|
583
|
+
require_db_connection!
|
|
584
|
+
|
|
585
|
+
types = Models::BusinessType.all.order(:key)
|
|
586
|
+
|
|
587
|
+
if types.empty?
|
|
588
|
+
puts "No business types found. Run 'simulate db seed' first."
|
|
589
|
+
return
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
puts "Business Types"
|
|
593
|
+
puts "=" * 60
|
|
594
|
+
puts "#{'Key'.ljust(20)} #{'Industry'.ljust(10)} #{'Categories'.rjust(10)} #{'Items'.rjust(8)}"
|
|
595
|
+
puts "-" * 60
|
|
596
|
+
|
|
597
|
+
total_cats = 0
|
|
598
|
+
total_items = 0
|
|
599
|
+
|
|
600
|
+
types.each do |bt|
|
|
601
|
+
cat_count = bt.categories.count
|
|
602
|
+
item_count = Models::Item.for_business_type(bt.key).count
|
|
603
|
+
total_cats += cat_count
|
|
604
|
+
total_items += item_count
|
|
605
|
+
|
|
606
|
+
industry = bt.industry || "N/A"
|
|
607
|
+
|
|
608
|
+
puts "#{bt.key.ljust(20)} #{industry.ljust(10)} #{cat_count.to_s.rjust(10)} #{item_count.to_s.rjust(8)}"
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
puts "-" * 60
|
|
612
|
+
puts "#{'TOTAL'.ljust(20)} #{' '.ljust(10)} #{total_cats.to_s.rjust(10)} #{total_items.to_s.rjust(8)}"
|
|
390
613
|
end
|
|
391
614
|
|
|
392
615
|
# ============================================
|
|
@@ -488,7 +711,8 @@ module CloverSandboxSimulator
|
|
|
488
711
|
rates.each do |rate|
|
|
489
712
|
rate_pct = (rate["rate"] || 0) / 100_000.0
|
|
490
713
|
default_str = rate["isDefault"] ? "Yes" : "No"
|
|
491
|
-
|
|
714
|
+
rate_str = format("%.3f%%", rate_pct)
|
|
715
|
+
puts "#{(rate['name'] || 'N/A').ljust(25)} #{rate_str.rjust(8)} #{default_str.ljust(10)} #{rate['id']}"
|
|
492
716
|
end
|
|
493
717
|
end
|
|
494
718
|
|
|
@@ -890,6 +1114,25 @@ module CloverSandboxSimulator
|
|
|
890
1114
|
"#{card_number[0..3]}********#{card_number[-4..]}"
|
|
891
1115
|
end
|
|
892
1116
|
|
|
1117
|
+
def format_cents(cents)
|
|
1118
|
+
"$#{'%.2f' % ((cents || 0) / 100.0)}"
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
def require_db_connection!
|
|
1122
|
+
url = Database.database_url
|
|
1123
|
+
Database.connect!(url) unless Database.connected?
|
|
1124
|
+
rescue Error => e
|
|
1125
|
+
puts "Database not available: #{e.message}"
|
|
1126
|
+
puts "Run 'simulate db create && simulate db migrate' first."
|
|
1127
|
+
exit 1
|
|
1128
|
+
end
|
|
1129
|
+
|
|
1130
|
+
def truncate_url(url, max_length)
|
|
1131
|
+
return url if url.nil? || url.length <= max_length
|
|
1132
|
+
|
|
1133
|
+
"...#{url[-(max_length - 3)..]}"
|
|
1134
|
+
end
|
|
1135
|
+
|
|
893
1136
|
def configure_logging
|
|
894
1137
|
config = CloverSandboxSimulator.configuration
|
|
895
1138
|
|
|
@@ -182,15 +182,36 @@ module CloverSandboxSimulator
|
|
|
182
182
|
apply_merchant_config(merchants.first)
|
|
183
183
|
end
|
|
184
184
|
|
|
185
|
+
# Parse .env.json, supporting both the legacy array format and the
|
|
186
|
+
# new object format: { "DATABASE_URL": "...", "merchants": [...] }
|
|
185
187
|
def load_merchants_file
|
|
186
188
|
return [] unless File.exist?(MERCHANTS_FILE)
|
|
187
189
|
|
|
188
|
-
JSON.parse(File.read(MERCHANTS_FILE))
|
|
190
|
+
data = JSON.parse(File.read(MERCHANTS_FILE))
|
|
191
|
+
return data if data.is_a?(Array) # legacy format
|
|
192
|
+
|
|
193
|
+
# New object format — extract merchants list
|
|
194
|
+
data.fetch("merchants", [])
|
|
189
195
|
rescue JSON::ParserError => e
|
|
190
196
|
warn "Failed to parse #{MERCHANTS_FILE}: #{e.message}"
|
|
191
197
|
[]
|
|
192
198
|
end
|
|
193
199
|
|
|
200
|
+
# Read the top-level DATABASE_URL from .env.json (new object format only).
|
|
201
|
+
# Returns nil when the file uses the legacy array format or has no key.
|
|
202
|
+
#
|
|
203
|
+
# @return [String, nil] The DATABASE_URL or nil
|
|
204
|
+
def self.database_url_from_file
|
|
205
|
+
return nil unless File.exist?(MERCHANTS_FILE)
|
|
206
|
+
|
|
207
|
+
data = JSON.parse(File.read(MERCHANTS_FILE))
|
|
208
|
+
return nil if data.is_a?(Array)
|
|
209
|
+
|
|
210
|
+
data["DATABASE_URL"]
|
|
211
|
+
rescue JSON::ParserError
|
|
212
|
+
nil
|
|
213
|
+
end
|
|
214
|
+
|
|
194
215
|
def apply_merchant_config(merchant)
|
|
195
216
|
@merchant_id = merchant["CLOVER_MERCHANT_ID"]
|
|
196
217
|
@merchant_name = merchant["CLOVER_MERCHANT_NAME"]
|
|
@@ -6,33 +6,48 @@
|
|
|
6
6
|
"description": "Start your meal right"
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
|
-
"name": "
|
|
9
|
+
"name": "Soups & Salads",
|
|
10
10
|
"sort_order": 2,
|
|
11
|
+
"description": "Fresh soups and crisp salads"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "Entrees",
|
|
15
|
+
"sort_order": 3,
|
|
11
16
|
"description": "Main courses"
|
|
12
17
|
},
|
|
13
18
|
{
|
|
14
19
|
"name": "Sides",
|
|
15
|
-
"sort_order":
|
|
20
|
+
"sort_order": 4,
|
|
16
21
|
"description": "Perfect accompaniments"
|
|
17
22
|
},
|
|
23
|
+
{
|
|
24
|
+
"name": "Kids Menu",
|
|
25
|
+
"sort_order": 5,
|
|
26
|
+
"description": "For our younger guests"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "Brunch",
|
|
30
|
+
"sort_order": 6,
|
|
31
|
+
"description": "Weekend brunch favorites"
|
|
32
|
+
},
|
|
18
33
|
{
|
|
19
34
|
"name": "Desserts",
|
|
20
|
-
"sort_order":
|
|
35
|
+
"sort_order": 7,
|
|
21
36
|
"description": "Sweet endings"
|
|
22
37
|
},
|
|
23
38
|
{
|
|
24
39
|
"name": "Drinks",
|
|
25
|
-
"sort_order":
|
|
40
|
+
"sort_order": 8,
|
|
26
41
|
"description": "Non-alcoholic beverages"
|
|
27
42
|
},
|
|
28
43
|
{
|
|
29
44
|
"name": "Alcoholic Beverages",
|
|
30
|
-
"sort_order":
|
|
45
|
+
"sort_order": 9,
|
|
31
46
|
"description": "Beer, wine, and cocktails"
|
|
32
47
|
},
|
|
33
48
|
{
|
|
34
49
|
"name": "Specials",
|
|
35
|
-
"sort_order":
|
|
50
|
+
"sort_order": 10,
|
|
36
51
|
"description": "Chef's daily specials"
|
|
37
52
|
}
|
|
38
53
|
]
|
|
@@ -215,27 +215,50 @@
|
|
|
215
215
|
{
|
|
216
216
|
"id": "kids_meal",
|
|
217
217
|
"name": "Kids Meal",
|
|
218
|
-
"description": "
|
|
218
|
+
"description": "Kids item + Drink for 10% off",
|
|
219
219
|
"discount_type": "percentage",
|
|
220
|
-
"discount_value":
|
|
220
|
+
"discount_value": 10,
|
|
221
221
|
"required_components": [
|
|
222
222
|
{
|
|
223
|
-
"category": "
|
|
223
|
+
"category": "Kids Menu",
|
|
224
224
|
"quantity": 1
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
|
-
"category": "
|
|
227
|
+
"category": "Drinks",
|
|
228
|
+
"quantity": 1
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
"applies_to": "total",
|
|
232
|
+
"max_discount_amount": 300,
|
|
233
|
+
"active": true
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
"id": "brunch_special",
|
|
237
|
+
"name": "Brunch Special",
|
|
238
|
+
"description": "Brunch item + Drink + Side for 15% off",
|
|
239
|
+
"discount_type": "percentage",
|
|
240
|
+
"discount_value": 15,
|
|
241
|
+
"required_components": [
|
|
242
|
+
{
|
|
243
|
+
"category": "Brunch",
|
|
228
244
|
"quantity": 1
|
|
229
245
|
},
|
|
230
246
|
{
|
|
231
247
|
"category": "Drinks",
|
|
232
248
|
"quantity": 1
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"category": "Sides",
|
|
252
|
+
"quantity": 1
|
|
233
253
|
}
|
|
234
254
|
],
|
|
235
|
-
"applies_to": "
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
|
|
255
|
+
"applies_to": "total",
|
|
256
|
+
"time_restricted": true,
|
|
257
|
+
"time_rules": {
|
|
258
|
+
"start_hour": 7,
|
|
259
|
+
"end_hour": 14
|
|
260
|
+
},
|
|
261
|
+
"max_discount_amount": 500,
|
|
239
262
|
"active": true
|
|
240
263
|
}
|
|
241
264
|
]
|
|
@@ -30,6 +30,48 @@
|
|
|
30
30
|
"category": "Appetizers",
|
|
31
31
|
"description": "Lightly fried with aioli"
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
"name": "Bruschetta",
|
|
35
|
+
"price": 999,
|
|
36
|
+
"category": "Appetizers",
|
|
37
|
+
"description": "Toasted bread with tomato, basil, and balsamic"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "Shrimp Cocktail",
|
|
41
|
+
"price": 1599,
|
|
42
|
+
"category": "Appetizers",
|
|
43
|
+
"description": "Chilled jumbo shrimp with cocktail sauce"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "Tomato Bisque",
|
|
47
|
+
"price": 699,
|
|
48
|
+
"category": "Soups & Salads",
|
|
49
|
+
"description": "Creamy tomato soup with croutons"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "French Onion Soup",
|
|
53
|
+
"price": 849,
|
|
54
|
+
"category": "Soups & Salads",
|
|
55
|
+
"description": "Classic broth with gruyère crouton"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "Garden Salad",
|
|
59
|
+
"price": 899,
|
|
60
|
+
"category": "Soups & Salads",
|
|
61
|
+
"description": "Mixed greens, tomato, cucumber, choice of dressing"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "Wedge Salad",
|
|
65
|
+
"price": 1099,
|
|
66
|
+
"category": "Soups & Salads",
|
|
67
|
+
"description": "Iceberg wedge with blue cheese and bacon"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "Clam Chowder",
|
|
71
|
+
"price": 899,
|
|
72
|
+
"category": "Soups & Salads",
|
|
73
|
+
"description": "New England style cream-based clam chowder"
|
|
74
|
+
},
|
|
33
75
|
{
|
|
34
76
|
"name": "Classic Burger",
|
|
35
77
|
"price": 1499,
|
|
@@ -90,6 +132,18 @@
|
|
|
90
132
|
"category": "Entrees",
|
|
91
133
|
"description": "Mixed greens with grilled chicken"
|
|
92
134
|
},
|
|
135
|
+
{
|
|
136
|
+
"name": "Mushroom Risotto",
|
|
137
|
+
"price": 1799,
|
|
138
|
+
"category": "Entrees",
|
|
139
|
+
"description": "Arborio rice with wild mushrooms and parmesan"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"name": "Shrimp Scampi",
|
|
143
|
+
"price": 2099,
|
|
144
|
+
"category": "Entrees",
|
|
145
|
+
"description": "Sautéed shrimp in garlic butter over linguine"
|
|
146
|
+
},
|
|
93
147
|
{
|
|
94
148
|
"name": "French Fries",
|
|
95
149
|
"price": 499,
|
|
@@ -126,6 +180,78 @@
|
|
|
126
180
|
"category": "Sides",
|
|
127
181
|
"description": "Seasonal vegetables"
|
|
128
182
|
},
|
|
183
|
+
{
|
|
184
|
+
"name": "Mac and Cheese Bites",
|
|
185
|
+
"price": 599,
|
|
186
|
+
"category": "Sides",
|
|
187
|
+
"description": "Crispy fried mac and cheese balls"
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "Garlic Bread",
|
|
191
|
+
"price": 449,
|
|
192
|
+
"category": "Sides",
|
|
193
|
+
"description": "Toasted with garlic butter and herbs"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "Chicken Tenders",
|
|
197
|
+
"price": 799,
|
|
198
|
+
"category": "Kids Menu",
|
|
199
|
+
"description": "Hand-breaded chicken strips with fries"
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
"name": "Kids Mac and Cheese",
|
|
203
|
+
"price": 649,
|
|
204
|
+
"category": "Kids Menu",
|
|
205
|
+
"description": "Classic creamy macaroni"
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "Mini Burger",
|
|
209
|
+
"price": 699,
|
|
210
|
+
"category": "Kids Menu",
|
|
211
|
+
"description": "Small patty with fries"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "Grilled Cheese",
|
|
215
|
+
"price": 599,
|
|
216
|
+
"category": "Kids Menu",
|
|
217
|
+
"description": "Melted cheddar on toasted bread with fries"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "Kids Quesadilla",
|
|
221
|
+
"price": 649,
|
|
222
|
+
"category": "Kids Menu",
|
|
223
|
+
"description": "Cheese quesadilla with sour cream"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
"name": "Eggs Benedict",
|
|
227
|
+
"price": 1499,
|
|
228
|
+
"category": "Brunch",
|
|
229
|
+
"description": "Poached eggs, ham, hollandaise on English muffin"
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
"name": "Avocado Toast",
|
|
233
|
+
"price": 1299,
|
|
234
|
+
"category": "Brunch",
|
|
235
|
+
"description": "Smashed avocado on sourdough with poached egg"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "Pancake Stack",
|
|
239
|
+
"price": 1099,
|
|
240
|
+
"category": "Brunch",
|
|
241
|
+
"description": "Fluffy buttermilk pancakes with maple syrup"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "French Toast",
|
|
245
|
+
"price": 1199,
|
|
246
|
+
"category": "Brunch",
|
|
247
|
+
"description": "Brioche dipped in vanilla custard with berries"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "Breakfast Burrito",
|
|
251
|
+
"price": 1199,
|
|
252
|
+
"category": "Brunch",
|
|
253
|
+
"description": "Scrambled eggs, chorizo, cheese, salsa verde"
|
|
254
|
+
},
|
|
129
255
|
{
|
|
130
256
|
"name": "Chocolate Brownie",
|
|
131
257
|
"price": 699,
|
|
@@ -150,6 +276,12 @@
|
|
|
150
276
|
"category": "Desserts",
|
|
151
277
|
"description": "Italian coffee-flavored dessert"
|
|
152
278
|
},
|
|
279
|
+
{
|
|
280
|
+
"name": "Crème Brûlée",
|
|
281
|
+
"price": 949,
|
|
282
|
+
"category": "Desserts",
|
|
283
|
+
"description": "Vanilla custard with caramelized sugar crust"
|
|
284
|
+
},
|
|
153
285
|
{
|
|
154
286
|
"name": "Soft Drink",
|
|
155
287
|
"price": 299,
|
|
@@ -180,6 +312,18 @@
|
|
|
180
312
|
"category": "Drinks",
|
|
181
313
|
"description": "Assorted flavors"
|
|
182
314
|
},
|
|
315
|
+
{
|
|
316
|
+
"name": "Sparkling Water",
|
|
317
|
+
"price": 349,
|
|
318
|
+
"category": "Drinks",
|
|
319
|
+
"description": "San Pellegrino or Perrier"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
"name": "Fresh Juice",
|
|
323
|
+
"price": 499,
|
|
324
|
+
"category": "Drinks",
|
|
325
|
+
"description": "Orange, apple, or cranberry"
|
|
326
|
+
},
|
|
183
327
|
{
|
|
184
328
|
"name": "Draft Beer",
|
|
185
329
|
"price": 599,
|
|
@@ -222,6 +366,12 @@
|
|
|
222
366
|
"category": "Alcoholic Beverages",
|
|
223
367
|
"description": "Classic cocktail"
|
|
224
368
|
},
|
|
369
|
+
{
|
|
370
|
+
"name": "Old Fashioned",
|
|
371
|
+
"price": 1299,
|
|
372
|
+
"category": "Alcoholic Beverages",
|
|
373
|
+
"description": "Bourbon, bitters, orange peel"
|
|
374
|
+
},
|
|
225
375
|
{
|
|
226
376
|
"name": "Chef's Special",
|
|
227
377
|
"price": 2499,
|
|
@@ -25,8 +25,11 @@
|
|
|
25
25
|
"Wine": ["Sales Tax", "Alcohol Tax"],
|
|
26
26
|
"Cocktails": ["Sales Tax", "Alcohol Tax"],
|
|
27
27
|
"Appetizers": ["Sales Tax"],
|
|
28
|
+
"Soups & Salads": ["Sales Tax"],
|
|
28
29
|
"Entrees": ["Sales Tax"],
|
|
29
30
|
"Sides": ["Sales Tax"],
|
|
31
|
+
"Kids Menu": ["Sales Tax"],
|
|
32
|
+
"Brunch": ["Sales Tax"],
|
|
30
33
|
"Desserts": ["Sales Tax"],
|
|
31
34
|
"Drinks": ["Sales Tax"],
|
|
32
35
|
"Specials": ["Sales Tax"],
|