skytab_sandbox_simulator 0.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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +10 -0
  3. data/bin/simulate +433 -0
  4. data/lib/skytab_sandbox_simulator/configuration.rb +205 -0
  5. data/lib/skytab_sandbox_simulator/data/bar_nightclub/categories.json +9 -0
  6. data/lib/skytab_sandbox_simulator/data/bar_nightclub/items.json +28 -0
  7. data/lib/skytab_sandbox_simulator/data/bar_nightclub/tenders.json +19 -0
  8. data/lib/skytab_sandbox_simulator/data/cafe_bakery/categories.json +9 -0
  9. data/lib/skytab_sandbox_simulator/data/cafe_bakery/items.json +30 -0
  10. data/lib/skytab_sandbox_simulator/data/cafe_bakery/tenders.json +17 -0
  11. data/lib/skytab_sandbox_simulator/data/fine_dining/categories.json +9 -0
  12. data/lib/skytab_sandbox_simulator/data/fine_dining/items.json +30 -0
  13. data/lib/skytab_sandbox_simulator/data/fine_dining/tenders.json +18 -0
  14. data/lib/skytab_sandbox_simulator/data/pizzeria/categories.json +9 -0
  15. data/lib/skytab_sandbox_simulator/data/pizzeria/items.json +28 -0
  16. data/lib/skytab_sandbox_simulator/data/pizzeria/tenders.json +18 -0
  17. data/lib/skytab_sandbox_simulator/data/restaurant/categories.json +44 -0
  18. data/lib/skytab_sandbox_simulator/data/restaurant/items.json +59 -0
  19. data/lib/skytab_sandbox_simulator/data/restaurant/tenders.json +22 -0
  20. data/lib/skytab_sandbox_simulator/database.rb +192 -0
  21. data/lib/skytab_sandbox_simulator/db/factories/business_types.rb +102 -0
  22. data/lib/skytab_sandbox_simulator/db/factories/categories.rb +243 -0
  23. data/lib/skytab_sandbox_simulator/db/factories/items.rb +976 -0
  24. data/lib/skytab_sandbox_simulator/db/factories/simulated_orders.rb +120 -0
  25. data/lib/skytab_sandbox_simulator/db/factories/simulated_payments.rb +75 -0
  26. data/lib/skytab_sandbox_simulator/db/migrate/20260316000000_enable_pgcrypto.rb +7 -0
  27. data/lib/skytab_sandbox_simulator/db/migrate/20260316000001_create_business_types.rb +18 -0
  28. data/lib/skytab_sandbox_simulator/db/migrate/20260316000002_create_categories.rb +18 -0
  29. data/lib/skytab_sandbox_simulator/db/migrate/20260316000003_create_items.rb +23 -0
  30. data/lib/skytab_sandbox_simulator/db/migrate/20260316000004_create_simulated_orders.rb +35 -0
  31. data/lib/skytab_sandbox_simulator/db/migrate/20260316000005_create_simulated_payments.rb +26 -0
  32. data/lib/skytab_sandbox_simulator/db/migrate/20260316000006_create_api_requests.rb +27 -0
  33. data/lib/skytab_sandbox_simulator/db/migrate/20260316000007_create_daily_summaries.rb +24 -0
  34. data/lib/skytab_sandbox_simulator/generators/data_loader.rb +125 -0
  35. data/lib/skytab_sandbox_simulator/generators/entity_generator.rb +107 -0
  36. data/lib/skytab_sandbox_simulator/generators/order_generator.rb +390 -0
  37. data/lib/skytab_sandbox_simulator/models/api_request.rb +43 -0
  38. data/lib/skytab_sandbox_simulator/models/business_type.rb +25 -0
  39. data/lib/skytab_sandbox_simulator/models/category.rb +17 -0
  40. data/lib/skytab_sandbox_simulator/models/daily_summary.rb +67 -0
  41. data/lib/skytab_sandbox_simulator/models/item.rb +32 -0
  42. data/lib/skytab_sandbox_simulator/models/record.rb +14 -0
  43. data/lib/skytab_sandbox_simulator/models/simulated_order.rb +40 -0
  44. data/lib/skytab_sandbox_simulator/models/simulated_payment.rb +28 -0
  45. data/lib/skytab_sandbox_simulator/seeder.rb +167 -0
  46. data/lib/skytab_sandbox_simulator/services/base_service.rb +227 -0
  47. data/lib/skytab_sandbox_simulator/services/skytab/catalog_service.rb +130 -0
  48. data/lib/skytab_sandbox_simulator/services/skytab/location_service.rb +54 -0
  49. data/lib/skytab_sandbox_simulator/services/skytab/order_service.rb +139 -0
  50. data/lib/skytab_sandbox_simulator/services/skytab/payment_service.rb +94 -0
  51. data/lib/skytab_sandbox_simulator/services/skytab/service_manager.rb +62 -0
  52. data/lib/skytab_sandbox_simulator/version.rb +5 -0
  53. data/lib/skytab_sandbox_simulator.rb +45 -0
  54. metadata +305 -0
@@ -0,0 +1,28 @@
1
+ {
2
+ "items": [
3
+ { "name": "House Lager", "price": 599, "category": "Draft Beer", "sku": "BAR-DRF-001" },
4
+ { "name": "IPA", "price": 699, "category": "Draft Beer", "sku": "BAR-DRF-002" },
5
+ { "name": "Stout", "price": 749, "category": "Draft Beer", "sku": "BAR-DRF-003" },
6
+ { "name": "Wheat Beer", "price": 649, "category": "Draft Beer", "sku": "BAR-DRF-004" },
7
+
8
+ { "name": "Margarita", "price": 1199, "category": "Cocktails", "sku": "BAR-COC-001" },
9
+ { "name": "Old Fashioned", "price": 1399, "category": "Cocktails", "sku": "BAR-COC-002" },
10
+ { "name": "Mojito", "price": 1199, "category": "Cocktails", "sku": "BAR-COC-003" },
11
+ { "name": "Espresso Martini", "price": 1499, "category": "Cocktails", "sku": "BAR-COC-004" },
12
+
13
+ { "name": "Whiskey Neat", "price": 1099, "category": "Spirits", "sku": "BAR-SPI-001" },
14
+ { "name": "Vodka Soda", "price": 899, "category": "Spirits", "sku": "BAR-SPI-002" },
15
+ { "name": "Tequila Shot", "price": 799, "category": "Spirits", "sku": "BAR-SPI-003" },
16
+ { "name": "Rum and Coke", "price": 899, "category": "Spirits", "sku": "BAR-SPI-004" },
17
+
18
+ { "name": "House Red", "price": 899, "category": "Wine", "sku": "BAR-WIN-001" },
19
+ { "name": "House White", "price": 899, "category": "Wine", "sku": "BAR-WIN-002" },
20
+ { "name": "Prosecco", "price": 999, "category": "Wine", "sku": "BAR-WIN-003" },
21
+ { "name": "Rose", "price": 999, "category": "Wine", "sku": "BAR-WIN-004" },
22
+
23
+ { "name": "Loaded Fries", "price": 899, "category": "Bar Snacks", "sku": "BAR-SNK-001" },
24
+ { "name": "Sliders", "price": 1199, "category": "Bar Snacks", "sku": "BAR-SNK-002" },
25
+ { "name": "Wings", "price": 1099, "category": "Bar Snacks", "sku": "BAR-SNK-003" },
26
+ { "name": "Pretzel Bites", "price": 799, "category": "Bar Snacks", "sku": "BAR-SNK-004" }
27
+ ]
28
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "tenders": [
3
+ { "label": "Cash", "pos_ref": "CASH", "weight": 10 },
4
+ { "label": "Visa", "pos_ref": "VISA", "weight": 30 },
5
+ { "label": "Mastercard", "pos_ref": "MASTERCARD", "weight": 25 },
6
+ { "label": "Amex", "pos_ref": "AMEX", "weight": 15 },
7
+ { "label": "Discover", "pos_ref": "DISCOVER", "weight": 5 },
8
+ { "label": "House Account", "pos_ref": "HOUSE_ACCOUNT", "weight": 15 }
9
+ ],
10
+ "revenue_classes": [
11
+ { "label": "Bar", "pos_ref": "BAR" },
12
+ { "label": "Dine-In", "pos_ref": "DINE_IN" },
13
+ { "label": "Takeout", "pos_ref": "TAKEOUT" }
14
+ ],
15
+ "tax_rates": [
16
+ { "name": "Sales Tax", "rate": 82500, "note": "8.25%" },
17
+ { "name": "Alcohol Tax", "rate": 100000, "note": "10.0%" }
18
+ ]
19
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "categories": [
3
+ { "name": "Coffee & Espresso", "sort_order": 1, "description": "Hot and cold coffee drinks" },
4
+ { "name": "Pastries", "sort_order": 2, "description": "Fresh-baked goods" },
5
+ { "name": "Breakfast", "sort_order": 3, "description": "Morning favorites" },
6
+ { "name": "Sandwiches & Wraps", "sort_order": 4, "description": "Lunch fare" },
7
+ { "name": "Smoothies & Juice", "sort_order": 5, "description": "Fresh blended drinks" }
8
+ ]
9
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "items": [
3
+ { "name": "House Drip Coffee", "price": 299, "category": "Coffee & Espresso", "sku": "CAFE-COF-001" },
4
+ { "name": "Espresso", "price": 350, "category": "Coffee & Espresso", "sku": "CAFE-COF-002" },
5
+ { "name": "Cappuccino", "price": 499, "category": "Coffee & Espresso", "sku": "CAFE-COF-003" },
6
+ { "name": "Latte", "price": 549, "category": "Coffee & Espresso", "sku": "CAFE-COF-004" },
7
+ { "name": "Cold Brew", "price": 499, "category": "Coffee & Espresso", "sku": "CAFE-COF-005" },
8
+
9
+ { "name": "Croissant", "price": 399, "category": "Pastries", "sku": "CAFE-PAS-001" },
10
+ { "name": "Blueberry Muffin", "price": 399, "category": "Pastries", "sku": "CAFE-PAS-002" },
11
+ { "name": "Cinnamon Roll", "price": 449, "category": "Pastries", "sku": "CAFE-PAS-003" },
12
+ { "name": "Chocolate Chip Cookie", "price": 299, "category": "Pastries", "sku": "CAFE-PAS-004" },
13
+ { "name": "Almond Croissant", "price": 449, "category": "Pastries", "sku": "CAFE-PAS-005" },
14
+
15
+ { "name": "Avocado Toast", "price": 1099, "category": "Breakfast", "sku": "CAFE-BRK-001" },
16
+ { "name": "Breakfast Burrito", "price": 999, "category": "Breakfast", "sku": "CAFE-BRK-002" },
17
+ { "name": "Acai Bowl", "price": 1199, "category": "Breakfast", "sku": "CAFE-BRK-003" },
18
+ { "name": "Yogurt Parfait", "price": 699, "category": "Breakfast", "sku": "CAFE-BRK-004" },
19
+
20
+ { "name": "Turkey Club", "price": 1199, "category": "Sandwiches & Wraps", "sku": "CAFE-SAN-001" },
21
+ { "name": "Caprese Panini", "price": 1099, "category": "Sandwiches & Wraps", "sku": "CAFE-SAN-002" },
22
+ { "name": "Chicken Caesar Wrap", "price": 1099, "category": "Sandwiches & Wraps", "sku": "CAFE-SAN-003" },
23
+ { "name": "BLT", "price": 999, "category": "Sandwiches & Wraps", "sku": "CAFE-SAN-004" },
24
+
25
+ { "name": "Berry Blast Smoothie", "price": 699, "category": "Smoothies & Juice", "sku": "CAFE-SMO-001" },
26
+ { "name": "Green Detox Juice", "price": 799, "category": "Smoothies & Juice", "sku": "CAFE-SMO-002" },
27
+ { "name": "Mango Tango Smoothie", "price": 699, "category": "Smoothies & Juice", "sku": "CAFE-SMO-003" },
28
+ { "name": "Fresh Orange Juice", "price": 499, "category": "Smoothies & Juice", "sku": "CAFE-SMO-004" }
29
+ ]
30
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "tenders": [
3
+ { "label": "Cash", "pos_ref": "CASH", "weight": 20 },
4
+ { "label": "Visa", "pos_ref": "VISA", "weight": 30 },
5
+ { "label": "Mastercard", "pos_ref": "MASTERCARD", "weight": 20 },
6
+ { "label": "Amex", "pos_ref": "AMEX", "weight": 10 },
7
+ { "label": "Discover", "pos_ref": "DISCOVER", "weight": 5 },
8
+ { "label": "Gift Card", "pos_ref": "GIFT_CARD", "weight": 15 }
9
+ ],
10
+ "revenue_classes": [
11
+ { "label": "Dine-In", "pos_ref": "DINE_IN" },
12
+ { "label": "Takeout", "pos_ref": "TAKEOUT" }
13
+ ],
14
+ "tax_rates": [
15
+ { "name": "Sales Tax", "rate": 82500, "note": "8.25%" }
16
+ ]
17
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "categories": [
3
+ { "name": "First Course", "sort_order": 1, "description": "Opening courses" },
4
+ { "name": "Main Course", "sort_order": 2, "description": "Principal courses" },
5
+ { "name": "Desserts", "sort_order": 3, "description": "Artisan desserts" },
6
+ { "name": "Wine List", "sort_order": 4, "description": "Curated wine selection" },
7
+ { "name": "Cocktails", "sort_order": 5, "description": "Signature cocktails" }
8
+ ]
9
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "items": [
3
+ { "name": "Seared Foie Gras", "price": 3200, "category": "First Course", "sku": "FIN-FST-001" },
4
+ { "name": "Lobster Bisque", "price": 1800, "category": "First Course", "sku": "FIN-FST-002" },
5
+ { "name": "Tuna Tartare", "price": 2400, "category": "First Course", "sku": "FIN-FST-003" },
6
+ { "name": "Burrata Salad", "price": 1900, "category": "First Course", "sku": "FIN-FST-004" },
7
+ { "name": "Oysters Half Dozen", "price": 2800, "category": "First Course", "sku": "FIN-FST-005" },
8
+
9
+ { "name": "Wagyu Ribeye", "price": 8500, "category": "Main Course", "sku": "FIN-MAN-001" },
10
+ { "name": "Chilean Sea Bass", "price": 4200, "category": "Main Course", "sku": "FIN-MAN-002" },
11
+ { "name": "Rack of Lamb", "price": 5200, "category": "Main Course", "sku": "FIN-MAN-003" },
12
+ { "name": "Duck Breast", "price": 3800, "category": "Main Course", "sku": "FIN-MAN-004" },
13
+ { "name": "Truffle Risotto", "price": 3200, "category": "Main Course", "sku": "FIN-MAN-005" },
14
+
15
+ { "name": "Creme Brulee", "price": 1600, "category": "Desserts", "sku": "FIN-DES-001" },
16
+ { "name": "Chocolate Souffle", "price": 1800, "category": "Desserts", "sku": "FIN-DES-002" },
17
+ { "name": "Dessert Tasting Plate", "price": 2200, "category": "Desserts", "sku": "FIN-DES-003" },
18
+ { "name": "Artisan Cheese Board", "price": 2600, "category": "Desserts", "sku": "FIN-DES-004" },
19
+ { "name": "Tiramisu", "price": 1500, "category": "Desserts", "sku": "FIN-DES-005" },
20
+
21
+ { "name": "Champagne", "price": 2200, "category": "Wine List", "sku": "FIN-WIN-001" },
22
+ { "name": "Cabernet Sauvignon", "price": 1800, "category": "Wine List", "sku": "FIN-WIN-002" },
23
+ { "name": "Pinot Noir", "price": 1600, "category": "Wine List", "sku": "FIN-WIN-003" },
24
+ { "name": "Chardonnay", "price": 1500, "category": "Wine List", "sku": "FIN-WIN-004" },
25
+
26
+ { "name": "Negroni", "price": 1800, "category": "Cocktails", "sku": "FIN-COC-001" },
27
+ { "name": "Manhattan", "price": 1900, "category": "Cocktails", "sku": "FIN-COC-002" },
28
+ { "name": "French 75", "price": 1700, "category": "Cocktails", "sku": "FIN-COC-003" }
29
+ ]
30
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "tenders": [
3
+ { "label": "Visa", "pos_ref": "VISA", "weight": 30 },
4
+ { "label": "Mastercard", "pos_ref": "MASTERCARD", "weight": 20 },
5
+ { "label": "Amex", "pos_ref": "AMEX", "weight": 25 },
6
+ { "label": "Discover", "pos_ref": "DISCOVER", "weight": 5 },
7
+ { "label": "Cash", "pos_ref": "CASH", "weight": 5 },
8
+ { "label": "House Account", "pos_ref": "HOUSE_ACCOUNT", "weight": 15 }
9
+ ],
10
+ "revenue_classes": [
11
+ { "label": "Dine-In", "pos_ref": "DINE_IN" },
12
+ { "label": "Bar", "pos_ref": "BAR" }
13
+ ],
14
+ "tax_rates": [
15
+ { "name": "Sales Tax", "rate": 82500, "note": "8.25%" },
16
+ { "name": "Alcohol Tax", "rate": 100000, "note": "10.0%" }
17
+ ]
18
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "categories": [
3
+ { "name": "Pizzas", "sort_order": 1, "description": "Hand-tossed pizzas" },
4
+ { "name": "Calzones", "sort_order": 2, "description": "Folded and baked" },
5
+ { "name": "Sides", "sort_order": 3, "description": "Perfect with pizza" },
6
+ { "name": "Drinks", "sort_order": 4, "description": "Beverages" },
7
+ { "name": "Desserts", "sort_order": 5, "description": "Sweet endings" }
8
+ ]
9
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "items": [
3
+ { "name": "Margherita", "price": 1499, "category": "Pizzas", "sku": "PIZ-PIZ-001" },
4
+ { "name": "Pepperoni", "price": 1599, "category": "Pizzas", "sku": "PIZ-PIZ-002" },
5
+ { "name": "Supreme", "price": 1899, "category": "Pizzas", "sku": "PIZ-PIZ-003" },
6
+ { "name": "Hawaiian", "price": 1599, "category": "Pizzas", "sku": "PIZ-PIZ-004" },
7
+ { "name": "BBQ Chicken Pizza", "price": 1799, "category": "Pizzas", "sku": "PIZ-PIZ-005" },
8
+ { "name": "Meat Lovers", "price": 1899, "category": "Pizzas", "sku": "PIZ-PIZ-006" },
9
+
10
+ { "name": "Classic Calzone", "price": 1299, "category": "Calzones", "sku": "PIZ-CAL-001" },
11
+ { "name": "Meat Calzone", "price": 1499, "category": "Calzones", "sku": "PIZ-CAL-002" },
12
+ { "name": "Stromboli", "price": 1399, "category": "Calzones", "sku": "PIZ-CAL-003" },
13
+ { "name": "Spinach Calzone", "price": 1299, "category": "Calzones", "sku": "PIZ-CAL-004" },
14
+
15
+ { "name": "Garlic Bread", "price": 499, "category": "Sides", "sku": "PIZ-SID-001" },
16
+ { "name": "Garden Salad", "price": 699, "category": "Sides", "sku": "PIZ-SID-002" },
17
+ { "name": "Caesar Salad", "price": 799, "category": "Sides", "sku": "PIZ-SID-003" },
18
+ { "name": "Garlic Knots", "price": 499, "category": "Sides", "sku": "PIZ-SID-004" },
19
+
20
+ { "name": "Fountain Drink", "price": 299, "category": "Drinks", "sku": "PIZ-DRK-001" },
21
+ { "name": "Iced Tea", "price": 299, "category": "Drinks", "sku": "PIZ-DRK-002" },
22
+ { "name": "Lemonade", "price": 349, "category": "Drinks", "sku": "PIZ-DRK-003" },
23
+
24
+ { "name": "Cannoli", "price": 599, "category": "Desserts", "sku": "PIZ-DES-001" },
25
+ { "name": "Chocolate Brownie", "price": 499, "category": "Desserts", "sku": "PIZ-DES-002" },
26
+ { "name": "Cheesecake", "price": 699, "category": "Desserts", "sku": "PIZ-DES-003" }
27
+ ]
28
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "tenders": [
3
+ { "label": "Cash", "pos_ref": "CASH", "weight": 20 },
4
+ { "label": "Visa", "pos_ref": "VISA", "weight": 30 },
5
+ { "label": "Mastercard", "pos_ref": "MASTERCARD", "weight": 20 },
6
+ { "label": "Amex", "pos_ref": "AMEX", "weight": 10 },
7
+ { "label": "Discover", "pos_ref": "DISCOVER", "weight": 5 },
8
+ { "label": "Gift Card", "pos_ref": "GIFT_CARD", "weight": 15 }
9
+ ],
10
+ "revenue_classes": [
11
+ { "label": "Dine-In", "pos_ref": "DINE_IN" },
12
+ { "label": "Takeout", "pos_ref": "TAKEOUT" },
13
+ { "label": "Delivery", "pos_ref": "DELIVERY" }
14
+ ],
15
+ "tax_rates": [
16
+ { "name": "Sales Tax", "rate": 82500, "note": "8.25%" }
17
+ ]
18
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "categories": [
3
+ {
4
+ "name": "Appetizers",
5
+ "sort_order": 1,
6
+ "description": "Starters and shareables"
7
+ },
8
+ {
9
+ "name": "Entrees",
10
+ "sort_order": 2,
11
+ "description": "Main courses"
12
+ },
13
+ {
14
+ "name": "Sides",
15
+ "sort_order": 3,
16
+ "description": "Side dishes"
17
+ },
18
+ {
19
+ "name": "Desserts",
20
+ "sort_order": 4,
21
+ "description": "Sweet finishes"
22
+ },
23
+ {
24
+ "name": "Beverages",
25
+ "sort_order": 5,
26
+ "description": "Non-alcoholic drinks"
27
+ },
28
+ {
29
+ "name": "Alcohol",
30
+ "sort_order": 6,
31
+ "description": "Beer, wine, and cocktails"
32
+ },
33
+ {
34
+ "name": "Kids Menu",
35
+ "sort_order": 7,
36
+ "description": "For our younger guests"
37
+ },
38
+ {
39
+ "name": "Specials",
40
+ "sort_order": 8,
41
+ "description": "Chef's daily specials"
42
+ }
43
+ ]
44
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "items": [
3
+ { "name": "Buffalo Wings", "price": 1299, "category": "Appetizers", "sku": "REST-APP-001" },
4
+ { "name": "Mozzarella Sticks", "price": 999, "category": "Appetizers", "sku": "REST-APP-002" },
5
+ { "name": "Loaded Nachos", "price": 1199, "category": "Appetizers", "sku": "REST-APP-003" },
6
+ { "name": "Spinach Artichoke Dip", "price": 1099, "category": "Appetizers", "sku": "REST-APP-004" },
7
+ { "name": "Calamari", "price": 1399, "category": "Appetizers", "sku": "REST-APP-005" },
8
+ { "name": "Bruschetta", "price": 999, "category": "Appetizers", "sku": "REST-APP-006" },
9
+ { "name": "Shrimp Cocktail", "price": 1599, "category": "Appetizers", "sku": "REST-APP-007" },
10
+
11
+ { "name": "Classic Burger", "price": 1499, "category": "Entrees", "sku": "REST-ENT-001" },
12
+ { "name": "Grilled Salmon", "price": 2199, "category": "Entrees", "sku": "REST-ENT-002" },
13
+ { "name": "NY Strip Steak", "price": 2899, "category": "Entrees", "sku": "REST-ENT-003" },
14
+ { "name": "Chicken Parmesan", "price": 1899, "category": "Entrees", "sku": "REST-ENT-004" },
15
+ { "name": "Fettuccine Alfredo", "price": 1599, "category": "Entrees", "sku": "REST-ENT-005" },
16
+ { "name": "Fish and Chips", "price": 1699, "category": "Entrees", "sku": "REST-ENT-006" },
17
+ { "name": "BBQ Ribs", "price": 2399, "category": "Entrees", "sku": "REST-ENT-007" },
18
+ { "name": "Mushroom Risotto", "price": 1799, "category": "Entrees", "sku": "REST-ENT-008" },
19
+ { "name": "Shrimp Scampi", "price": 2099, "category": "Entrees", "sku": "REST-ENT-009" },
20
+
21
+ { "name": "French Fries", "price": 499, "category": "Sides", "sku": "REST-SID-001" },
22
+ { "name": "Sweet Potato Fries", "price": 599, "category": "Sides", "sku": "REST-SID-002" },
23
+ { "name": "Onion Rings", "price": 549, "category": "Sides", "sku": "REST-SID-003" },
24
+ { "name": "Coleslaw", "price": 399, "category": "Sides", "sku": "REST-SID-004" },
25
+ { "name": "Mashed Potatoes", "price": 499, "category": "Sides", "sku": "REST-SID-005" },
26
+ { "name": "Steamed Vegetables", "price": 449, "category": "Sides", "sku": "REST-SID-006" },
27
+ { "name": "Garlic Bread", "price": 449, "category": "Sides", "sku": "REST-SID-007" },
28
+
29
+ { "name": "Chocolate Brownie", "price": 699, "category": "Desserts", "sku": "REST-DES-001" },
30
+ { "name": "New York Cheesecake", "price": 799, "category": "Desserts", "sku": "REST-DES-002" },
31
+ { "name": "Apple Pie", "price": 749, "category": "Desserts", "sku": "REST-DES-003" },
32
+ { "name": "Tiramisu", "price": 899, "category": "Desserts", "sku": "REST-DES-004" },
33
+ { "name": "Creme Brulee", "price": 949, "category": "Desserts", "sku": "REST-DES-005" },
34
+
35
+ { "name": "Soft Drink", "price": 299, "category": "Beverages", "sku": "REST-BEV-001" },
36
+ { "name": "Iced Tea", "price": 299, "category": "Beverages", "sku": "REST-BEV-002" },
37
+ { "name": "Lemonade", "price": 349, "category": "Beverages", "sku": "REST-BEV-003" },
38
+ { "name": "Coffee", "price": 349, "category": "Beverages", "sku": "REST-BEV-004" },
39
+ { "name": "Hot Tea", "price": 299, "category": "Beverages", "sku": "REST-BEV-005" },
40
+ { "name": "Sparkling Water", "price": 349, "category": "Beverages", "sku": "REST-BEV-006" },
41
+ { "name": "Fresh Juice", "price": 499, "category": "Beverages", "sku": "REST-BEV-007" },
42
+
43
+ { "name": "Draft Beer", "price": 599, "category": "Alcohol", "sku": "REST-ALC-001" },
44
+ { "name": "Domestic Beer", "price": 499, "category": "Alcohol", "sku": "REST-ALC-002" },
45
+ { "name": "Import Beer", "price": 649, "category": "Alcohol", "sku": "REST-ALC-003" },
46
+ { "name": "House Wine", "price": 799, "category": "Alcohol", "sku": "REST-ALC-004" },
47
+ { "name": "Margarita", "price": 999, "category": "Alcohol", "sku": "REST-ALC-005" },
48
+ { "name": "Old Fashioned", "price": 1299, "category": "Alcohol", "sku": "REST-ALC-006" },
49
+
50
+ { "name": "Chicken Tenders", "price": 799, "category": "Kids Menu", "sku": "REST-KID-001" },
51
+ { "name": "Kids Mac and Cheese", "price": 649, "category": "Kids Menu", "sku": "REST-KID-002" },
52
+ { "name": "Mini Burger", "price": 699, "category": "Kids Menu", "sku": "REST-KID-003" },
53
+ { "name": "Grilled Cheese", "price": 599, "category": "Kids Menu", "sku": "REST-KID-004" },
54
+ { "name": "Kids Quesadilla", "price": 649, "category": "Kids Menu", "sku": "REST-KID-005" },
55
+
56
+ { "name": "Chef's Special", "price": 2499, "category": "Specials", "sku": "REST-SPE-001" },
57
+ { "name": "Soup of the Day", "price": 599, "category": "Specials", "sku": "REST-SPE-002" }
58
+ ]
59
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "tenders": [
3
+ { "label": "Cash", "pos_ref": "CASH", "weight": 15 },
4
+ { "label": "Visa", "pos_ref": "VISA", "weight": 30 },
5
+ { "label": "Mastercard", "pos_ref": "MASTERCARD", "weight": 20 },
6
+ { "label": "Amex", "pos_ref": "AMEX", "weight": 10 },
7
+ { "label": "Discover", "pos_ref": "DISCOVER", "weight": 5 },
8
+ { "label": "Gift Card", "pos_ref": "GIFT_CARD", "weight": 10 },
9
+ { "label": "House Account", "pos_ref": "HOUSE_ACCOUNT", "weight": 10 }
10
+ ],
11
+ "revenue_classes": [
12
+ { "label": "Dine-In", "pos_ref": "DINE_IN" },
13
+ { "label": "Takeout", "pos_ref": "TAKEOUT" },
14
+ { "label": "Delivery", "pos_ref": "DELIVERY" },
15
+ { "label": "Bar", "pos_ref": "BAR" },
16
+ { "label": "Catering", "pos_ref": "CATERING" }
17
+ ],
18
+ "tax_rates": [
19
+ { "name": "Sales Tax", "rate": 82500, "note": "8.25% — divide by 10,000 for percentage" },
20
+ { "name": "Alcohol Tax", "rate": 100000, "note": "10.0% — divide by 10,000 for percentage" }
21
+ ]
22
+ }
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "logger"
5
+
6
+ module SkytabSandboxSimulator
7
+ # Standalone ActiveRecord connection manager for PostgreSQL.
8
+ #
9
+ # Provides database connectivity without requiring Rails.
10
+ # Used for persisting SkyTab sandbox data (locations, orders, etc.)
11
+ # alongside the existing JSON-file and API-based workflows.
12
+ #
13
+ # @example Connect and run migrations
14
+ # SkytabSandboxSimulator::Database.connect!("postgres://localhost:5432/skytab_simulator_development")
15
+ # SkytabSandboxSimulator::Database.migrate!
16
+ module Database
17
+ # Directory containing ActiveRecord migration files
18
+ MIGRATIONS_PATH = File.expand_path("db/migrate", __dir__).freeze
19
+
20
+ # Default test database name
21
+ TEST_DATABASE = "skytab_simulator_test"
22
+
23
+ class << self
24
+ # Create the database specified in the connection URL.
25
+ #
26
+ # @param url [String] PostgreSQL connection URL
27
+ # @return [void]
28
+ def create!(url)
29
+ db_name = URI.parse(url).path.delete_prefix("/")
30
+ maintenance_url = url.sub(%r{/[^/]+\z}, "/postgres")
31
+
32
+ ActiveRecord::Base.establish_connection(maintenance_url)
33
+ ActiveRecord::Base.connection.create_database(db_name)
34
+ SkytabSandboxSimulator.logger.info("Database created: #{db_name}")
35
+ rescue ActiveRecord::DatabaseAlreadyExists, ActiveRecord::StatementInvalid => e
36
+ if e.message.include?("already exists")
37
+ SkytabSandboxSimulator.logger.info("Database already exists: #{db_name}")
38
+ else
39
+ raise
40
+ end
41
+ ensure
42
+ ActiveRecord::Base.connection_pool.disconnect!
43
+ end
44
+
45
+ # Drop the database specified in the connection URL.
46
+ #
47
+ # @param url [String] PostgreSQL connection URL
48
+ # @return [void]
49
+ def drop!(url)
50
+ db_name = URI.parse(url).path.delete_prefix("/")
51
+ maintenance_url = url.sub(%r{/[^/]+\z}, "/postgres")
52
+
53
+ ActiveRecord::Base.establish_connection(maintenance_url)
54
+ ActiveRecord::Base.connection.drop_database(db_name)
55
+ SkytabSandboxSimulator.logger.info("Database dropped: #{db_name}")
56
+ rescue ActiveRecord::StatementInvalid => e
57
+ if e.message.include?("does not exist")
58
+ SkytabSandboxSimulator.logger.info("Database does not exist: #{db_name}")
59
+ else
60
+ raise
61
+ end
62
+ ensure
63
+ ActiveRecord::Base.connection_pool.disconnect!
64
+ end
65
+
66
+ # Return the configured database URL from .env.json.
67
+ #
68
+ # @return [String] PostgreSQL URL
69
+ # @raise [Error] if no DATABASE_URL is configured
70
+ def database_url
71
+ url = Configuration.database_url_from_file
72
+ raise Error, "No DATABASE_URL found in .env.json" unless url
73
+
74
+ url
75
+ end
76
+
77
+ # Establish a standalone ActiveRecord connection to PostgreSQL.
78
+ #
79
+ # @param url [String] A PostgreSQL connection URL
80
+ # @return [void]
81
+ def connect!(url)
82
+ unless url.match?(%r{\Apostgres(ql)?://}i)
83
+ raise ArgumentError, "Expected a PostgreSQL URL (postgres:// or postgresql://), got: #{url.split('://').first}://"
84
+ end
85
+
86
+ ActiveRecord::Base.establish_connection(url)
87
+
88
+ # Verify the connection is actually usable
89
+ ActiveRecord::Base.connection.execute("SELECT 1")
90
+
91
+ ActiveRecord::Base.logger = SkytabSandboxSimulator.logger
92
+
93
+ SkytabSandboxSimulator.logger.info("Database connected: #{sanitize_url(url)}")
94
+ end
95
+
96
+ # Run pending migrations from lib/skytab_sandbox_simulator/db/migrate/.
97
+ #
98
+ # @return [void]
99
+ def migrate!
100
+ ensure_connected!
101
+
102
+ SkytabSandboxSimulator.logger.info("Running migrations from #{MIGRATIONS_PATH}")
103
+
104
+ context = ActiveRecord::MigrationContext.new(MIGRATIONS_PATH)
105
+ context.migrate
106
+
107
+ SkytabSandboxSimulator.logger.info("Migrations complete")
108
+ end
109
+
110
+ # Seed the database with realistic SkyTab data using FactoryBot.
111
+ #
112
+ # @param business_type [Symbol, String, nil] Optional business type.
113
+ # Seeds all types if nil.
114
+ # @return [Hash] Summary counts
115
+ def seed!(business_type: nil)
116
+ ensure_connected!
117
+ load_factories!
118
+
119
+ business_type ||= SkytabSandboxSimulator.configuration.business_type
120
+ Seeder.seed!(business_type: business_type)
121
+ end
122
+
123
+ # Check whether a database connection is established and usable.
124
+ #
125
+ # @return [Boolean]
126
+ def connected?
127
+ ActiveRecord::Base.connection_pool.with_connection do |conn|
128
+ conn.active?
129
+ end
130
+ rescue StandardError
131
+ false
132
+ end
133
+
134
+ # Disconnect from the database and clean up the connection pool.
135
+ #
136
+ # @return [void]
137
+ def disconnect!
138
+ ActiveRecord::Base.connection_pool.disconnect!
139
+ SkytabSandboxSimulator.logger.info("Database disconnected")
140
+ end
141
+
142
+ # Build the test database URL.
143
+ #
144
+ # @param base_url [String, nil] Base URL to derive test URL from.
145
+ # @return [String] PostgreSQL URL pointing to the test database
146
+ def test_database_url(base_url: nil)
147
+ url = base_url || Configuration.database_url_from_file
148
+ return "postgres://localhost:5432/#{TEST_DATABASE}" if url.nil?
149
+
150
+ uri = URI.parse(url)
151
+ uri.path = "/#{TEST_DATABASE}"
152
+ uri.to_s
153
+ rescue URI::InvalidURIError
154
+ "postgres://localhost:5432/#{TEST_DATABASE}"
155
+ end
156
+
157
+ private
158
+
159
+ # Raise if no database connection has been established yet.
160
+ def ensure_connected!
161
+ return if connected?
162
+
163
+ raise SkytabSandboxSimulator::Error,
164
+ "Database not connected. Call Database.connect!(url) first."
165
+ end
166
+
167
+ # Load FactoryBot factory definitions from the factories directory.
168
+ def load_factories!
169
+ return if @factories_loaded
170
+
171
+ require "factory_bot"
172
+
173
+ factories_path = File.expand_path("db/factories", __dir__)
174
+ FactoryBot.definition_file_paths = [factories_path] if Dir.exist?(factories_path)
175
+ FactoryBot.find_definitions
176
+ @factories_loaded = true
177
+ rescue StandardError => e
178
+ SkytabSandboxSimulator.logger.warn("Could not load factories: #{e.message}")
179
+ end
180
+
181
+ # Strip credentials from a database URL for safe logging.
182
+ def sanitize_url(url)
183
+ uri = URI.parse(url)
184
+ uri.user = "***" if uri.user
185
+ uri.password = "***" if uri.password
186
+ uri.to_s
187
+ rescue URI::InvalidURIError
188
+ url.gsub(%r{://[^@]+@}, "://***:***@")
189
+ end
190
+ end
191
+ end
192
+ end