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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/bin/simulate +245 -2
  4. data/lib/clover_sandbox_simulator/configuration.rb +22 -1
  5. data/lib/clover_sandbox_simulator/data/restaurant/categories.json +21 -6
  6. data/lib/clover_sandbox_simulator/data/restaurant/combos.json +31 -8
  7. data/lib/clover_sandbox_simulator/data/restaurant/items.json +150 -0
  8. data/lib/clover_sandbox_simulator/data/restaurant/tax_rates.json +3 -0
  9. data/lib/clover_sandbox_simulator/database.rb +228 -0
  10. data/lib/clover_sandbox_simulator/db/factories/api_requests.rb +94 -0
  11. data/lib/clover_sandbox_simulator/db/factories/business_types.rb +178 -0
  12. data/lib/clover_sandbox_simulator/db/factories/categories.rb +331 -0
  13. data/lib/clover_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
  14. data/lib/clover_sandbox_simulator/db/factories/items.rb +1363 -0
  15. data/lib/clover_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
  16. data/lib/clover_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
  17. data/lib/clover_sandbox_simulator/db/migrate/20260206000000_enable_pgcrypto.rb +7 -0
  18. data/lib/clover_sandbox_simulator/db/migrate/20260206000001_create_business_types.rb +18 -0
  19. data/lib/clover_sandbox_simulator/db/migrate/20260206000002_create_categories.rb +18 -0
  20. data/lib/clover_sandbox_simulator/db/migrate/20260206000003_create_items.rb +23 -0
  21. data/lib/clover_sandbox_simulator/db/migrate/20260206000004_create_simulated_orders.rb +36 -0
  22. data/lib/clover_sandbox_simulator/db/migrate/20260206000005_create_simulated_payments.rb +26 -0
  23. data/lib/clover_sandbox_simulator/db/migrate/20260206000006_create_api_requests.rb +25 -0
  24. data/lib/clover_sandbox_simulator/db/migrate/20260206000007_create_daily_summaries.rb +24 -0
  25. data/lib/clover_sandbox_simulator/generators/data_loader.rb +129 -34
  26. data/lib/clover_sandbox_simulator/generators/order_generator.rb +312 -48
  27. data/lib/clover_sandbox_simulator/models/api_request.rb +43 -0
  28. data/lib/clover_sandbox_simulator/models/business_type.rb +25 -0
  29. data/lib/clover_sandbox_simulator/models/category.rb +18 -0
  30. data/lib/clover_sandbox_simulator/models/daily_summary.rb +68 -0
  31. data/lib/clover_sandbox_simulator/models/item.rb +33 -0
  32. data/lib/clover_sandbox_simulator/models/record.rb +16 -0
  33. data/lib/clover_sandbox_simulator/models/simulated_order.rb +42 -0
  34. data/lib/clover_sandbox_simulator/models/simulated_payment.rb +28 -0
  35. data/lib/clover_sandbox_simulator/seeder.rb +197 -0
  36. data/lib/clover_sandbox_simulator/services/base_service.rb +68 -3
  37. data/lib/clover_sandbox_simulator/services/clover/discount_service.rb +33 -21
  38. data/lib/clover_sandbox_simulator/services/clover/order_service.rb +49 -0
  39. data/lib/clover_sandbox_simulator/services/clover/refund_service.rb +15 -2
  40. data/lib/clover_sandbox_simulator/services/clover/tender_service.rb +43 -12
  41. data/lib/clover_sandbox_simulator.rb +4 -0
  42. metadata +83 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74de5928a11de82d6c3b389aec6fd92a4c63844eb55ad22bbfafecb805e4aca8
4
- data.tar.gz: 2f11d97b104c0a58a37266b9792157d497ed30ff10fd39bb7bca964239893dc9
3
+ metadata.gz: 71bf747b5954c1ed19674690e6bd87b50565e6134d271578d0ca555107c5768b
4
+ data.tar.gz: a6e887ab824caa87d013d98fefc58040d0668849b05f60694375d14202792ad5
5
5
  SHA512:
6
- metadata.gz: 5ff02a1e8a9e90d8230c6af743887851e65c0eb266f9574571cdbcc3e973f06c719abb5fa64b164cdc14b473ee48355f0f822149ba89a0c543c36afa5b97eb68
7
- data.tar.gz: 2ae75ceca9d7d77e762fa5fbb94361cd85bea8bdd3e4b8a8c15bc9461aa228f941096e7c079d681bbce4722068b068ecae27496562875fc9019e715dfe8e1167
6
+ metadata.gz: 01fbdfe1c3deb2506c2c49b8f800e4636b9c34150047276f034b85a4f51a60681feb0d6374abe2682481de2d44410e337a44b4f5b9a84ef93ebc0003e396d801
7
+ data.tar.gz: 845682838c47593dbfff5face1418d6a1359224bfbd1c167c314527d7ca7a1c4a3a9c39a6ac2d10a8b81bcb3a9180a17014f4881c2be139b55ba3947b22e31ef
data/Gemfile CHANGED
@@ -4,6 +4,9 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
+ # Project management tracking (shared across all TOS projects)
8
+ gem "linear_api", path: "../linear"
9
+
7
10
  group :development, :test do
8
11
  gem "pry", "~> 0.14"
9
12
  gem "pry-byebug", "~> 3.10"
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 v1.0.0"
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
- puts "#{(rate['name'] || 'N/A').ljust(25)} #{'%.3f%' % rate_pct.rjust(8)} #{default_str.ljust(10)} #{rate['id']}"
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": "Entrees",
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": 3,
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": 4,
35
+ "sort_order": 7,
21
36
  "description": "Sweet endings"
22
37
  },
23
38
  {
24
39
  "name": "Drinks",
25
- "sort_order": 5,
40
+ "sort_order": 8,
26
41
  "description": "Non-alcoholic beverages"
27
42
  },
28
43
  {
29
44
  "name": "Alcoholic Beverages",
30
- "sort_order": 6,
45
+ "sort_order": 9,
31
46
  "description": "Beer, wine, and cocktails"
32
47
  },
33
48
  {
34
49
  "name": "Specials",
35
- "sort_order": 7,
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": "Side + Drink free with entree (kids under 12)",
218
+ "description": "Kids item + Drink for 10% off",
219
219
  "discount_type": "percentage",
220
- "discount_value": 100,
220
+ "discount_value": 10,
221
221
  "required_components": [
222
222
  {
223
- "category": "Entrees",
223
+ "category": "Kids Menu",
224
224
  "quantity": 1
225
225
  },
226
226
  {
227
- "category": "Sides",
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": "cheapest_items",
236
- "max_items_discounted": 2,
237
- "requires_kid": true,
238
- "max_discount_amount": 898,
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"],