square_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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +176 -0
  4. data/bin/simulate +388 -0
  5. data/lib/square_sandbox_simulator/configuration.rb +193 -0
  6. data/lib/square_sandbox_simulator/data/cafe_bakery/categories.json +54 -0
  7. data/lib/square_sandbox_simulator/data/cafe_bakery/combos.json +33 -0
  8. data/lib/square_sandbox_simulator/data/cafe_bakery/coupon_codes.json +133 -0
  9. data/lib/square_sandbox_simulator/data/cafe_bakery/discounts.json +113 -0
  10. data/lib/square_sandbox_simulator/data/cafe_bakery/items.json +55 -0
  11. data/lib/square_sandbox_simulator/data/cafe_bakery/modifiers.json +73 -0
  12. data/lib/square_sandbox_simulator/data/cafe_bakery/tax_rates.json +26 -0
  13. data/lib/square_sandbox_simulator/data/cafe_bakery/tenders.json +41 -0
  14. data/lib/square_sandbox_simulator/data/restaurant/categories.json +54 -0
  15. data/lib/square_sandbox_simulator/data/restaurant/combos.json +265 -0
  16. data/lib/square_sandbox_simulator/data/restaurant/coupon_codes.json +266 -0
  17. data/lib/square_sandbox_simulator/data/restaurant/discounts.json +198 -0
  18. data/lib/square_sandbox_simulator/data/restaurant/gift_cards.json +82 -0
  19. data/lib/square_sandbox_simulator/data/restaurant/items.json +388 -0
  20. data/lib/square_sandbox_simulator/data/restaurant/modifiers.json +62 -0
  21. data/lib/square_sandbox_simulator/data/restaurant/tax_rates.json +38 -0
  22. data/lib/square_sandbox_simulator/data/restaurant/tenders.json +41 -0
  23. data/lib/square_sandbox_simulator/data/salon_spa/categories.json +24 -0
  24. data/lib/square_sandbox_simulator/data/salon_spa/combos.json +88 -0
  25. data/lib/square_sandbox_simulator/data/salon_spa/coupon_codes.json +96 -0
  26. data/lib/square_sandbox_simulator/data/salon_spa/discounts.json +93 -0
  27. data/lib/square_sandbox_simulator/data/salon_spa/gift_cards.json +47 -0
  28. data/lib/square_sandbox_simulator/data/salon_spa/items.json +100 -0
  29. data/lib/square_sandbox_simulator/data/salon_spa/modifiers.json +49 -0
  30. data/lib/square_sandbox_simulator/data/salon_spa/tax_rates.json +17 -0
  31. data/lib/square_sandbox_simulator/data/salon_spa/tenders.json +41 -0
  32. data/lib/square_sandbox_simulator/database.rb +224 -0
  33. data/lib/square_sandbox_simulator/db/factories/api_requests.rb +95 -0
  34. data/lib/square_sandbox_simulator/db/factories/business_types.rb +178 -0
  35. data/lib/square_sandbox_simulator/db/factories/categories.rb +379 -0
  36. data/lib/square_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
  37. data/lib/square_sandbox_simulator/db/factories/items.rb +1526 -0
  38. data/lib/square_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
  39. data/lib/square_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
  40. data/lib/square_sandbox_simulator/db/migrate/20260312000000_enable_pgcrypto.rb +7 -0
  41. data/lib/square_sandbox_simulator/db/migrate/20260312000001_create_business_types.rb +18 -0
  42. data/lib/square_sandbox_simulator/db/migrate/20260312000002_create_categories.rb +18 -0
  43. data/lib/square_sandbox_simulator/db/migrate/20260312000003_create_items.rb +23 -0
  44. data/lib/square_sandbox_simulator/db/migrate/20260312000004_create_simulated_orders.rb +36 -0
  45. data/lib/square_sandbox_simulator/db/migrate/20260312000005_create_simulated_payments.rb +26 -0
  46. data/lib/square_sandbox_simulator/db/migrate/20260312000006_create_api_requests.rb +27 -0
  47. data/lib/square_sandbox_simulator/db/migrate/20260312000007_create_daily_summaries.rb +24 -0
  48. data/lib/square_sandbox_simulator/generators/data_loader.rb +202 -0
  49. data/lib/square_sandbox_simulator/generators/entity_generator.rb +248 -0
  50. data/lib/square_sandbox_simulator/generators/order_generator.rb +632 -0
  51. data/lib/square_sandbox_simulator/models/api_request.rb +43 -0
  52. data/lib/square_sandbox_simulator/models/business_type.rb +25 -0
  53. data/lib/square_sandbox_simulator/models/category.rb +18 -0
  54. data/lib/square_sandbox_simulator/models/daily_summary.rb +68 -0
  55. data/lib/square_sandbox_simulator/models/item.rb +33 -0
  56. data/lib/square_sandbox_simulator/models/record.rb +16 -0
  57. data/lib/square_sandbox_simulator/models/simulated_order.rb +42 -0
  58. data/lib/square_sandbox_simulator/models/simulated_payment.rb +28 -0
  59. data/lib/square_sandbox_simulator/seeder.rb +242 -0
  60. data/lib/square_sandbox_simulator/services/base_service.rb +253 -0
  61. data/lib/square_sandbox_simulator/services/square/catalog_service.rb +203 -0
  62. data/lib/square_sandbox_simulator/services/square/customer_service.rb +130 -0
  63. data/lib/square_sandbox_simulator/services/square/order_service.rb +121 -0
  64. data/lib/square_sandbox_simulator/services/square/payment_service.rb +136 -0
  65. data/lib/square_sandbox_simulator/services/square/services_manager.rb +68 -0
  66. data/lib/square_sandbox_simulator/services/square/team_service.rb +108 -0
  67. data/lib/square_sandbox_simulator/version.rb +5 -0
  68. data/lib/square_sandbox_simulator.rb +47 -0
  69. metadata +348 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97c99937dc2438c125576c76a62612f9642a065d4be80f501680dc02bf2f3118
4
+ data.tar.gz: 99dc484286596d4a19ae911f9d5600dc784445323f94d7f87b61e33595d0d566
5
+ SHA512:
6
+ metadata.gz: 1a4e5e86e18e94828bc3bda772e22fc243814779615b3f8eade31da9187524c412610fc75915f10519d2e4aa3af3a373dc775fd9ed9f856741c2102593b11ad2
7
+ data.tar.gz: fba074d8567aec60944de486d64e5e1576c47a4df91fdff97c9fb8f16a7bc0d3e4225aaa262cd8467e2b6bad8cdffd3ee2fda2d5b91264881ba253dfd74342bc
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 TheOwnerStack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Square Sandbox Simulator
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/square_sandbox_simulator.svg)](https://rubygems.org/gems/square_sandbox_simulator)
4
+
5
+ Generate realistic POS data in your Square sandbox — categories, items, discounts, taxes, team members, customers, orders, payments, and refunds. Built for testing Square integrations without manual data entry.
6
+
7
+ ## Features
8
+
9
+ - **9 business types** — restaurant, cafe/bakery, bar, food truck, fine dining, pizzeria, retail clothing, retail general, salon/spa
10
+ - **1000+ menu items** with realistic prices via FactoryBot factories
11
+ - **Realistic order simulation** — meal periods, dining options, tips, split payments, refunds
12
+ - **Idempotent operations** — safe to re-run; entities matched by name, orders tracked by ID
13
+ - **Full audit trail** — every API request/response persisted to PostgreSQL
14
+ - **Timezone-aware** — fetches merchant timezone from Square API
15
+ - **CLI + programmatic API** — use from terminal or integrate into your Rails app
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ gem install square_sandbox_simulator
21
+ ```
22
+
23
+ Or add to your Gemfile:
24
+
25
+ ```ruby
26
+ gem "square_sandbox_simulator"
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Configure credentials
32
+
33
+ Create a `.env.json` file:
34
+
35
+ ```json
36
+ {
37
+ "DATABASE_URL": "postgres://localhost:5432/square_simulator_development",
38
+ "locations": [
39
+ {
40
+ "SQUARE_LOCATION_ID": "YOUR_LOCATION_ID",
41
+ "SQUARE_ACCESS_TOKEN": "YOUR_SANDBOX_ACCESS_TOKEN",
42
+ "SQUARE_LOCATION_NAME": "Test Location"
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ ### 2. Set up database (optional, for audit trail)
49
+
50
+ ```bash
51
+ simulate db create
52
+ simulate db migrate
53
+ simulate db seed --type restaurant
54
+ ```
55
+
56
+ ### 3. Create entities in Square
57
+
58
+ ```bash
59
+ simulate setup --business_type restaurant
60
+ ```
61
+
62
+ This creates categories, items, discounts, tax rates, team members, and customers in your Square sandbox.
63
+
64
+ ### 4. Generate orders
65
+
66
+ ```bash
67
+ # Full realistic day (40-120 orders based on day of week)
68
+ simulate day
69
+
70
+ # Specific count
71
+ simulate generate -n 50
72
+
73
+ # Busy day (2x volume)
74
+ simulate day -x 2.0
75
+ ```
76
+
77
+ ## CLI Commands
78
+
79
+ | Command | Description |
80
+ |---------|-------------|
81
+ | `simulate setup` | Create entities (categories, items, discounts, taxes, team, customers) |
82
+ | `simulate generate -n N` | Generate N orders for today |
83
+ | `simulate day [-x MULT]` | Generate realistic full day of orders |
84
+ | `simulate status` | Show entity counts |
85
+ | `simulate orders [-l N]` | List recent orders |
86
+ | `simulate summary [-d DATE]` | Daily summary (requires DB) |
87
+ | `simulate delete --confirm` | Delete all entities |
88
+ | `simulate locations` | List configured locations |
89
+ | `simulate db create` | Create PostgreSQL database |
90
+ | `simulate db migrate` | Run migrations |
91
+ | `simulate db seed` | Seed business type data |
92
+ | `simulate db reset --confirm` | Drop, create, migrate, seed |
93
+
94
+ **Global flags:** `-v` (verbose), `-l LOCATION_ID`, `-i INDEX` (location index)
95
+
96
+ ## Programmatic Usage
97
+
98
+ ```ruby
99
+ require "square_sandbox_simulator"
100
+
101
+ # Configure
102
+ SquareSandboxSimulator.configure do |config|
103
+ config.location_id = "YOUR_LOCATION_ID"
104
+ config.access_token = "YOUR_ACCESS_TOKEN"
105
+ config.business_type = :restaurant
106
+ end
107
+
108
+ # Set up entities
109
+ generator = SquareSandboxSimulator::Generators::EntityGenerator.new(
110
+ business_type: :restaurant
111
+ )
112
+ result = generator.setup_all
113
+ # => { categories: [...], items: [...], discounts: [...], tax_rates: [...], ... }
114
+
115
+ # Generate orders
116
+ order_gen = SquareSandboxSimulator::Generators::OrderGenerator.new(
117
+ refund_percentage: 5
118
+ )
119
+ orders = order_gen.generate_realistic_day(multiplier: 1.0)
120
+ # or
121
+ orders = order_gen.generate_for_date(Date.today, count: 50)
122
+ ```
123
+
124
+ ## Order Simulation Details
125
+
126
+ Orders follow realistic restaurant patterns:
127
+
128
+ | Meal Period | Hours | Weight | Avg Items | Avg Party |
129
+ |-------------|-------|--------|-----------|-----------|
130
+ | Breakfast | 7-10 | 15% | 3-6 | 1-3 |
131
+ | Lunch | 11-14 | 30% | 3-7 | 1-4 |
132
+ | Happy Hour | 15-17 | 10% | 3-6 | 2-5 |
133
+ | Dinner | 17-21 | 35% | 4-9 | 2-6 |
134
+ | Late Night | 21-23 | 10% | 3-6 | 1-3 |
135
+
136
+ **Daily volume:** weekday 40-60, friday 70-100, saturday 80-120, sunday 50-80
137
+
138
+ **Payment mix:** 75% card, 25% cash. Tips: 15-25% dine-in, 0-15% takeout, 10-20% delivery.
139
+
140
+ **Refunds:** 5% of orders by default (configurable).
141
+
142
+ ## Database Schema
143
+
144
+ All tables use UUID primary keys. The gem works without a database — audit logging is optional and non-blocking.
145
+
146
+ | Table | Purpose |
147
+ |-------|---------|
148
+ | `business_types` | 9 business types with order profiles |
149
+ | `categories` | Menu categories per business type |
150
+ | `items` | Menu items with prices (cents) |
151
+ | `simulated_orders` | Generated order records |
152
+ | `simulated_payments` | Payment records per order |
153
+ | `api_requests` | Full API audit trail (request + response) |
154
+ | `daily_summaries` | Aggregated daily metrics |
155
+
156
+ ## Services
157
+
158
+ The gem wraps 5 Square API service areas:
159
+
160
+ - **CatalogService** — categories, items, discounts, taxes (CRUD + batch)
161
+ - **OrderService** — create, search, calculate orders
162
+ - **PaymentService** — card payments, cash payments, refunds
163
+ - **CustomerService** — create/list customers (20 default)
164
+ - **TeamService** — create/search team members (5 default)
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ bundle install
170
+ bundle exec rspec # 575 tests, 100% branch coverage
171
+ bundle exec rubocop # 0 offenses
172
+ ```
173
+
174
+ ## License
175
+
176
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/bin/simulate ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "square_sandbox_simulator"
6
+ require "thor"
7
+
8
+ module SquareSandboxSimulator
9
+ # Command-line interface for Square Sandbox Simulator
10
+ class CLI < Thor
11
+ class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable verbose logging"
12
+ class_option :location, type: :string, aliases: "-l", desc: "Location ID to use (from .env.json)"
13
+ class_option :location_index, type: :numeric, aliases: "-i", desc: "Location index to use (0-based, from .env.json)"
14
+
15
+ desc "locations", "List available locations from .env.json"
16
+ def locations
17
+ puts "Square Sandbox Simulator - Available Locations"
18
+ puts "=" * 60
19
+
20
+ config = SquareSandboxSimulator.configuration
21
+ locs = config.available_locations
22
+
23
+ if locs.empty?
24
+ puts "No locations found in .env.json"
25
+ return
26
+ end
27
+
28
+ puts "\n#{"Index".ljust(6)} #{"Location ID".ljust(20)} Name"
29
+ puts "-" * 60
30
+
31
+ locs.each_with_index do |loc, idx|
32
+ puts "#{idx.to_s.ljust(6)} #{(loc[:id] || "N/A").ljust(20)} #{loc[:name] || "N/A"}"
33
+ end
34
+
35
+ puts "\nUse: simulate <command> -i <index> or -l <location_id>"
36
+ end
37
+
38
+ desc "setup", "Set up entities (categories, items, discounts, taxes, team members, customers)"
39
+ option :business_type, type: :string, default: "restaurant", desc: "Business type (restaurant, retail_clothing, cafe_bakery, etc.)"
40
+ def setup
41
+ configure_logging
42
+
43
+ puts "Square Sandbox Simulator - Entity Setup"
44
+ puts "=" * 50
45
+
46
+ generator = Generators::EntityGenerator.new(business_type: options[:business_type].to_sym)
47
+ generator.setup_all
48
+
49
+ puts "\nSetup complete!"
50
+ end
51
+
52
+ desc "generate", "Generate orders for today"
53
+ option :count, type: :numeric, aliases: "-n", desc: "Number of orders to generate"
54
+ option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
55
+ desc: "Percentage of orders to refund (0-100, default 5)"
56
+ def generate
57
+ configure_logging
58
+
59
+ puts "Square Sandbox Simulator - Order Generation"
60
+ puts "=" * 50
61
+
62
+ generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
63
+ count = options[:count]
64
+
65
+ orders = generator.generate_today(count: count)
66
+
67
+ puts "\nGenerated #{orders.size} orders!"
68
+ end
69
+
70
+ desc "day", "Generate a realistic full day of operations"
71
+ option :multiplier, type: :numeric, aliases: "-x", default: 1.0, desc: "Order multiplier (0.5 = slow day, 2.0 = busy day)"
72
+ option :refund_percentage, type: :numeric, aliases: "-r", default: 5,
73
+ desc: "Percentage of orders to refund (0-100, default 5)"
74
+ def day
75
+ configure_logging
76
+
77
+ puts "Square Sandbox Simulator - Realistic Day"
78
+ puts "=" * 50
79
+
80
+ generator = Generators::OrderGenerator.new(refund_percentage: options[:refund_percentage])
81
+ orders = generator.generate_realistic_day(multiplier: options[:multiplier])
82
+
83
+ puts "\nGenerated #{orders.size} orders!"
84
+ end
85
+
86
+ desc "status", "Show current Square location status"
87
+ def status
88
+ configure_logging
89
+
90
+ puts "Square Sandbox Simulator - Status"
91
+ puts "=" * 50
92
+
93
+ services = Services::Square::ServicesManager.new
94
+
95
+ categories = services.catalog.list_catalog(types: "CATEGORY")
96
+ items = services.catalog.list_catalog(types: "ITEM")
97
+ discounts = services.catalog.list_catalog(types: "DISCOUNT")
98
+ taxes = services.catalog.list_catalog(types: "TAX")
99
+ customers = services.customer.list_customers
100
+ team_members = services.team.search_team_members
101
+
102
+ puts "\nEntity Counts:"
103
+ puts " Categories: #{categories.size}"
104
+ puts " Items: #{items.size}"
105
+ puts " Discounts: #{discounts.size}"
106
+ puts " Tax Rates: #{taxes.size}"
107
+ puts " Team Members: #{team_members.size}"
108
+ puts " Customers: #{customers.size}"
109
+
110
+ if categories.any?
111
+ puts "\nCategories:"
112
+ categories.each { |c| puts " - #{c.dig("category_data", "name")}" }
113
+ end
114
+
115
+ if taxes.any?
116
+ puts "\nTax Rates:"
117
+ taxes.each do |t|
118
+ td = t["tax_data"] || {}
119
+ puts " - #{td["name"]}: #{td["percentage"]}%"
120
+ end
121
+ end
122
+
123
+ return unless discounts.any?
124
+
125
+ puts "\nDiscounts:"
126
+ discounts.each do |d|
127
+ dd = d["discount_data"] || {}
128
+ type = dd["discount_type"] || "N/A"
129
+ value = if dd["percentage"]
130
+ "#{dd["percentage"]}%"
131
+ elsif dd.dig("amount_money", "amount")
132
+ "$#{format("%.2f", dd["amount_money"]["amount"] / 100.0)}"
133
+ else
134
+ "N/A"
135
+ end
136
+ puts " - #{dd["name"]} (#{type}: #{value})"
137
+ end
138
+ end
139
+
140
+ desc "orders", "List recent orders"
141
+ option :limit, type: :numeric, aliases: "-l", default: 20, desc: "Number of orders to show"
142
+ def orders
143
+ configure_logging
144
+
145
+ puts "Square Sandbox Simulator - Recent Orders"
146
+ puts "=" * 50
147
+
148
+ services = Services::Square::ServicesManager.new
149
+ recent = services.order.search_orders
150
+
151
+ if recent.empty?
152
+ puts "No orders found."
153
+ puts "\nRun 'simulate generate' to create orders."
154
+ return
155
+ end
156
+
157
+ # Limit display
158
+ display = recent.first(options[:limit])
159
+
160
+ puts "\nFound #{recent.size} orders (showing #{display.size}):\n\n"
161
+
162
+ total_revenue = 0
163
+ display.each do |order|
164
+ total = order.dig("total_money", "amount") || 0
165
+ total_revenue += total
166
+ state = order["state"] || "unknown"
167
+ order["created_at"] || "N/A"
168
+
169
+ state_marker = case state
170
+ when "OPEN" then "[OPEN]"
171
+ when "COMPLETED" then "[PAID]"
172
+ else "[#{state}]"
173
+ end
174
+
175
+ items_count = (order["line_items"] || []).size
176
+ puts "#{state_marker.ljust(12)} $#{format("%.2f", total / 100.0)} | #{items_count} items | #{order["id"]}"
177
+ end
178
+
179
+ puts "\n#{"-" * 50}"
180
+ puts "Total: $#{format("%.2f", total_revenue / 100.0)} across #{display.size} orders"
181
+ end
182
+
183
+ desc "summary", "Show daily summary for current location"
184
+ option :date, type: :string, aliases: "-d", desc: "Date (YYYY-MM-DD, default: today)"
185
+ def summary
186
+ configure_logging
187
+ require_db_connection!
188
+
189
+ date = options[:date] ? Date.parse(options[:date]) : Date.today
190
+ location_id = SquareSandboxSimulator.configuration.location_id
191
+
192
+ s = Models::DailySummary.for_merchant(location_id).on_date(date).first
193
+
194
+ unless s
195
+ puts "No summary found for location #{location_id} on #{date}."
196
+ puts "Run 'simulate generate' to create orders first."
197
+ return
198
+ end
199
+
200
+ puts "Daily Summary: #{date}"
201
+ puts "=" * 50
202
+ puts " Location: #{location_id}"
203
+ puts " Orders: #{s.order_count}"
204
+ puts " Payments: #{s.payment_count}"
205
+ puts " Refunds: #{s.refund_count}"
206
+ puts ""
207
+ puts " Revenue: #{format_cents(s.total_revenue)}"
208
+ puts " Tax: #{format_cents(s.total_tax)}"
209
+ puts " Tips: #{format_cents(s.total_tips)}"
210
+ puts " Discounts: #{format_cents(s.total_discounts)}"
211
+
212
+ breakdown = s.breakdown || {}
213
+
214
+ if breakdown["by_meal_period"]&.any?
215
+ puts ""
216
+ puts " By Meal Period:"
217
+ breakdown["by_meal_period"].each do |period, count|
218
+ rev = breakdown.dig("revenue_by_meal_period", period) || 0
219
+ puts " #{period.ljust(15)} #{count.to_s.rjust(3)} orders #{format_cents(rev)}"
220
+ end
221
+ end
222
+
223
+ if breakdown["by_dining_option"]&.any?
224
+ puts ""
225
+ puts " By Dining Option:"
226
+ breakdown["by_dining_option"].each do |opt, count|
227
+ rev = breakdown.dig("revenue_by_dining_option", opt) || 0
228
+ puts " #{opt.ljust(15)} #{count.to_s.rjust(3)} orders #{format_cents(rev)}"
229
+ end
230
+ end
231
+
232
+ return unless breakdown["by_tender"]&.any?
233
+
234
+ puts ""
235
+ puts " By Tender:"
236
+ breakdown["by_tender"].each do |tender, count|
237
+ puts " #{tender.ljust(20)} #{count} payments"
238
+ end
239
+ end
240
+
241
+ desc "delete", "Delete all entities (use with caution!)"
242
+ option :confirm, type: :boolean, desc: "Confirm deletion"
243
+ def delete
244
+ unless options[:confirm]
245
+ puts "This will delete ALL entities from Square!"
246
+ puts " Run with --confirm to proceed."
247
+ return
248
+ end
249
+
250
+ configure_logging
251
+
252
+ puts "Deleting all entities..."
253
+
254
+ generator = Generators::EntityGenerator.new
255
+ generator.delete_all
256
+
257
+ puts "\nAll entities deleted!"
258
+ end
259
+
260
+ # ============================================
261
+ # Database Management Commands (db: namespace)
262
+ # ============================================
263
+
264
+ desc "db SUBCOMMAND", "Database management commands"
265
+ subcommand "db", Class.new(Thor) {
266
+ def self.banner(task, _namespace = true, _subcommand = true)
267
+ "simulate db #{task.usage}"
268
+ end
269
+
270
+ namespace "db"
271
+
272
+ desc "create", "Create the PostgreSQL database"
273
+ def create
274
+ db = SquareSandboxSimulator::Database
275
+ url = db.database_url
276
+ puts "Creating database..."
277
+ db.create!(url)
278
+ puts "Done."
279
+ rescue SquareSandboxSimulator::Error => e
280
+ puts "Error: #{e.message}"
281
+ exit 1
282
+ end
283
+
284
+ desc "migrate", "Run pending migrations"
285
+ def migrate
286
+ db = SquareSandboxSimulator::Database
287
+ url = db.database_url
288
+ puts "Connecting and running migrations..."
289
+ db.connect!(url)
290
+ db.migrate!
291
+ puts "Done."
292
+ rescue SquareSandboxSimulator::Error => e
293
+ puts "Error: #{e.message}"
294
+ exit 1
295
+ end
296
+
297
+ desc "seed", "Seed the database with realistic data via FactoryBot"
298
+ option :type, type: :string, desc: "Business type to seed (e.g. restaurant, retail_clothing). Seeds all if omitted."
299
+ def seed
300
+ db = SquareSandboxSimulator::Database
301
+ url = db.database_url
302
+ puts "Connecting and seeding..."
303
+ db.connect!(url)
304
+ bt = options[:type]&.to_sym
305
+ result = db.seed!(business_type: bt)
306
+ puts "Seeded: #{result[:business_types]} business types, #{result[:categories]} categories, #{result[:items]} items"
307
+ puts " (#{result[:created]} created, #{result[:found]} already existed)"
308
+ rescue SquareSandboxSimulator::Error => e
309
+ puts "Error: #{e.message}"
310
+ exit 1
311
+ end
312
+
313
+ desc "reset", "Drop, create, migrate, and seed the database"
314
+ option :type, type: :string, desc: "Business type to seed (e.g. restaurant). Seeds all if omitted."
315
+ option :confirm, type: :boolean, desc: "Confirm destructive operation"
316
+ def reset
317
+ unless options[:confirm]
318
+ puts "This will DROP and recreate the database. All data will be lost."
319
+ puts "Run with --confirm to proceed."
320
+ return
321
+ end
322
+
323
+ db = SquareSandboxSimulator::Database
324
+ url = db.database_url
325
+
326
+ puts "Dropping database..."
327
+ db.drop!(url)
328
+
329
+ puts "Creating database..."
330
+ db.create!(url)
331
+
332
+ puts "Connecting and running migrations..."
333
+ db.connect!(url)
334
+ db.migrate!
335
+
336
+ puts "Seeding..."
337
+ bt = options[:type]&.to_sym
338
+ result = db.seed!(business_type: bt)
339
+ puts "Seeded: #{result[:business_types]} business types, #{result[:categories]} categories, #{result[:items]} items"
340
+ puts "Done."
341
+ rescue SquareSandboxSimulator::Error => e
342
+ puts "Error: #{e.message}"
343
+ exit 1
344
+ end
345
+ }
346
+
347
+ desc "version", "Show version"
348
+ def version
349
+ puts "Square Sandbox Simulator v#{SquareSandboxSimulator::VERSION}"
350
+ end
351
+
352
+ private
353
+
354
+ def format_cents(cents)
355
+ "$#{format("%.2f", (cents || 0) / 100.0)}"
356
+ end
357
+
358
+ def require_db_connection!
359
+ url = Database.database_url
360
+ Database.connect!(url) unless Database.connected?
361
+ rescue Error => e
362
+ puts "Database not available: #{e.message}"
363
+ puts "Run 'simulate db create && simulate db migrate' first."
364
+ exit 1
365
+ end
366
+
367
+ def configure_logging
368
+ config = SquareSandboxSimulator.configuration
369
+
370
+ config.logger.level = if options[:verbose]
371
+ Logger::DEBUG
372
+ else
373
+ Logger::INFO
374
+ end
375
+
376
+ # Load specific location if specified
377
+ if options[:location]
378
+ config.load_location(location_id: options[:location])
379
+ elsif options[:location_index]
380
+ config.load_location(index: options[:location_index])
381
+ end
382
+
383
+ puts "Using location: #{config.location_name || config.location_id}"
384
+ end
385
+ end
386
+ end
387
+
388
+ SquareSandboxSimulator::CLI.start(ARGV)