clover_sandbox_simulator 1.0.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 (25) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +10 -0
  3. data/README.md +316 -0
  4. data/bin/simulate +209 -0
  5. data/lib/clover_sandbox_simulator/configuration.rb +51 -0
  6. data/lib/clover_sandbox_simulator/data/restaurant/categories.json +39 -0
  7. data/lib/clover_sandbox_simulator/data/restaurant/discounts.json +39 -0
  8. data/lib/clover_sandbox_simulator/data/restaurant/items.json +238 -0
  9. data/lib/clover_sandbox_simulator/data/restaurant/modifiers.json +62 -0
  10. data/lib/clover_sandbox_simulator/data/restaurant/tenders.json +41 -0
  11. data/lib/clover_sandbox_simulator/generators/data_loader.rb +54 -0
  12. data/lib/clover_sandbox_simulator/generators/entity_generator.rb +164 -0
  13. data/lib/clover_sandbox_simulator/generators/order_generator.rb +540 -0
  14. data/lib/clover_sandbox_simulator/services/base_service.rb +111 -0
  15. data/lib/clover_sandbox_simulator/services/clover/customer_service.rb +82 -0
  16. data/lib/clover_sandbox_simulator/services/clover/discount_service.rb +58 -0
  17. data/lib/clover_sandbox_simulator/services/clover/employee_service.rb +82 -0
  18. data/lib/clover_sandbox_simulator/services/clover/inventory_service.rb +120 -0
  19. data/lib/clover_sandbox_simulator/services/clover/order_service.rb +170 -0
  20. data/lib/clover_sandbox_simulator/services/clover/payment_service.rb +123 -0
  21. data/lib/clover_sandbox_simulator/services/clover/services_manager.rb +49 -0
  22. data/lib/clover_sandbox_simulator/services/clover/tax_service.rb +53 -0
  23. data/lib/clover_sandbox_simulator/services/clover/tender_service.rb +117 -0
  24. data/lib/clover_sandbox_simulator.rb +43 -0
  25. metadata +195 -0
@@ -0,0 +1,238 @@
1
+ {
2
+ "items": [
3
+ {
4
+ "name": "Buffalo Wings",
5
+ "price": 1299,
6
+ "category": "Appetizers",
7
+ "description": "Crispy wings tossed in buffalo sauce"
8
+ },
9
+ {
10
+ "name": "Loaded Nachos",
11
+ "price": 1099,
12
+ "category": "Appetizers",
13
+ "description": "Tortilla chips with cheese, jalapeños, and salsa"
14
+ },
15
+ {
16
+ "name": "Mozzarella Sticks",
17
+ "price": 899,
18
+ "category": "Appetizers",
19
+ "description": "Golden fried with marinara sauce"
20
+ },
21
+ {
22
+ "name": "Spinach Artichoke Dip",
23
+ "price": 1199,
24
+ "category": "Appetizers",
25
+ "description": "Creamy dip with tortilla chips"
26
+ },
27
+ {
28
+ "name": "Calamari",
29
+ "price": 1399,
30
+ "category": "Appetizers",
31
+ "description": "Lightly fried with aioli"
32
+ },
33
+ {
34
+ "name": "Classic Burger",
35
+ "price": 1499,
36
+ "category": "Entrees",
37
+ "description": "8oz beef patty with lettuce, tomato, onion"
38
+ },
39
+ {
40
+ "name": "Bacon Cheeseburger",
41
+ "price": 1699,
42
+ "category": "Entrees",
43
+ "description": "Burger with bacon and cheddar"
44
+ },
45
+ {
46
+ "name": "Grilled Salmon",
47
+ "price": 2199,
48
+ "category": "Entrees",
49
+ "description": "Atlantic salmon with lemon herb butter"
50
+ },
51
+ {
52
+ "name": "NY Strip Steak",
53
+ "price": 2899,
54
+ "category": "Entrees",
55
+ "description": "12oz steak cooked to order"
56
+ },
57
+ {
58
+ "name": "Chicken Parmesan",
59
+ "price": 1899,
60
+ "category": "Entrees",
61
+ "description": "Breaded chicken with marinara and mozzarella"
62
+ },
63
+ {
64
+ "name": "Fettuccine Alfredo",
65
+ "price": 1599,
66
+ "category": "Entrees",
67
+ "description": "Creamy parmesan sauce over pasta"
68
+ },
69
+ {
70
+ "name": "Fish and Chips",
71
+ "price": 1699,
72
+ "category": "Entrees",
73
+ "description": "Beer-battered cod with fries"
74
+ },
75
+ {
76
+ "name": "BBQ Ribs",
77
+ "price": 2399,
78
+ "category": "Entrees",
79
+ "description": "Full rack with house BBQ sauce"
80
+ },
81
+ {
82
+ "name": "Caesar Salad",
83
+ "price": 1199,
84
+ "category": "Entrees",
85
+ "description": "Romaine, parmesan, croutons"
86
+ },
87
+ {
88
+ "name": "Grilled Chicken Salad",
89
+ "price": 1399,
90
+ "category": "Entrees",
91
+ "description": "Mixed greens with grilled chicken"
92
+ },
93
+ {
94
+ "name": "French Fries",
95
+ "price": 499,
96
+ "category": "Sides",
97
+ "description": "Crispy golden fries"
98
+ },
99
+ {
100
+ "name": "Sweet Potato Fries",
101
+ "price": 599,
102
+ "category": "Sides",
103
+ "description": "With honey drizzle"
104
+ },
105
+ {
106
+ "name": "Onion Rings",
107
+ "price": 549,
108
+ "category": "Sides",
109
+ "description": "Beer-battered and fried"
110
+ },
111
+ {
112
+ "name": "Coleslaw",
113
+ "price": 399,
114
+ "category": "Sides",
115
+ "description": "Creamy house-made coleslaw"
116
+ },
117
+ {
118
+ "name": "Mashed Potatoes",
119
+ "price": 499,
120
+ "category": "Sides",
121
+ "description": "Creamy with gravy"
122
+ },
123
+ {
124
+ "name": "Steamed Vegetables",
125
+ "price": 449,
126
+ "category": "Sides",
127
+ "description": "Seasonal vegetables"
128
+ },
129
+ {
130
+ "name": "Chocolate Brownie",
131
+ "price": 699,
132
+ "category": "Desserts",
133
+ "description": "Warm brownie with ice cream"
134
+ },
135
+ {
136
+ "name": "New York Cheesecake",
137
+ "price": 799,
138
+ "category": "Desserts",
139
+ "description": "Classic creamy cheesecake"
140
+ },
141
+ {
142
+ "name": "Apple Pie",
143
+ "price": 749,
144
+ "category": "Desserts",
145
+ "description": "A la mode with vanilla ice cream"
146
+ },
147
+ {
148
+ "name": "Tiramisu",
149
+ "price": 899,
150
+ "category": "Desserts",
151
+ "description": "Italian coffee-flavored dessert"
152
+ },
153
+ {
154
+ "name": "Soft Drink",
155
+ "price": 299,
156
+ "category": "Drinks",
157
+ "description": "Coke, Sprite, Dr Pepper"
158
+ },
159
+ {
160
+ "name": "Iced Tea",
161
+ "price": 299,
162
+ "category": "Drinks",
163
+ "description": "Fresh brewed, sweetened or unsweetened"
164
+ },
165
+ {
166
+ "name": "Lemonade",
167
+ "price": 349,
168
+ "category": "Drinks",
169
+ "description": "Fresh squeezed lemonade"
170
+ },
171
+ {
172
+ "name": "Coffee",
173
+ "price": 349,
174
+ "category": "Drinks",
175
+ "description": "Regular or decaf"
176
+ },
177
+ {
178
+ "name": "Hot Tea",
179
+ "price": 299,
180
+ "category": "Drinks",
181
+ "description": "Assorted flavors"
182
+ },
183
+ {
184
+ "name": "Draft Beer",
185
+ "price": 599,
186
+ "category": "Alcoholic Beverages",
187
+ "description": "Rotating selection of craft beers"
188
+ },
189
+ {
190
+ "name": "Domestic Beer",
191
+ "price": 499,
192
+ "category": "Alcoholic Beverages",
193
+ "description": "Bud Light, Coors, Miller"
194
+ },
195
+ {
196
+ "name": "Import Beer",
197
+ "price": 649,
198
+ "category": "Alcoholic Beverages",
199
+ "description": "Corona, Heineken, Guinness"
200
+ },
201
+ {
202
+ "name": "House Wine",
203
+ "price": 799,
204
+ "category": "Alcoholic Beverages",
205
+ "description": "Red or white by the glass"
206
+ },
207
+ {
208
+ "name": "Premium Wine",
209
+ "price": 1199,
210
+ "category": "Alcoholic Beverages",
211
+ "description": "Cabernet, Chardonnay, Pinot Noir"
212
+ },
213
+ {
214
+ "name": "Margarita",
215
+ "price": 999,
216
+ "category": "Alcoholic Beverages",
217
+ "description": "House or frozen"
218
+ },
219
+ {
220
+ "name": "Long Island Iced Tea",
221
+ "price": 1099,
222
+ "category": "Alcoholic Beverages",
223
+ "description": "Classic cocktail"
224
+ },
225
+ {
226
+ "name": "Chef's Special",
227
+ "price": 2499,
228
+ "category": "Specials",
229
+ "description": "Ask your server for today's special"
230
+ },
231
+ {
232
+ "name": "Soup of the Day",
233
+ "price": 599,
234
+ "category": "Specials",
235
+ "description": "Fresh made daily"
236
+ }
237
+ ]
238
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "modifier_groups": [
3
+ {
4
+ "name": "Temperature",
5
+ "min_required": 0,
6
+ "max_allowed": 1,
7
+ "modifiers": [
8
+ { "name": "Rare", "price": 0 },
9
+ { "name": "Medium Rare", "price": 0 },
10
+ { "name": "Medium", "price": 0 },
11
+ { "name": "Medium Well", "price": 0 },
12
+ { "name": "Well Done", "price": 0 }
13
+ ]
14
+ },
15
+ {
16
+ "name": "Add-Ons",
17
+ "min_required": 0,
18
+ "max_allowed": 5,
19
+ "modifiers": [
20
+ { "name": "Extra Cheese", "price": 150 },
21
+ { "name": "Bacon", "price": 200 },
22
+ { "name": "Avocado", "price": 175 },
23
+ { "name": "Fried Egg", "price": 125 },
24
+ { "name": "Jalapeños", "price": 75 }
25
+ ]
26
+ },
27
+ {
28
+ "name": "Side Choice",
29
+ "min_required": 1,
30
+ "max_allowed": 1,
31
+ "modifiers": [
32
+ { "name": "French Fries", "price": 0 },
33
+ { "name": "Sweet Potato Fries", "price": 100 },
34
+ { "name": "Onion Rings", "price": 100 },
35
+ { "name": "Side Salad", "price": 0 },
36
+ { "name": "Coleslaw", "price": 0 }
37
+ ]
38
+ },
39
+ {
40
+ "name": "Dressing",
41
+ "min_required": 0,
42
+ "max_allowed": 1,
43
+ "modifiers": [
44
+ { "name": "Ranch", "price": 0 },
45
+ { "name": "Blue Cheese", "price": 0 },
46
+ { "name": "Caesar", "price": 0 },
47
+ { "name": "Balsamic Vinaigrette", "price": 0 },
48
+ { "name": "Honey Mustard", "price": 0 }
49
+ ]
50
+ },
51
+ {
52
+ "name": "Drink Size",
53
+ "min_required": 1,
54
+ "max_allowed": 1,
55
+ "modifiers": [
56
+ { "name": "Small", "price": 0 },
57
+ { "name": "Medium", "price": 50 },
58
+ { "name": "Large", "price": 100 }
59
+ ]
60
+ }
61
+ ]
62
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "tenders": [
3
+ {
4
+ "label": "Cash",
5
+ "label_key": "com.clover.tender.cash",
6
+ "opens_cash_drawer": true,
7
+ "weight": 30
8
+ },
9
+ {
10
+ "label": "Check",
11
+ "label_key": "com.clover.tender.check",
12
+ "opens_cash_drawer": true,
13
+ "weight": 5
14
+ },
15
+ {
16
+ "label": "Gift Card",
17
+ "label_key": "com.clover.tender.external_gift_card",
18
+ "opens_cash_drawer": false,
19
+ "weight": 15
20
+ },
21
+ {
22
+ "label": "External Payment",
23
+ "label_key": "com.clover.tender.external_payment",
24
+ "opens_cash_drawer": false,
25
+ "weight": 10
26
+ },
27
+ {
28
+ "label": "Mobile Payment",
29
+ "label_key": "com.clover.tender.mobile_payment",
30
+ "opens_cash_drawer": false,
31
+ "weight": 20
32
+ },
33
+ {
34
+ "label": "Store Credit",
35
+ "label_key": "com.clover.tender.store_credit",
36
+ "opens_cash_drawer": false,
37
+ "weight": 10
38
+ }
39
+ ],
40
+ "note": "Credit Card and Debit Card are intentionally excluded - they are broken in Clover sandbox"
41
+ }
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloverSandboxSimulator
4
+ module Generators
5
+ # Loads data from JSON files for different business types
6
+ class DataLoader
7
+ attr_reader :business_type
8
+
9
+ def initialize(business_type: :restaurant)
10
+ @business_type = business_type
11
+ end
12
+
13
+ def categories
14
+ @categories ||= load_json("categories")["categories"]
15
+ end
16
+
17
+ def items
18
+ @items ||= load_json("items")["items"]
19
+ end
20
+
21
+ def discounts
22
+ @discounts ||= load_json("discounts")["discounts"]
23
+ end
24
+
25
+ def tenders
26
+ @tenders ||= load_json("tenders")["tenders"]
27
+ end
28
+
29
+ def modifiers
30
+ @modifiers ||= load_json("modifiers")["modifier_groups"]
31
+ end
32
+
33
+ def items_for_category(category_name)
34
+ items.select { |item| item["category"] == category_name }
35
+ end
36
+
37
+ private
38
+
39
+ def load_json(filename)
40
+ path = File.join(data_path, "#{filename}.json")
41
+
42
+ unless File.exist?(path)
43
+ raise Error, "Data file not found: #{path}"
44
+ end
45
+
46
+ JSON.parse(File.read(path))
47
+ end
48
+
49
+ def data_path
50
+ File.join(CloverSandboxSimulator.root, "lib", "clover_sandbox_simulator", "data", business_type.to_s)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CloverSandboxSimulator
4
+ module Generators
5
+ # Creates restaurant entities in Clover (categories, items, discounts, etc.)
6
+ class EntityGenerator
7
+ attr_reader :services, :data, :logger
8
+
9
+ def initialize(services: nil, business_type: :restaurant)
10
+ @services = services || Services::Clover::ServicesManager.new
11
+ @data = DataLoader.new(business_type: business_type)
12
+ @logger = CloverSandboxSimulator.logger
13
+ end
14
+
15
+ # Set up all entities (categories, items, discounts, etc.)
16
+ def setup_all
17
+ logger.info "=" * 60
18
+ logger.info "Setting up restaurant entities in Clover..."
19
+ logger.info "=" * 60
20
+
21
+ results = {
22
+ categories: setup_categories,
23
+ items: setup_items,
24
+ discounts: setup_discounts,
25
+ employees: setup_employees,
26
+ customers: setup_customers
27
+ }
28
+
29
+ logger.info "=" * 60
30
+ logger.info "Entity setup complete!"
31
+ logger.info " Categories: #{results[:categories].size}"
32
+ logger.info " Items: #{results[:items].size}"
33
+ logger.info " Discounts: #{results[:discounts].size}"
34
+ logger.info " Employees: #{results[:employees].size}"
35
+ logger.info " Customers: #{results[:customers].size}"
36
+ logger.info "=" * 60
37
+
38
+ results
39
+ end
40
+
41
+ # Create categories from data file
42
+ def setup_categories
43
+ logger.info "Setting up categories..."
44
+
45
+ existing = services.inventory.get_categories
46
+ existing_names = existing.map { |c| c["name"] }
47
+
48
+ created = []
49
+ data.categories.each do |cat_data|
50
+ if existing_names.include?(cat_data["name"])
51
+ logger.debug "Category '#{cat_data["name"]}' already exists, skipping"
52
+ created << existing.find { |c| c["name"] == cat_data["name"] }
53
+ else
54
+ cat = services.inventory.create_category(
55
+ name: cat_data["name"],
56
+ sort_order: cat_data["sort_order"]
57
+ )
58
+ created << cat if cat
59
+ end
60
+ end
61
+
62
+ logger.info "Categories ready: #{created.size}"
63
+ created
64
+ end
65
+
66
+ # Create items from data file
67
+ def setup_items
68
+ logger.info "Setting up menu items..."
69
+
70
+ # Build category lookup
71
+ categories = services.inventory.get_categories
72
+ category_lookup = categories.each_with_object({}) do |cat, hash|
73
+ hash[cat["name"]] = cat["id"]
74
+ end
75
+
76
+ existing = services.inventory.get_items
77
+ existing_names = existing.map { |i| i["name"] }
78
+
79
+ created = []
80
+ data.items.each do |item_data|
81
+ if existing_names.include?(item_data["name"])
82
+ logger.debug "Item '#{item_data["name"]}' already exists, skipping"
83
+ created << existing.find { |i| i["name"] == item_data["name"] }
84
+ else
85
+ category_id = category_lookup[item_data["category"]]
86
+
87
+ item = services.inventory.create_item(
88
+ name: item_data["name"],
89
+ price: item_data["price"],
90
+ category_id: category_id
91
+ )
92
+ created << item if item
93
+ end
94
+ end
95
+
96
+ logger.info "Items ready: #{created.size}"
97
+ created
98
+ end
99
+
100
+ # Create discounts from data file
101
+ def setup_discounts
102
+ logger.info "Setting up discounts..."
103
+
104
+ existing = services.discount.get_discounts
105
+ existing_names = existing.map { |d| d["name"] }
106
+
107
+ created = []
108
+ data.discounts.each do |disc_data|
109
+ if existing_names.include?(disc_data["name"])
110
+ logger.debug "Discount '#{disc_data["name"]}' already exists, skipping"
111
+ created << existing.find { |d| d["name"] == disc_data["name"] }
112
+ else
113
+ disc = if disc_data["percentage"]
114
+ services.discount.create_percentage_discount(
115
+ name: disc_data["name"],
116
+ percentage: disc_data["percentage"]
117
+ )
118
+ else
119
+ services.discount.create_fixed_discount(
120
+ name: disc_data["name"],
121
+ amount: disc_data["amount"]
122
+ )
123
+ end
124
+ created << disc if disc
125
+ end
126
+ end
127
+
128
+ logger.info "Discounts ready: #{created.size}"
129
+ created
130
+ end
131
+
132
+ # Ensure employees exist
133
+ def setup_employees
134
+ logger.info "Setting up employees..."
135
+ employees = services.employee.ensure_employees(count: 5)
136
+ logger.info "Employees ready: #{employees.size}"
137
+ employees
138
+ end
139
+
140
+ # Ensure customers exist
141
+ def setup_customers
142
+ logger.info "Setting up customers..."
143
+ customers = services.customer.ensure_customers(count: 20)
144
+ logger.info "Customers ready: #{customers.size}"
145
+ customers
146
+ end
147
+
148
+ # Delete all entities (for clean slate)
149
+ def delete_all
150
+ logger.warn "=" * 60
151
+ logger.warn "DELETING ALL ENTITIES..."
152
+ logger.warn "=" * 60
153
+
154
+ services.inventory.delete_all
155
+
156
+ services.discount.get_discounts.each do |d|
157
+ services.discount.delete_discount(d["id"])
158
+ end
159
+
160
+ logger.info "All entities deleted"
161
+ end
162
+ end
163
+ end
164
+ end