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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +176 -0
- data/bin/simulate +388 -0
- data/lib/square_sandbox_simulator/configuration.rb +193 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/categories.json +54 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/combos.json +33 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/coupon_codes.json +133 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/discounts.json +113 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/items.json +55 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/modifiers.json +73 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/tax_rates.json +26 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/tenders.json +41 -0
- data/lib/square_sandbox_simulator/data/restaurant/categories.json +54 -0
- data/lib/square_sandbox_simulator/data/restaurant/combos.json +265 -0
- data/lib/square_sandbox_simulator/data/restaurant/coupon_codes.json +266 -0
- data/lib/square_sandbox_simulator/data/restaurant/discounts.json +198 -0
- data/lib/square_sandbox_simulator/data/restaurant/gift_cards.json +82 -0
- data/lib/square_sandbox_simulator/data/restaurant/items.json +388 -0
- data/lib/square_sandbox_simulator/data/restaurant/modifiers.json +62 -0
- data/lib/square_sandbox_simulator/data/restaurant/tax_rates.json +38 -0
- data/lib/square_sandbox_simulator/data/restaurant/tenders.json +41 -0
- data/lib/square_sandbox_simulator/data/salon_spa/categories.json +24 -0
- data/lib/square_sandbox_simulator/data/salon_spa/combos.json +88 -0
- data/lib/square_sandbox_simulator/data/salon_spa/coupon_codes.json +96 -0
- data/lib/square_sandbox_simulator/data/salon_spa/discounts.json +93 -0
- data/lib/square_sandbox_simulator/data/salon_spa/gift_cards.json +47 -0
- data/lib/square_sandbox_simulator/data/salon_spa/items.json +100 -0
- data/lib/square_sandbox_simulator/data/salon_spa/modifiers.json +49 -0
- data/lib/square_sandbox_simulator/data/salon_spa/tax_rates.json +17 -0
- data/lib/square_sandbox_simulator/data/salon_spa/tenders.json +41 -0
- data/lib/square_sandbox_simulator/database.rb +224 -0
- data/lib/square_sandbox_simulator/db/factories/api_requests.rb +95 -0
- data/lib/square_sandbox_simulator/db/factories/business_types.rb +178 -0
- data/lib/square_sandbox_simulator/db/factories/categories.rb +379 -0
- data/lib/square_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
- data/lib/square_sandbox_simulator/db/factories/items.rb +1526 -0
- data/lib/square_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
- data/lib/square_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000000_enable_pgcrypto.rb +7 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000001_create_business_types.rb +18 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000002_create_categories.rb +18 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000003_create_items.rb +23 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000004_create_simulated_orders.rb +36 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000005_create_simulated_payments.rb +26 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000006_create_api_requests.rb +27 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000007_create_daily_summaries.rb +24 -0
- data/lib/square_sandbox_simulator/generators/data_loader.rb +202 -0
- data/lib/square_sandbox_simulator/generators/entity_generator.rb +248 -0
- data/lib/square_sandbox_simulator/generators/order_generator.rb +632 -0
- data/lib/square_sandbox_simulator/models/api_request.rb +43 -0
- data/lib/square_sandbox_simulator/models/business_type.rb +25 -0
- data/lib/square_sandbox_simulator/models/category.rb +18 -0
- data/lib/square_sandbox_simulator/models/daily_summary.rb +68 -0
- data/lib/square_sandbox_simulator/models/item.rb +33 -0
- data/lib/square_sandbox_simulator/models/record.rb +16 -0
- data/lib/square_sandbox_simulator/models/simulated_order.rb +42 -0
- data/lib/square_sandbox_simulator/models/simulated_payment.rb +28 -0
- data/lib/square_sandbox_simulator/seeder.rb +242 -0
- data/lib/square_sandbox_simulator/services/base_service.rb +253 -0
- data/lib/square_sandbox_simulator/services/square/catalog_service.rb +203 -0
- data/lib/square_sandbox_simulator/services/square/customer_service.rb +130 -0
- data/lib/square_sandbox_simulator/services/square/order_service.rb +121 -0
- data/lib/square_sandbox_simulator/services/square/payment_service.rb +136 -0
- data/lib/square_sandbox_simulator/services/square/services_manager.rb +68 -0
- data/lib/square_sandbox_simulator/services/square/team_service.rb +108 -0
- data/lib/square_sandbox_simulator/version.rb +5 -0
- data/lib/square_sandbox_simulator.rb +47 -0
- 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
|
+
[](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)
|