lightspeed_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/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +215 -0
- data/bin/simulate +162 -0
- data/lib/lightspeed_sandbox_simulator/configuration.rb +151 -0
- data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/categories.json +9 -0
- data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/items.json +26 -0
- data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/tenders.json +8 -0
- data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/categories.json +9 -0
- data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/items.json +28 -0
- data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/tenders.json +8 -0
- data/lib/lightspeed_sandbox_simulator/data/restaurant/categories.json +9 -0
- data/lib/lightspeed_sandbox_simulator/data/restaurant/items.json +29 -0
- data/lib/lightspeed_sandbox_simulator/data/restaurant/tenders.json +9 -0
- data/lib/lightspeed_sandbox_simulator/data/retail_general/categories.json +9 -0
- data/lib/lightspeed_sandbox_simulator/data/retail_general/items.json +17 -0
- data/lib/lightspeed_sandbox_simulator/data/retail_general/tenders.json +8 -0
- data/lib/lightspeed_sandbox_simulator/database.rb +116 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/api_requests.rb +10 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/business_types.rb +9 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/categories.rb +9 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/items.rb +11 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/simulated_orders.rb +17 -0
- data/lib/lightspeed_sandbox_simulator/db/factories/simulated_payments.rb +13 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000001_enable_pgcrypto.rb +7 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000002_create_business_types.rb +14 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000003_create_categories.rb +13 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000004_create_items.rb +14 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000005_create_simulated_orders.rb +23 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000006_create_simulated_payments.rb +16 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000007_create_api_requests.rb +18 -0
- data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000008_create_daily_summaries.rb +20 -0
- data/lib/lightspeed_sandbox_simulator/generators/data_loader.rb +75 -0
- data/lib/lightspeed_sandbox_simulator/generators/entity_generator.rb +96 -0
- data/lib/lightspeed_sandbox_simulator/generators/order_generator.rb +293 -0
- data/lib/lightspeed_sandbox_simulator/models/api_request.rb +9 -0
- data/lib/lightspeed_sandbox_simulator/models/business_type.rb +12 -0
- data/lib/lightspeed_sandbox_simulator/models/category.rb +12 -0
- data/lib/lightspeed_sandbox_simulator/models/daily_summary.rb +44 -0
- data/lib/lightspeed_sandbox_simulator/models/item.rb +11 -0
- data/lib/lightspeed_sandbox_simulator/models/simulated_order.rb +15 -0
- data/lib/lightspeed_sandbox_simulator/models/simulated_payment.rb +13 -0
- data/lib/lightspeed_sandbox_simulator/seeder.rb +79 -0
- data/lib/lightspeed_sandbox_simulator/services/base_service.rb +158 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/business_service.rb +21 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/menu_service.rb +70 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/order_service.rb +74 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/payment_method_service.rb +30 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/payment_service.rb +55 -0
- data/lib/lightspeed_sandbox_simulator/services/lightspeed/services_manager.rb +54 -0
- data/lib/lightspeed_sandbox_simulator.rb +30 -0
- metadata +332 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 91921b4c2a3b26ae9a5444309b1b9ea33816c83c281ff8bdd2bd0961fa2e55de
|
|
4
|
+
data.tar.gz: 7aa36df7a8e33b200db8106c5821a4983ba6d0cf190580ef478be969dc64abd1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4bd2dae32589ca317c60335aead9fb0b906adbe4947e8600a8b6a5ea711d7e0bc2a9dc94065b4c0288374c16a769850c0b41f4df4b2405108ceb71fc566037d0
|
|
7
|
+
data.tar.gz: c8a316d2e875b428477b49e54f7d6259fc77504049de10e900f20403009ca472316d115edf731688278cac0216ed288388d2f58c138fff340e4a50788508627b
|
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dan1d
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Lightspeed Sandbox Simulator
|
|
2
|
+
|
|
3
|
+
A Ruby gem for simulating POS operations against the **Lightspeed K-Series API**. Generates realistic orders, payments, and transaction data for development and testing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Entity Setup** — Seed categories, menu items, and payment methods via the Lightspeed K-Series API
|
|
8
|
+
- **Order Generation** — Create realistic daily order patterns with meal periods, dining options, tips, and discounts
|
|
9
|
+
- **Payment Simulation** — Process payments with weighted tender selection (Cash, Credit Card, etc.)
|
|
10
|
+
- **Refund Processing** — Simulate refund flows at configurable percentages
|
|
11
|
+
- **Multi-Business-Type Support** — Restaurant, Cafe & Bakery, Bar & Nightclub, Retail General
|
|
12
|
+
- **Database Tracking** — Optional PostgreSQL persistence for orders, payments, API requests, and daily summaries
|
|
13
|
+
- **Thor CLI** — Command-line interface for all operations
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'lightspeed_sandbox_simulator'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install lightspeed_sandbox_simulator
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
### Environment Variables
|
|
30
|
+
|
|
31
|
+
Create a `.env` file:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
LIGHTSPEED_ACCESS_TOKEN=your_access_token
|
|
35
|
+
LIGHTSPEED_BUSINESS_ID=your_business_id
|
|
36
|
+
LIGHTSPEED_CLIENT_ID=your_client_id # optional
|
|
37
|
+
LIGHTSPEED_CLIENT_SECRET=your_client_secret # optional
|
|
38
|
+
LIGHTSPEED_REFRESH_TOKEN=your_refresh_token # optional
|
|
39
|
+
LOG_LEVEL=INFO
|
|
40
|
+
TAX_RATE=20.0
|
|
41
|
+
LIGHTSPEED_TIMEZONE=America/New_York
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Multi-Merchant Support
|
|
45
|
+
|
|
46
|
+
Create a `.env.json` file for multiple merchants:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
[
|
|
50
|
+
{
|
|
51
|
+
"LIGHTSPEED_ACCESS_TOKEN": "token_1",
|
|
52
|
+
"LIGHTSPEED_BUSINESS_ID": "business_1",
|
|
53
|
+
"LIGHTSPEED_DEVICE_NAME": "Store A"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"LIGHTSPEED_ACCESS_TOKEN": "token_2",
|
|
57
|
+
"LIGHTSPEED_BUSINESS_ID": "business_2",
|
|
58
|
+
"LIGHTSPEED_DEVICE_NAME": "Store B"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Ruby Configuration
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
LightspeedSandboxSimulator.configure do |config|
|
|
67
|
+
config.access_token = "your_access_token"
|
|
68
|
+
config.business_id = "your_business_id"
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
### CLI
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Set up menu entities (categories, items, payment methods)
|
|
78
|
+
bin/simulate setup --business-type restaurant
|
|
79
|
+
|
|
80
|
+
# Generate a day's worth of orders
|
|
81
|
+
bin/simulate generate --count 50 --business-type restaurant
|
|
82
|
+
|
|
83
|
+
# Generate a realistic day (volume based on day-of-week)
|
|
84
|
+
bin/simulate generate --realistic --business-type cafe_bakery
|
|
85
|
+
|
|
86
|
+
# Generate a rush period
|
|
87
|
+
bin/simulate rush --period dinner --count 20
|
|
88
|
+
|
|
89
|
+
# List available merchants
|
|
90
|
+
bin/simulate merchants
|
|
91
|
+
|
|
92
|
+
# Database operations (optional)
|
|
93
|
+
bin/simulate db:create
|
|
94
|
+
bin/simulate db:migrate
|
|
95
|
+
bin/simulate db:seed --business-type all
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Ruby API
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
require "lightspeed_sandbox_simulator"
|
|
102
|
+
|
|
103
|
+
config = LightspeedSandboxSimulator::Configuration.new
|
|
104
|
+
config.access_token = "your_token"
|
|
105
|
+
config.business_id = "your_business_id"
|
|
106
|
+
|
|
107
|
+
# Set up entities
|
|
108
|
+
generator = LightspeedSandboxSimulator::Generators::EntityGenerator.new(
|
|
109
|
+
config: config,
|
|
110
|
+
business_type: :restaurant
|
|
111
|
+
)
|
|
112
|
+
result = generator.setup_all
|
|
113
|
+
# => { categories: [...], items: [...], payment_methods: [...] }
|
|
114
|
+
|
|
115
|
+
# Generate orders
|
|
116
|
+
order_gen = LightspeedSandboxSimulator::Generators::OrderGenerator.new(
|
|
117
|
+
config: config,
|
|
118
|
+
business_type: :restaurant,
|
|
119
|
+
refund_percentage: 5
|
|
120
|
+
)
|
|
121
|
+
orders = order_gen.generate_today(count: 50)
|
|
122
|
+
|
|
123
|
+
# Use services directly
|
|
124
|
+
manager = LightspeedSandboxSimulator::Services::Lightspeed::ServicesManager.new(
|
|
125
|
+
config: config
|
|
126
|
+
)
|
|
127
|
+
manager.menu.list_categories
|
|
128
|
+
manager.menu.create_item(name: "Espresso", price: 3.50, category_id: 1)
|
|
129
|
+
manager.orders.create_local_order(items: [{ item_id: 1, quantity: 2 }], table_number: 5)
|
|
130
|
+
manager.payments.create_payment(order_id: 100, amount: 25.50, payment_method_id: 1)
|
|
131
|
+
manager.business.fetch_business
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Business Types
|
|
135
|
+
|
|
136
|
+
| Type | Key | Categories |
|
|
137
|
+
|------|-----|------------|
|
|
138
|
+
| Restaurant | `:restaurant` | Appetizers, Entrees, Sides, Desserts, Beverages |
|
|
139
|
+
| Cafe & Bakery | `:cafe_bakery` | Hot Drinks, Cold Drinks, Pastries, Sandwiches, Snacks |
|
|
140
|
+
| Bar & Nightclub | `:bar_nightclub` | Beer, Wine, Cocktails, Spirits, Bar Food |
|
|
141
|
+
| Retail General | `:retail_general` | Electronics, Clothing, Home, Sports, Accessories |
|
|
142
|
+
|
|
143
|
+
## Order Generation Details
|
|
144
|
+
|
|
145
|
+
Orders are distributed across five meal periods with realistic weights:
|
|
146
|
+
|
|
147
|
+
| Period | Weight | Items per Order |
|
|
148
|
+
|--------|--------|-----------------|
|
|
149
|
+
| Breakfast | 15% | 1–3 |
|
|
150
|
+
| Lunch | 30% | 2–4 |
|
|
151
|
+
| Happy Hour | 10% | 2–4 |
|
|
152
|
+
| Dinner | 35% | 3–6 |
|
|
153
|
+
| Late Night | 10% | 1–3 |
|
|
154
|
+
|
|
155
|
+
Dining options (eat-in, takeaway, delivery) vary by period. Tips and discounts are calculated with configurable probability distributions.
|
|
156
|
+
|
|
157
|
+
## Database (Optional)
|
|
158
|
+
|
|
159
|
+
PostgreSQL tracking is optional. When configured, the gem persists:
|
|
160
|
+
|
|
161
|
+
- **API Requests** — Full audit log of all Lightspeed API calls
|
|
162
|
+
- **Simulated Orders** — Order details with meal period, dining option, amounts
|
|
163
|
+
- **Simulated Payments** — Payment records with tender type and amounts
|
|
164
|
+
- **Daily Summaries** — Aggregated daily stats with breakdowns
|
|
165
|
+
- **Business Types, Categories, Items** — Seeded reference data
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Set DATABASE_URL in .env or .env.json
|
|
169
|
+
DATABASE_URL=postgres://localhost:5432/lightspeed_sandbox
|
|
170
|
+
|
|
171
|
+
bin/simulate db:create
|
|
172
|
+
bin/simulate db:migrate
|
|
173
|
+
bin/simulate db:seed --business-type all
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Lightspeed K-Series API
|
|
177
|
+
|
|
178
|
+
This gem targets the [Lightspeed K-Series (L-Series) API](https://developers.lightspeedhq.com/):
|
|
179
|
+
|
|
180
|
+
- **Base URL**: `https://api.lsk.lightspeed.app`
|
|
181
|
+
- **Auth**: OAuth2 Bearer token
|
|
182
|
+
- **Pagination**: Cursor-based
|
|
183
|
+
- **API Version**: V2
|
|
184
|
+
|
|
185
|
+
### Endpoints Used
|
|
186
|
+
|
|
187
|
+
| Method | Endpoint | Purpose |
|
|
188
|
+
|--------|----------|---------|
|
|
189
|
+
| GET | `/api/v2/businesses/{id}` | Business info |
|
|
190
|
+
| GET | `/api/v2/businesses/{id}/menu/categories` | List categories |
|
|
191
|
+
| POST | `/api/v2/businesses/{id}/menu/categories` | Create category |
|
|
192
|
+
| GET | `/api/v2/businesses/{id}/menu/items` | List items |
|
|
193
|
+
| POST | `/api/v2/businesses/{id}/menu/items` | Create item |
|
|
194
|
+
| GET | `/api/v2/businesses/{id}/payment-methods` | List payment methods |
|
|
195
|
+
| POST | `/api/v2/businesses/{id}/payment-methods` | Create payment method |
|
|
196
|
+
| POST | `/api/v2/businesses/{id}/orders/local` | Create dine-in order |
|
|
197
|
+
| POST | `/api/v2/businesses/{id}/orders/toGo` | Create takeout order |
|
|
198
|
+
| GET | `/api/v2/businesses/{id}/orders` | Fetch orders |
|
|
199
|
+
| POST | `/api/v2/businesses/{id}/payments` | Create payment |
|
|
200
|
+
| GET | `/api/v2/businesses/{id}/payments` | Fetch payments |
|
|
201
|
+
| GET | `/api/v2/businesses/{id}/tax-rates` | List tax rates |
|
|
202
|
+
| GET | `/api/v2/businesses/{id}/floorplans` | List floor plans |
|
|
203
|
+
|
|
204
|
+
## Development
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
bundle install
|
|
208
|
+
bundle exec rspec # Run tests (275 examples)
|
|
209
|
+
COVERAGE=true bundle exec rspec # With coverage report (100% line + branch)
|
|
210
|
+
bundle exec rubocop # Lint (0 offenses)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
[MIT](LICENSE)
|
data/bin/simulate
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'thor'
|
|
5
|
+
require_relative '../lib/lightspeed_sandbox_simulator'
|
|
6
|
+
|
|
7
|
+
module LightspeedSandboxSimulator
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
desc 'version', 'Show version'
|
|
10
|
+
def version
|
|
11
|
+
puts 'lightspeed_sandbox_simulator v0.1.0'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc 'merchants', 'List configured API devices'
|
|
15
|
+
def merchants
|
|
16
|
+
devices = Configuration.available_merchants
|
|
17
|
+
if devices.empty?
|
|
18
|
+
puts 'No devices found in .env.json'
|
|
19
|
+
return
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
devices.each do |d|
|
|
23
|
+
puts "##{d[:index]}: #{d[:name] || 'unnamed'} (business_id: #{d[:business_id]})"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc 'setup', 'Set up POS entities (categories, items, payment methods)'
|
|
28
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
29
|
+
method_option :business_type, aliases: '-b', type: :string, default: 'restaurant'
|
|
30
|
+
def setup
|
|
31
|
+
config = load_config(options[:index])
|
|
32
|
+
bt = options[:business_type].to_sym
|
|
33
|
+
gen = Generators::EntityGenerator.new(config: config, business_type: bt)
|
|
34
|
+
result = gen.setup_all
|
|
35
|
+
puts "Setup complete: #{result[:categories].size} categories, " \
|
|
36
|
+
"#{result[:items].size} items, #{result[:payment_methods].size} payment methods"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc 'generate', 'Generate orders for today'
|
|
40
|
+
method_option :count, aliases: '-n', type: :numeric, desc: 'Number of orders'
|
|
41
|
+
method_option :refund, aliases: '-r', type: :numeric, default: 5, desc: 'Refund percentage'
|
|
42
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
43
|
+
method_option :business_type, aliases: '-b', type: :string, default: 'restaurant'
|
|
44
|
+
def generate
|
|
45
|
+
config = load_config(options[:index])
|
|
46
|
+
gen = Generators::OrderGenerator.new(
|
|
47
|
+
config: config,
|
|
48
|
+
business_type: options[:business_type].to_sym,
|
|
49
|
+
refund_percentage: options[:refund]
|
|
50
|
+
)
|
|
51
|
+
orders = gen.generate_today(count: options[:count])
|
|
52
|
+
puts "Generated #{orders.size} orders"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
desc 'day', 'Generate a realistic full day'
|
|
56
|
+
method_option :multiplier, aliases: '-x', type: :numeric, default: 1.0
|
|
57
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
58
|
+
method_option :business_type, aliases: '-b', type: :string, default: 'restaurant'
|
|
59
|
+
def day
|
|
60
|
+
config = load_config(options[:index])
|
|
61
|
+
gen = Generators::OrderGenerator.new(
|
|
62
|
+
config: config,
|
|
63
|
+
business_type: options[:business_type].to_sym
|
|
64
|
+
)
|
|
65
|
+
orders = gen.generate_realistic_day(multiplier: options[:multiplier])
|
|
66
|
+
puts "Generated #{orders.size} orders (#{options[:multiplier]}x)"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
desc 'rush', 'Generate a meal period rush'
|
|
70
|
+
method_option :period, aliases: '-p', type: :string, default: 'dinner'
|
|
71
|
+
method_option :count, aliases: '-n', type: :numeric, default: 20
|
|
72
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
73
|
+
def rush
|
|
74
|
+
config = load_config(options[:index])
|
|
75
|
+
gen = Generators::OrderGenerator.new(config: config)
|
|
76
|
+
orders = gen.generate_rush(
|
|
77
|
+
period: options[:period].to_sym,
|
|
78
|
+
count: options[:count]
|
|
79
|
+
)
|
|
80
|
+
puts "Generated #{orders.size} #{options[:period]} orders"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'full', 'Full setup + order generation'
|
|
84
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
85
|
+
method_option :business_type, aliases: '-b', type: :string, default: 'restaurant'
|
|
86
|
+
def full
|
|
87
|
+
invoke :setup
|
|
88
|
+
invoke :generate
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
desc 'status', 'Show entity counts'
|
|
92
|
+
method_option :index, aliases: '-i', type: :numeric, desc: 'Device index'
|
|
93
|
+
def status
|
|
94
|
+
config = load_config(options[:index])
|
|
95
|
+
manager = Services::Lightspeed::ServicesManager.new(config: config)
|
|
96
|
+
|
|
97
|
+
categories = manager.menu.list_categories
|
|
98
|
+
cat_count = extract_count(categories, 'categories')
|
|
99
|
+
items = manager.menu.list_items
|
|
100
|
+
item_count = extract_count(items, 'items')
|
|
101
|
+
methods = manager.payment_methods.list_payment_methods
|
|
102
|
+
method_count = extract_count(methods, 'paymentMethods')
|
|
103
|
+
|
|
104
|
+
puts "Categories: #{cat_count}"
|
|
105
|
+
puts "Items: #{item_count}"
|
|
106
|
+
puts "Payment Methods: #{method_count}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
desc 'business_types', 'List available business types'
|
|
110
|
+
def business_types
|
|
111
|
+
Generators::DataLoader::BUSINESS_TYPES.each do |bt|
|
|
112
|
+
loader = Generators::DataLoader.new(business_type: bt)
|
|
113
|
+
cats = loader.load_categories.size
|
|
114
|
+
items = loader.load_items.size
|
|
115
|
+
puts "#{bt}: #{cats} categories, #{items} items"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
desc 'db SUBCOMMAND', 'Database management'
|
|
120
|
+
method_option :business_type, aliases: '-b', type: :string, default: 'restaurant'
|
|
121
|
+
def db(subcommand)
|
|
122
|
+
url = Database.database_url
|
|
123
|
+
case subcommand
|
|
124
|
+
when 'create' then Database.create!(url)
|
|
125
|
+
when 'migrate' then Database.connect!(url) && Database.migrate!
|
|
126
|
+
when 'seed' then Database.connect!(url) && Database.seed!(business_type: options[:business_type].to_sym)
|
|
127
|
+
when 'reset'
|
|
128
|
+
Database.drop!(url)
|
|
129
|
+
Database.create!(url)
|
|
130
|
+
Database.connect!(url)
|
|
131
|
+
Database.migrate!
|
|
132
|
+
Database.seed!(business_type: options[:business_type].to_sym)
|
|
133
|
+
else puts "Unknown subcommand: #{subcommand}. Use: create, migrate, seed, reset"
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def load_config(index = nil)
|
|
140
|
+
if index
|
|
141
|
+
config = Configuration.load_merchant(index: index)
|
|
142
|
+
raise Error, "Device ##{index} not found" unless config
|
|
143
|
+
|
|
144
|
+
config
|
|
145
|
+
else
|
|
146
|
+
LightspeedSandboxSimulator.configuration
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def extract_count(data, key)
|
|
151
|
+
if data.is_a?(Hash) && data.key?(key)
|
|
152
|
+
Array(data[key]).size
|
|
153
|
+
elsif data.is_a?(Array)
|
|
154
|
+
data.size
|
|
155
|
+
else
|
|
156
|
+
0
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
LightspeedSandboxSimulator::CLI.start(ARGV)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dotenv'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'logger'
|
|
6
|
+
require 'tzinfo'
|
|
7
|
+
|
|
8
|
+
module LightspeedSandboxSimulator
|
|
9
|
+
class Configuration
|
|
10
|
+
MERCHANT_KEYS = {
|
|
11
|
+
'LIGHTSPEED_CLIENT_ID' => :client_id,
|
|
12
|
+
'LIGHTSPEED_CLIENT_SECRET' => :client_secret,
|
|
13
|
+
'LIGHTSPEED_ACCESS_TOKEN' => :access_token,
|
|
14
|
+
'LIGHTSPEED_REFRESH_TOKEN' => :refresh_token,
|
|
15
|
+
'LIGHTSPEED_BUSINESS_ID' => :business_id,
|
|
16
|
+
'LIGHTSPEED_DEVICE_NAME' => :device_name
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
DEFAULTS = {
|
|
20
|
+
base_url: 'https://api.lsk.lightspeed.app',
|
|
21
|
+
auth_url: 'https://cloud.lsk.lightspeed.app',
|
|
22
|
+
tax_rate: 20.0,
|
|
23
|
+
merchant_timezone: 'America/New_York'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
attr_accessor :client_id, :client_secret, :access_token, :refresh_token,
|
|
27
|
+
:business_id, :base_url, :auth_url, :tax_rate, :log_level,
|
|
28
|
+
:device_name, :merchant_timezone
|
|
29
|
+
|
|
30
|
+
attr_writer :logger
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
Dotenv.load if File.exist?('.env')
|
|
34
|
+
@client_id = ENV.fetch('LIGHTSPEED_CLIENT_ID', nil)
|
|
35
|
+
@client_secret = ENV.fetch('LIGHTSPEED_CLIENT_SECRET', nil)
|
|
36
|
+
@access_token = ENV.fetch('LIGHTSPEED_ACCESS_TOKEN', nil)
|
|
37
|
+
@refresh_token = ENV.fetch('LIGHTSPEED_REFRESH_TOKEN', nil)
|
|
38
|
+
@business_id = ENV.fetch('LIGHTSPEED_BUSINESS_ID', nil)
|
|
39
|
+
@base_url = normalize_url(ENV.fetch('LIGHTSPEED_BASE_URL', DEFAULTS[:base_url]))
|
|
40
|
+
@auth_url = normalize_url(ENV.fetch('LIGHTSPEED_AUTH_URL', DEFAULTS[:auth_url]))
|
|
41
|
+
@tax_rate = ENV.fetch('TAX_RATE', DEFAULTS[:tax_rate]).to_f
|
|
42
|
+
@log_level = ENV.fetch('LOG_LEVEL', 'INFO')
|
|
43
|
+
@device_name = ENV.fetch('LIGHTSPEED_DEVICE_NAME', nil)
|
|
44
|
+
@merchant_timezone = ENV.fetch('LIGHTSPEED_TIMEZONE', DEFAULTS[:merchant_timezone])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate!
|
|
48
|
+
raise ConfigurationError, 'LIGHTSPEED_ACCESS_TOKEN is required' if access_token.nil? || access_token.empty?
|
|
49
|
+
raise ConfigurationError, 'LIGHTSPEED_BUSINESS_ID is required' if business_id.nil? || business_id.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def logger
|
|
53
|
+
@logger ||= build_logger
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def auth_token
|
|
57
|
+
access_token
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def merchant_time_now
|
|
61
|
+
tz = TZInfo::Timezone.get(merchant_timezone)
|
|
62
|
+
tz.now
|
|
63
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
|
64
|
+
Time.now
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.load_from_merchants_file(path = '.env.json')
|
|
68
|
+
return nil unless File.exist?(path)
|
|
69
|
+
|
|
70
|
+
data = JSON.parse(File.read(path))
|
|
71
|
+
case data
|
|
72
|
+
when Array
|
|
73
|
+
data
|
|
74
|
+
when Hash
|
|
75
|
+
data['merchants'] || []
|
|
76
|
+
end
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.load_merchant(index: nil, name: nil, path: '.env.json')
|
|
82
|
+
merchants = load_from_merchants_file(path)
|
|
83
|
+
return nil if merchants.nil? || merchants.empty?
|
|
84
|
+
|
|
85
|
+
merchant = find_merchant(merchants, index: index, name: name)
|
|
86
|
+
return nil unless merchant
|
|
87
|
+
|
|
88
|
+
build_config_from_merchant(merchant)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.find_merchant(merchants, index: nil, name: nil)
|
|
92
|
+
if name
|
|
93
|
+
merchants.find { |m| m['LIGHTSPEED_DEVICE_NAME']&.downcase == name.downcase }
|
|
94
|
+
elsif index
|
|
95
|
+
merchants[index]
|
|
96
|
+
else
|
|
97
|
+
merchants.first
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
private_class_method :find_merchant
|
|
101
|
+
|
|
102
|
+
def self.build_config_from_merchant(merchant)
|
|
103
|
+
config = new
|
|
104
|
+
MERCHANT_KEYS.each do |env_key, attr|
|
|
105
|
+
value = merchant[env_key]
|
|
106
|
+
config.public_send(:"#{attr}=", value) unless value.to_s.empty?
|
|
107
|
+
end
|
|
108
|
+
config
|
|
109
|
+
end
|
|
110
|
+
private_class_method :build_config_from_merchant
|
|
111
|
+
|
|
112
|
+
def self.available_merchants(path = '.env.json')
|
|
113
|
+
merchants = load_from_merchants_file(path)
|
|
114
|
+
return [] if merchants.nil?
|
|
115
|
+
|
|
116
|
+
merchants.map.with_index do |m, i|
|
|
117
|
+
{ index: i, name: m['LIGHTSPEED_DEVICE_NAME'], business_id: m['LIGHTSPEED_BUSINESS_ID'] }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def self.database_url_from_file(path = '.env.json')
|
|
122
|
+
return nil unless File.exist?(path)
|
|
123
|
+
|
|
124
|
+
data = JSON.parse(File.read(path))
|
|
125
|
+
data.is_a?(Hash) ? data['DATABASE_URL'] : nil
|
|
126
|
+
rescue JSON::ParserError
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def normalize_url(url)
|
|
133
|
+
url&.chomp('/')
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_logger
|
|
137
|
+
log = Logger.new($stdout)
|
|
138
|
+
log.level = parse_log_level(log_level)
|
|
139
|
+
log.formatter = proc { |severity, _datetime, _progname, msg| "[#{severity}] #{msg}\n" }
|
|
140
|
+
log
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_log_level(level)
|
|
144
|
+
{
|
|
145
|
+
'DEBUG' => Logger::DEBUG, 'INFO' => Logger::INFO,
|
|
146
|
+
'WARN' => Logger::WARN, 'ERROR' => Logger::ERROR,
|
|
147
|
+
'FATAL' => Logger::FATAL
|
|
148
|
+
}.fetch(level.to_s.upcase, Logger::INFO)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"categories": [
|
|
3
|
+
{ "name": "Draft Beer", "sort_order": 1, "description": "Beers on tap" },
|
|
4
|
+
{ "name": "Bottled Beer", "sort_order": 2, "description": "Bottled and canned beers" },
|
|
5
|
+
{ "name": "Cocktails", "sort_order": 3, "description": "Mixed drinks" },
|
|
6
|
+
{ "name": "Wine", "sort_order": 4, "description": "Wine by the glass" },
|
|
7
|
+
{ "name": "Bar Snacks", "sort_order": 5, "description": "Bar food and snacks" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"items": [
|
|
3
|
+
{ "name": "IPA Pint", "price": 7.50, "category": "Draft Beer", "sku": "BAR-DRF-001" },
|
|
4
|
+
{ "name": "Lager Pint", "price": 6.50, "category": "Draft Beer", "sku": "BAR-DRF-002" },
|
|
5
|
+
{ "name": "Stout Pint", "price": 7.00, "category": "Draft Beer", "sku": "BAR-DRF-003" },
|
|
6
|
+
{ "name": "Wheat Beer", "price": 7.50, "category": "Draft Beer", "sku": "BAR-DRF-004" },
|
|
7
|
+
{ "name": "Pale Ale", "price": 7.00, "category": "Draft Beer", "sku": "BAR-DRF-005" },
|
|
8
|
+
{ "name": "Corona", "price": 6.00, "category": "Bottled Beer", "sku": "BAR-BTL-001" },
|
|
9
|
+
{ "name": "Heineken", "price": 5.50, "category": "Bottled Beer", "sku": "BAR-BTL-002" },
|
|
10
|
+
{ "name": "Budweiser", "price": 5.00, "category": "Bottled Beer", "sku": "BAR-BTL-003" },
|
|
11
|
+
{ "name": "Seltzer", "price": 5.50, "category": "Bottled Beer", "sku": "BAR-BTL-004" },
|
|
12
|
+
{ "name": "Margarita", "price": 12.00, "category": "Cocktails", "sku": "BAR-COK-001" },
|
|
13
|
+
{ "name": "Old Fashioned", "price": 14.00, "category": "Cocktails", "sku": "BAR-COK-002" },
|
|
14
|
+
{ "name": "Mojito", "price": 12.00, "category": "Cocktails", "sku": "BAR-COK-003" },
|
|
15
|
+
{ "name": "Espresso Martini", "price": 13.00, "category": "Cocktails", "sku": "BAR-COK-004" },
|
|
16
|
+
{ "name": "Long Island", "price": 14.00, "category": "Cocktails", "sku": "BAR-COK-005" },
|
|
17
|
+
{ "name": "House Red Wine", "price": 9.00, "category": "Wine", "sku": "BAR-WIN-001" },
|
|
18
|
+
{ "name": "House White Wine", "price": 9.00, "category": "Wine", "sku": "BAR-WIN-002" },
|
|
19
|
+
{ "name": "Prosecco", "price": 10.00, "category": "Wine", "sku": "BAR-WIN-003" },
|
|
20
|
+
{ "name": "Chicken Wings", "price": 10.99, "category": "Bar Snacks", "sku": "BAR-SNK-001" },
|
|
21
|
+
{ "name": "Loaded Fries", "price": 8.99, "category": "Bar Snacks", "sku": "BAR-SNK-002" },
|
|
22
|
+
{ "name": "Nachos", "price": 11.99, "category": "Bar Snacks", "sku": "BAR-SNK-003" },
|
|
23
|
+
{ "name": "Slider Trio", "price": 12.99, "category": "Bar Snacks", "sku": "BAR-SNK-004" },
|
|
24
|
+
{ "name": "Mixed Nuts", "price": 5.99, "category": "Bar Snacks", "sku": "BAR-SNK-005" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tenders": [
|
|
3
|
+
{ "name": "Cash", "description": "Cash payment", "weight": 20 },
|
|
4
|
+
{ "name": "Credit Card", "description": "Credit card payment", "weight": 55 },
|
|
5
|
+
{ "name": "Debit Card", "description": "Debit card payment", "weight": 20 },
|
|
6
|
+
{ "name": "Gift Card", "description": "Gift card payment", "weight": 5 }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"categories": [
|
|
3
|
+
{ "name": "Hot Drinks", "sort_order": 1, "description": "Coffee, tea, and hot beverages" },
|
|
4
|
+
{ "name": "Cold Drinks", "sort_order": 2, "description": "Iced and cold beverages" },
|
|
5
|
+
{ "name": "Pastries", "sort_order": 3, "description": "Fresh baked pastries" },
|
|
6
|
+
{ "name": "Sandwiches", "sort_order": 4, "description": "Fresh sandwiches and wraps" },
|
|
7
|
+
{ "name": "Cakes", "sort_order": 5, "description": "Cakes and slices" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"items": [
|
|
3
|
+
{ "name": "Espresso", "price": 3.50, "category": "Hot Drinks", "sku": "CAFE-HOT-001" },
|
|
4
|
+
{ "name": "Cappuccino", "price": 4.50, "category": "Hot Drinks", "sku": "CAFE-HOT-002" },
|
|
5
|
+
{ "name": "Latte", "price": 4.75, "category": "Hot Drinks", "sku": "CAFE-HOT-003" },
|
|
6
|
+
{ "name": "Hot Chocolate", "price": 4.25, "category": "Hot Drinks", "sku": "CAFE-HOT-004" },
|
|
7
|
+
{ "name": "English Breakfast Tea", "price": 3.25, "category": "Hot Drinks", "sku": "CAFE-HOT-005" },
|
|
8
|
+
{ "name": "Iced Latte", "price": 5.25, "category": "Cold Drinks", "sku": "CAFE-CLD-001" },
|
|
9
|
+
{ "name": "Iced Americano", "price": 4.75, "category": "Cold Drinks", "sku": "CAFE-CLD-002" },
|
|
10
|
+
{ "name": "Smoothie", "price": 5.99, "category": "Cold Drinks", "sku": "CAFE-CLD-003" },
|
|
11
|
+
{ "name": "Fresh Orange Juice", "price": 4.50, "category": "Cold Drinks", "sku": "CAFE-CLD-004" },
|
|
12
|
+
{ "name": "Croissant", "price": 3.50, "category": "Pastries", "sku": "CAFE-PAS-001" },
|
|
13
|
+
{ "name": "Pain au Chocolat", "price": 3.75, "category": "Pastries", "sku": "CAFE-PAS-002" },
|
|
14
|
+
{ "name": "Blueberry Muffin", "price": 3.99, "category": "Pastries", "sku": "CAFE-PAS-003" },
|
|
15
|
+
{ "name": "Cinnamon Roll", "price": 4.25, "category": "Pastries", "sku": "CAFE-PAS-004" },
|
|
16
|
+
{ "name": "Scone", "price": 3.25, "category": "Pastries", "sku": "CAFE-PAS-005" },
|
|
17
|
+
{ "name": "Club Sandwich", "price": 8.99, "category": "Sandwiches", "sku": "CAFE-SAN-001" },
|
|
18
|
+
{ "name": "BLT", "price": 7.99, "category": "Sandwiches", "sku": "CAFE-SAN-002" },
|
|
19
|
+
{ "name": "Avocado Toast", "price": 9.50, "category": "Sandwiches", "sku": "CAFE-SAN-003" },
|
|
20
|
+
{ "name": "Panini", "price": 8.50, "category": "Sandwiches", "sku": "CAFE-SAN-004" },
|
|
21
|
+
{ "name": "Chicken Wrap", "price": 8.99, "category": "Sandwiches", "sku": "CAFE-SAN-005" },
|
|
22
|
+
{ "name": "Carrot Cake", "price": 5.50, "category": "Cakes", "sku": "CAFE-CAK-001" },
|
|
23
|
+
{ "name": "Victoria Sponge", "price": 5.25, "category": "Cakes", "sku": "CAFE-CAK-002" },
|
|
24
|
+
{ "name": "Brownie", "price": 4.50, "category": "Cakes", "sku": "CAFE-CAK-003" },
|
|
25
|
+
{ "name": "Lemon Drizzle", "price": 4.99, "category": "Cakes", "sku": "CAFE-CAK-004" },
|
|
26
|
+
{ "name": "Red Velvet Slice", "price": 5.75, "category": "Cakes", "sku": "CAFE-CAK-005" }
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tenders": [
|
|
3
|
+
{ "name": "Cash", "description": "Cash payment", "weight": 30 },
|
|
4
|
+
{ "name": "Credit Card", "description": "Credit card payment", "weight": 50 },
|
|
5
|
+
{ "name": "Debit Card", "description": "Debit card payment", "weight": 15 },
|
|
6
|
+
{ "name": "Gift Card", "description": "Gift card payment", "weight": 5 }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"categories": [
|
|
3
|
+
{ "name": "Appetizers", "sort_order": 1, "description": "Starters and small plates" },
|
|
4
|
+
{ "name": "Entrees", "sort_order": 2, "description": "Main course dishes" },
|
|
5
|
+
{ "name": "Sides", "sort_order": 3, "description": "Side dishes" },
|
|
6
|
+
{ "name": "Drinks", "sort_order": 4, "description": "Beverages" },
|
|
7
|
+
{ "name": "Desserts", "sort_order": 5, "description": "Sweets and treats" }
|
|
8
|
+
]
|
|
9
|
+
}
|