revel_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 +21 -0
- data/README.md +305 -0
- data/bin/simulate +187 -0
- data/lib/revel_sandbox_simulator/configuration.rb +109 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/restaurant/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/restaurant/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/restaurant/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/retail_general/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/retail_general/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/retail_general/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/database.rb +92 -0
- data/lib/revel_sandbox_simulator/db/factories/api_requests.rb +15 -0
- data/lib/revel_sandbox_simulator/db/factories/business_types.rb +34 -0
- data/lib/revel_sandbox_simulator/db/factories/categories.rb +10 -0
- data/lib/revel_sandbox_simulator/db/factories/items.rb +12 -0
- data/lib/revel_sandbox_simulator/db/factories/simulated_orders.rb +25 -0
- data/lib/revel_sandbox_simulator/db/factories/simulated_payments.rb +14 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000001_enable_pgcrypto.rb +7 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000002_create_business_types.rb +16 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000003_create_categories.rb +16 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000004_create_items.rb +19 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000005_create_simulated_orders.rb +27 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000006_create_simulated_payments.rb +22 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000007_create_api_requests.rb +22 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000008_create_daily_summaries.rb +21 -0
- data/lib/revel_sandbox_simulator/generators/data_loader.rb +80 -0
- data/lib/revel_sandbox_simulator/generators/entity_generator.rb +61 -0
- data/lib/revel_sandbox_simulator/generators/order_generator.rb +260 -0
- data/lib/revel_sandbox_simulator/models/api_request.rb +11 -0
- data/lib/revel_sandbox_simulator/models/business_type.rb +14 -0
- data/lib/revel_sandbox_simulator/models/category.rb +15 -0
- data/lib/revel_sandbox_simulator/models/daily_summary.rb +53 -0
- data/lib/revel_sandbox_simulator/models/item.rb +18 -0
- data/lib/revel_sandbox_simulator/models/record.rb +9 -0
- data/lib/revel_sandbox_simulator/models/simulated_order.rb +20 -0
- data/lib/revel_sandbox_simulator/models/simulated_payment.rb +16 -0
- data/lib/revel_sandbox_simulator/seeder.rb +115 -0
- data/lib/revel_sandbox_simulator/services/base_service.rb +143 -0
- data/lib/revel_sandbox_simulator/services/revel/customer_service.rb +53 -0
- data/lib/revel_sandbox_simulator/services/revel/establishment_service.rb +19 -0
- data/lib/revel_sandbox_simulator/services/revel/order_service.rb +77 -0
- data/lib/revel_sandbox_simulator/services/revel/payment_service.rb +52 -0
- data/lib/revel_sandbox_simulator/services/revel/product_service.rb +87 -0
- data/lib/revel_sandbox_simulator/services/revel/services_manager.rb +45 -0
- data/lib/revel_sandbox_simulator/version.rb +5 -0
- data/lib/revel_sandbox_simulator.rb +38 -0
- metadata +335 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 868687b6e6e899f5d9a2cc31cf0b8bcf2b11b90460478573d5d11d5deda5de8d
|
|
4
|
+
data.tar.gz: 61d95323c8945f5864068ac2dd2296c6cc1a08053ca76c441df3d441e5c46950
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e5cf9325b9f942ef03665a99d112e75f885c0fec937943423874eaad9963d35519fb64df1aab5001e64506458892dda5266f7d7d2dfb0691fb5bdbb6a3590c32
|
|
7
|
+
data.tar.gz: 872f44345caf18b98fccbfd9cd33c296917987441510d5c96c9ea0c1a18be8a1eac35fcb0444f8a56c2a25795b35c06ff6350f6e8f7e7c87a8f09bb7df5f704b
|
data/LICENSE
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,305 @@
|
|
|
1
|
+
# Revel Sandbox Simulator
|
|
2
|
+
|
|
3
|
+
A Ruby gem for simulating Point of Sale operations against the **Revel Systems API**. Generates realistic restaurant, cafe, bar, and retail orders with payments and transaction data for testing integrations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **4 Business Types**: Restaurant, Cafe/Bakery, Bar/Nightclub, Retail General — each with 5 categories and 25 items
|
|
8
|
+
- **100 Menu/Product Items**: Spread across 20 categories with realistic pricing
|
|
9
|
+
- **Revel API Integration**: Tastypie-style REST with `API-AUTHENTICATION` header
|
|
10
|
+
- **Meal Period Simulation**: Orders distributed across breakfast, lunch, happy hour, dinner, and late night
|
|
11
|
+
- **Order Types**: Dine-in (`dining_option: 0`), Takeout (`1`), and Delivery (`2`)
|
|
12
|
+
- **Dynamic Order Volume**: 40–120 orders/day based on day of week
|
|
13
|
+
- **Tips & Taxes**: Variable tip rates by dining option (15–25% dine-in, 5–15% takeout, 10–20% delivery)
|
|
14
|
+
- **Discounts**: 10–20% applied probabilistically (8% chance per order)
|
|
15
|
+
- **Multiple Payment Methods**: Cash, Credit Card, Debit Card, Gift Card, Check, Mobile Pay — weighted selection
|
|
16
|
+
- **PostgreSQL Audit Trail**: Track all simulated orders, payments, and API requests
|
|
17
|
+
- **Daily Summaries**: Automated aggregation of revenue, tax, tips, and discounts by meal period and tender
|
|
18
|
+
- **Multi-Merchant Support**: Configure multiple API credentials via `.env.json`
|
|
19
|
+
- **Database Seeding**: Idempotent FactoryBot-based seeder for all 4 business types
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
Add to your Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "revel_sandbox_simulator"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install directly:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
gem install revel_sandbox_simulator
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
### Getting API Credentials
|
|
44
|
+
|
|
45
|
+
1. Log in to your **Revel Systems Backoffice**
|
|
46
|
+
2. Navigate to Settings and generate an **API Key** and **API Secret**
|
|
47
|
+
3. The auth header is `API-AUTHENTICATION: api_key:api_secret` — the simulator handles this automatically
|
|
48
|
+
|
|
49
|
+
### Multi-Merchant Setup (Recommended)
|
|
50
|
+
|
|
51
|
+
Create a `.env.json` file:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"DATABASE_URL": "postgres://localhost:5432/revel_simulator_development",
|
|
56
|
+
"merchants": [
|
|
57
|
+
{
|
|
58
|
+
"REVEL_API_KEY": "your-api-key",
|
|
59
|
+
"REVEL_API_SECRET": "your-api-secret",
|
|
60
|
+
"REVEL_ESTABLISHMENT_ID": "1",
|
|
61
|
+
"REVEL_ESTABLISHMENT_NAME": "Main Restaurant"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"REVEL_API_KEY": "second-key",
|
|
65
|
+
"REVEL_API_SECRET": "second-secret",
|
|
66
|
+
"REVEL_ESTABLISHMENT_ID": "2",
|
|
67
|
+
"REVEL_ESTABLISHMENT_NAME": "Cafe Location"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Single Merchant Setup
|
|
74
|
+
|
|
75
|
+
Use a `.env` file:
|
|
76
|
+
|
|
77
|
+
```env
|
|
78
|
+
REVEL_API_KEY=your-api-key
|
|
79
|
+
REVEL_API_SECRET=your-api-secret
|
|
80
|
+
REVEL_ESTABLISHMENT_ID=1
|
|
81
|
+
REVEL_BASE_URL=https://sandbox.revelup.com
|
|
82
|
+
LOG_LEVEL=INFO
|
|
83
|
+
TAX_RATE=8.25
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Database Setup
|
|
87
|
+
|
|
88
|
+
The simulator uses PostgreSQL to persist audit data (simulated orders, payments, API requests, daily summaries):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
./bin/simulate db create
|
|
92
|
+
./bin/simulate db migrate
|
|
93
|
+
./bin/simulate db seed
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
### Quick Start
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Full setup + order generation in one command
|
|
102
|
+
./bin/simulate full
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Commands
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Show version
|
|
109
|
+
./bin/simulate version
|
|
110
|
+
|
|
111
|
+
# List configured merchants
|
|
112
|
+
./bin/simulate merchants
|
|
113
|
+
|
|
114
|
+
# Set up POS entities (categories, products, customers)
|
|
115
|
+
./bin/simulate setup
|
|
116
|
+
|
|
117
|
+
# Generate orders for today (random count based on day of week)
|
|
118
|
+
./bin/simulate generate
|
|
119
|
+
|
|
120
|
+
# Generate a specific number of orders
|
|
121
|
+
./bin/simulate generate -n 25
|
|
122
|
+
|
|
123
|
+
# Generate a realistic full day of operations
|
|
124
|
+
./bin/simulate day
|
|
125
|
+
|
|
126
|
+
# Busy day (2x normal volume)
|
|
127
|
+
./bin/simulate day -x 2.0
|
|
128
|
+
|
|
129
|
+
# Generate a lunch or dinner rush
|
|
130
|
+
./bin/simulate rush -p lunch -n 20
|
|
131
|
+
./bin/simulate rush -p dinner -n 30
|
|
132
|
+
|
|
133
|
+
# Check current entity counts
|
|
134
|
+
./bin/simulate status
|
|
135
|
+
|
|
136
|
+
# Use a specific merchant by index
|
|
137
|
+
./bin/simulate setup -i 0
|
|
138
|
+
./bin/simulate generate -i 1 -n 20
|
|
139
|
+
|
|
140
|
+
# List available business types
|
|
141
|
+
./bin/simulate business_types
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Database Management
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
./bin/simulate db create # Create PostgreSQL database
|
|
148
|
+
./bin/simulate db migrate # Run pending migrations
|
|
149
|
+
./bin/simulate db seed # Seed business types, categories, items
|
|
150
|
+
./bin/simulate db reset # Drop, create, migrate, and seed
|
|
151
|
+
|
|
152
|
+
# Reporting
|
|
153
|
+
./bin/simulate summary # Show daily summary
|
|
154
|
+
./bin/simulate orders # List recent orders
|
|
155
|
+
./bin/simulate audit # Show recent API requests
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Business Types
|
|
159
|
+
|
|
160
|
+
| Type | Categories | Items | Description |
|
|
161
|
+
|------|-----------|-------|-------------|
|
|
162
|
+
| `restaurant` | 5 | 25 | Full-service casual dining |
|
|
163
|
+
| `cafe_bakery` | 5 | 25 | Coffee shop with pastries and light fare |
|
|
164
|
+
| `bar_nightclub` | 5 | 25 | Craft cocktails, draft beer, late-night bites |
|
|
165
|
+
| `retail_general` | 5 | 25 | Electronics, clothing, home goods |
|
|
166
|
+
|
|
167
|
+
## Revel Systems API Endpoints
|
|
168
|
+
|
|
169
|
+
| Endpoint | Operations |
|
|
170
|
+
|----------|-----------|
|
|
171
|
+
| `/products/ProductCategory/` | CRUD for product categories |
|
|
172
|
+
| `/resources/Product/` | CRUD for products/items |
|
|
173
|
+
| `/resources/Order/` | Create and fetch orders |
|
|
174
|
+
| `/resources/OrderItem/` | Create order line items |
|
|
175
|
+
| `/resources/Payment/` | Create payments and refunds |
|
|
176
|
+
| `/resources/Customer/` | CRUD for customers |
|
|
177
|
+
| `/enterprise/Establishment/` | Fetch establishment info |
|
|
178
|
+
|
|
179
|
+
### Key Differences from Clover/Square/Epos Now
|
|
180
|
+
|
|
181
|
+
| Feature | Revel | Clover | Square | Epos Now |
|
|
182
|
+
|---------|-------|--------|--------|----------|
|
|
183
|
+
| Auth | API-AUTHENTICATION header | OAuth2 Bearer | OAuth2 Bearer | Basic Auth |
|
|
184
|
+
| Response format | Tastypie (`meta`+`objects`) | Direct JSON | `data` envelope | Direct JSON |
|
|
185
|
+
| Pagination | offset/limit (Django) | offset/limit | Cursor-based | page (200/page) |
|
|
186
|
+
| DELETE | Blocked on most resources | Supported | Supported | Body `[{Id: int}]` |
|
|
187
|
+
| IDs | Integer | UUID-like string | UUID-like string | Integer |
|
|
188
|
+
| Orders | Separate Order + OrderItem | Separate endpoints | Single order | Single transaction |
|
|
189
|
+
|
|
190
|
+
## Order Patterns
|
|
191
|
+
|
|
192
|
+
### Daily Volume
|
|
193
|
+
|
|
194
|
+
| Day | Min Orders | Max Orders |
|
|
195
|
+
|-----|-----------|-----------|
|
|
196
|
+
| Weekday | 40 | 60 |
|
|
197
|
+
| Friday | 70 | 100 |
|
|
198
|
+
| Saturday | 80 | 120 |
|
|
199
|
+
| Sunday | 50 | 80 |
|
|
200
|
+
|
|
201
|
+
### Meal Periods
|
|
202
|
+
|
|
203
|
+
| Period | Weight | Items | Typical Total |
|
|
204
|
+
|--------|--------|-------|--------------|
|
|
205
|
+
| Breakfast | 15% | 1–3 | $8–$20 |
|
|
206
|
+
| Lunch | 30% | 2–4 | $12–$35 |
|
|
207
|
+
| Happy Hour | 10% | 2–4 | $10–$25 |
|
|
208
|
+
| Dinner | 35% | 3–6 | $20–$60 |
|
|
209
|
+
| Late Night | 10% | 1–3 | $8–$25 |
|
|
210
|
+
|
|
211
|
+
## Audit Trail & Persistence
|
|
212
|
+
|
|
213
|
+
### Models
|
|
214
|
+
|
|
215
|
+
| Model | Purpose |
|
|
216
|
+
|-------|---------|
|
|
217
|
+
| `BusinessType` | 4 business types with category/item associations |
|
|
218
|
+
| `Category` | 20 categories linked to business types |
|
|
219
|
+
| `Item` | 100 items with SKUs, pricing, and category assignments |
|
|
220
|
+
| `SimulatedOrder` | Every generated order with meal period, dining option, amounts |
|
|
221
|
+
| `SimulatedPayment` | Payment records with tender type and amounts |
|
|
222
|
+
| `ApiRequest` | Full audit log of every HTTP call (method, URL, status, duration) |
|
|
223
|
+
| `DailySummary` | Daily aggregation of revenue, tax, tips, discounts by period/tender |
|
|
224
|
+
|
|
225
|
+
## Development
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Install dependencies
|
|
229
|
+
bundle install
|
|
230
|
+
|
|
231
|
+
# Run all tests (444 examples)
|
|
232
|
+
bundle exec rspec
|
|
233
|
+
|
|
234
|
+
# Run with coverage report (99.7% line, 92.62% branch)
|
|
235
|
+
bundle exec rspec
|
|
236
|
+
|
|
237
|
+
# Run linter (0 offenses required)
|
|
238
|
+
bundle exec rubocop
|
|
239
|
+
|
|
240
|
+
# Run specific test groups
|
|
241
|
+
bundle exec rspec spec/services/
|
|
242
|
+
bundle exec rspec spec/generators/
|
|
243
|
+
bundle exec rspec spec/models/
|
|
244
|
+
|
|
245
|
+
# Build the gem
|
|
246
|
+
gem build revel_sandbox_simulator.gemspec
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Test Coverage
|
|
250
|
+
|
|
251
|
+
- **444 examples, 0 failures**
|
|
252
|
+
- **99.7% line coverage** (674/676 lines)
|
|
253
|
+
- **92.62% branch coverage** (113/122 branches)
|
|
254
|
+
- **RuboCop: 0 offenses** (62 files)
|
|
255
|
+
|
|
256
|
+
## Ruby API
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
require "revel_sandbox_simulator"
|
|
260
|
+
|
|
261
|
+
# Configure
|
|
262
|
+
RevelSandboxSimulator.configure do |config|
|
|
263
|
+
config.api_key = "your-api-key"
|
|
264
|
+
config.api_secret = "your-api-secret"
|
|
265
|
+
config.establishment_id = "1"
|
|
266
|
+
config.base_url = "https://sandbox.revelup.com"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Use services directly
|
|
270
|
+
manager = RevelSandboxSimulator::Services::Revel::ServicesManager.new
|
|
271
|
+
|
|
272
|
+
# Products
|
|
273
|
+
categories = manager.product.fetch_categories
|
|
274
|
+
products = manager.product.fetch_products
|
|
275
|
+
manager.product.create_category(name: "Specials", sort_order: 99)
|
|
276
|
+
manager.product.create_product(name: "Daily Special", price: 1499, category_id: 1)
|
|
277
|
+
|
|
278
|
+
# Orders
|
|
279
|
+
result = manager.order.create_order(
|
|
280
|
+
items: [
|
|
281
|
+
{ product_id: 1, quantity: 2, unit_price: 999 },
|
|
282
|
+
{ product_id: 3, quantity: 1 }
|
|
283
|
+
],
|
|
284
|
+
dining_option: :dine_in
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Payments
|
|
288
|
+
manager.payment.create_payment(
|
|
289
|
+
order_id: result["id"],
|
|
290
|
+
amount: 2997,
|
|
291
|
+
payment_type: :credit_card,
|
|
292
|
+
tip_amount: 450
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Customers
|
|
296
|
+
manager.customer.ensure_customers(count: 20)
|
|
297
|
+
|
|
298
|
+
# Generators
|
|
299
|
+
generator = RevelSandboxSimulator::Generators::OrderGenerator.new(refund_percentage: 5)
|
|
300
|
+
orders = generator.generate_today(count: 25)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
[MIT License](LICENSE)
|
data/bin/simulate
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/revel_sandbox_simulator"
|
|
5
|
+
require "thor"
|
|
6
|
+
|
|
7
|
+
module RevelSandboxSimulator
|
|
8
|
+
class CLI < Thor
|
|
9
|
+
class_option :verbose, type: :boolean, aliases: "-v", desc: "Enable debug logging"
|
|
10
|
+
class_option :merchant_index, type: :numeric, aliases: "-i", desc: "Merchant index from .env.json"
|
|
11
|
+
|
|
12
|
+
desc "version", "Show version"
|
|
13
|
+
def version
|
|
14
|
+
puts "revel_sandbox_simulator v#{VERSION}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "merchants", "List configured merchants"
|
|
18
|
+
def merchants
|
|
19
|
+
apply_options
|
|
20
|
+
merchants = RevelSandboxSimulator.configuration.available_merchants
|
|
21
|
+
if merchants.empty?
|
|
22
|
+
puts "No merchants configured. Create a .env.json file."
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
merchants.each_with_index do |m, i|
|
|
26
|
+
puts " [#{i}] #{m['REVEL_ESTABLISHMENT_NAME'] || 'Unnamed'} (#{m['REVEL_ESTABLISHMENT_ID']})"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc "business_types", "List available business types"
|
|
31
|
+
def business_types
|
|
32
|
+
Seeder::SEED_MAP.each do |key, data|
|
|
33
|
+
cat_count = data[:categories].size
|
|
34
|
+
item_count = data[:categories].values.sum(&:size)
|
|
35
|
+
puts " #{key} (#{data[:industry]}) — #{cat_count} categories, #{item_count} items"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "setup", "Set up POS entities (categories, products, customers)"
|
|
40
|
+
option :business_type, type: :string, aliases: "-b", desc: "Business type (restaurant, cafe_bakery, etc.)"
|
|
41
|
+
def setup
|
|
42
|
+
apply_options
|
|
43
|
+
config = RevelSandboxSimulator.configuration
|
|
44
|
+
config.business_type = options[:business_type].to_sym if options[:business_type]
|
|
45
|
+
generator = Generators::EntityGenerator.new(config: config)
|
|
46
|
+
generator.setup_all
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
desc "generate", "Generate orders for today"
|
|
50
|
+
option :count, type: :numeric, aliases: "-n", desc: "Number of orders"
|
|
51
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5, desc: "Refund percentage"
|
|
52
|
+
def generate
|
|
53
|
+
apply_options
|
|
54
|
+
generator = Generators::OrderGenerator.new(
|
|
55
|
+
config: RevelSandboxSimulator.configuration,
|
|
56
|
+
refund_percentage: options[:refund_percentage]
|
|
57
|
+
)
|
|
58
|
+
generator.generate_today(count: options[:count])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc "day", "Generate a realistic full day of operations"
|
|
62
|
+
option :multiplier, type: :numeric, aliases: "-x", default: 1.0, desc: "Volume multiplier"
|
|
63
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5, desc: "Refund percentage"
|
|
64
|
+
def day
|
|
65
|
+
apply_options
|
|
66
|
+
generator = Generators::OrderGenerator.new(
|
|
67
|
+
config: RevelSandboxSimulator.configuration,
|
|
68
|
+
refund_percentage: options[:refund_percentage]
|
|
69
|
+
)
|
|
70
|
+
generator.generate_realistic_day(multiplier: options[:multiplier])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
desc "rush", "Generate a meal period rush"
|
|
74
|
+
option :period, type: :string, aliases: "-p", default: "lunch", desc: "Meal period"
|
|
75
|
+
option :count, type: :numeric, aliases: "-n", default: 15, desc: "Number of orders"
|
|
76
|
+
def rush
|
|
77
|
+
apply_options
|
|
78
|
+
generator = Generators::OrderGenerator.new(config: RevelSandboxSimulator.configuration)
|
|
79
|
+
generator.generate_rush(period: options[:period], count: options[:count])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
desc "full", "Setup + generate in one command"
|
|
83
|
+
option :business_type, type: :string, aliases: "-b", default: "restaurant"
|
|
84
|
+
option :multiplier, type: :numeric, aliases: "-x", default: 1.0
|
|
85
|
+
option :refund_percentage, type: :numeric, aliases: "-r", default: 5
|
|
86
|
+
def full
|
|
87
|
+
apply_options
|
|
88
|
+
config = RevelSandboxSimulator.configuration
|
|
89
|
+
config.business_type = options[:business_type].to_sym
|
|
90
|
+
|
|
91
|
+
entity_gen = Generators::EntityGenerator.new(config: config)
|
|
92
|
+
entity_gen.setup_all
|
|
93
|
+
|
|
94
|
+
order_gen = Generators::OrderGenerator.new(config: config, refund_percentage: options[:refund_percentage])
|
|
95
|
+
order_gen.generate_realistic_day(multiplier: options[:multiplier])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "status", "Show entity counts"
|
|
99
|
+
def status
|
|
100
|
+
apply_options
|
|
101
|
+
puts "Revel Sandbox Simulator Status"
|
|
102
|
+
puts "=" * 40
|
|
103
|
+
if Database.connected?
|
|
104
|
+
puts " Business Types: #{Models::BusinessType.count}"
|
|
105
|
+
puts " Categories: #{Models::Category.count}"
|
|
106
|
+
puts " Items: #{Models::Item.count}"
|
|
107
|
+
puts " Orders: #{Models::SimulatedOrder.count}"
|
|
108
|
+
puts " Payments: #{Models::SimulatedPayment.count}"
|
|
109
|
+
puts " API Requests: #{Models::ApiRequest.count}"
|
|
110
|
+
else
|
|
111
|
+
puts " Database not connected"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
desc "summary", "Show daily summary"
|
|
116
|
+
option :date, type: :string, aliases: "-d", desc: "Date (YYYY-MM-DD)"
|
|
117
|
+
def summary
|
|
118
|
+
apply_options
|
|
119
|
+
date = options[:date] ? Date.parse(options[:date]) : Date.today
|
|
120
|
+
summary = Models::DailySummary.generate_for!(date)
|
|
121
|
+
puts "Daily Summary for #{date}"
|
|
122
|
+
puts " Orders: #{summary.order_count}"
|
|
123
|
+
puts " Revenue: $#{format('%.2f', summary.total_revenue / 100.0)}"
|
|
124
|
+
puts " Tax: $#{format('%.2f', summary.total_tax / 100.0)}"
|
|
125
|
+
puts " Tips: $#{format('%.2f', summary.total_tips / 100.0)}"
|
|
126
|
+
puts " Discounts: $#{format('%.2f', summary.total_discounts / 100.0)}"
|
|
127
|
+
puts " Refunds: #{summary.refund_count}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
desc "orders", "List recent orders"
|
|
131
|
+
option :limit, type: :numeric, aliases: "-l", default: 10
|
|
132
|
+
def orders
|
|
133
|
+
apply_options
|
|
134
|
+
Models::SimulatedOrder.order(created_at: :desc).limit(options[:limit]).each do |order|
|
|
135
|
+
puts " #{order.revel_order_id} | #{order.status} | #{order.meal_period} | " \
|
|
136
|
+
"$#{format('%.2f', order.total / 100.0)} | #{order.dining_option}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
desc "audit", "Show recent API requests"
|
|
141
|
+
option :limit, type: :numeric, aliases: "-l", default: 10
|
|
142
|
+
def audit
|
|
143
|
+
apply_options
|
|
144
|
+
Models::ApiRequest.recent.limit(options[:limit]).each do |req|
|
|
145
|
+
status_indicator = req.error_message ? "ERR" : "OK"
|
|
146
|
+
puts " #{req.http_method} #{req.url} [#{req.response_status}] #{status_indicator} (#{req.duration_ms}ms)"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
desc "db SUBCOMMAND", "Database management"
|
|
151
|
+
def db(subcommand = "help")
|
|
152
|
+
apply_options
|
|
153
|
+
case subcommand
|
|
154
|
+
when "create"
|
|
155
|
+
Database.create!
|
|
156
|
+
when "migrate"
|
|
157
|
+
Database.connect!
|
|
158
|
+
Database.migrate!
|
|
159
|
+
when "seed"
|
|
160
|
+
Database.connect!
|
|
161
|
+
Database.seed!(business_type: options[:business_type])
|
|
162
|
+
when "reset"
|
|
163
|
+
Database.drop!
|
|
164
|
+
Database.create!
|
|
165
|
+
Database.connect!
|
|
166
|
+
Database.migrate!
|
|
167
|
+
Database.seed!
|
|
168
|
+
else
|
|
169
|
+
puts "Usage: simulate db {create|migrate|seed|reset}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def apply_options
|
|
176
|
+
config = RevelSandboxSimulator.configuration
|
|
177
|
+
config.log_level = "DEBUG" if options[:verbose]
|
|
178
|
+
config.load_merchant(index: options[:merchant_index]) if options[:merchant_index]
|
|
179
|
+
|
|
180
|
+
Database.connect!(config.database_url) if config.database_url
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
RevelSandboxSimulator.logger.debug("DB connection skipped: #{e.message}")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
RevelSandboxSimulator::CLI.start(ARGV)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "tzinfo"
|
|
6
|
+
|
|
7
|
+
module RevelSandboxSimulator
|
|
8
|
+
class Configuration
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
base_url: "https://sandbox.revelup.com",
|
|
11
|
+
business_type: :restaurant,
|
|
12
|
+
tax_rate: 8.25,
|
|
13
|
+
log_level: "INFO",
|
|
14
|
+
timezone: "America/New_York"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_accessor :api_key, :api_secret, :establishment_id, :base_url,
|
|
18
|
+
:business_type, :tax_rate, :database_url, :log_level, :timezone
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
DEFAULTS.each { |key, value| send(:"#{key}=", value) }
|
|
22
|
+
load_from_env
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def auth_header
|
|
26
|
+
validate!
|
|
27
|
+
"#{api_key}:#{api_secret}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate!
|
|
31
|
+
raise ConfigurationError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
32
|
+
raise ConfigurationError, "api_secret is required" if api_secret.nil? || api_secret.empty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def logger
|
|
36
|
+
@logger ||= build_logger
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_writer :logger
|
|
40
|
+
|
|
41
|
+
def load_merchant(name: nil, index: nil)
|
|
42
|
+
merchants = available_merchants
|
|
43
|
+
raise ConfigurationError, "No merchants configured in .env.json" if merchants.empty?
|
|
44
|
+
|
|
45
|
+
merchant = if name
|
|
46
|
+
merchants.find { |m| m["REVEL_ESTABLISHMENT_NAME"] == name }
|
|
47
|
+
elsif index
|
|
48
|
+
merchants[index]
|
|
49
|
+
else
|
|
50
|
+
merchants.first
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
raise ConfigurationError, "Merchant not found" unless merchant
|
|
54
|
+
|
|
55
|
+
apply_merchant(merchant)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def available_merchants
|
|
59
|
+
return @available_merchants if defined?(@available_merchants)
|
|
60
|
+
|
|
61
|
+
json_path = File.join(Dir.pwd, ".env.json")
|
|
62
|
+
return @available_merchants = [] unless File.exist?(json_path)
|
|
63
|
+
|
|
64
|
+
data = JSON.parse(File.read(json_path))
|
|
65
|
+
self.database_url = data["DATABASE_URL"] if data["DATABASE_URL"]
|
|
66
|
+
@available_merchants = data.fetch("merchants", [])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def merchant_time_now
|
|
70
|
+
tz = TZInfo::Timezone.get(timezone)
|
|
71
|
+
tz.now
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def merchant_date_today
|
|
75
|
+
merchant_time_now.to_date
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def load_from_env
|
|
81
|
+
self.api_key = ENV.fetch("REVEL_API_KEY", nil)
|
|
82
|
+
self.api_secret = ENV.fetch("REVEL_API_SECRET", nil)
|
|
83
|
+
self.establishment_id = ENV.fetch("REVEL_ESTABLISHMENT_ID", nil)
|
|
84
|
+
self.base_url = ENV.fetch("REVEL_BASE_URL", nil) || DEFAULTS[:base_url]
|
|
85
|
+
self.database_url = ENV.fetch("DATABASE_URL", nil)
|
|
86
|
+
self.business_type = ENV.fetch("BUSINESS_TYPE", nil)&.to_sym || DEFAULTS[:business_type]
|
|
87
|
+
self.tax_rate = ENV.fetch("TAX_RATE", nil)&.to_f || DEFAULTS[:tax_rate]
|
|
88
|
+
self.log_level = ENV.fetch("LOG_LEVEL", nil) || DEFAULTS[:log_level]
|
|
89
|
+
self.timezone = ENV.fetch("TIMEZONE", nil) || DEFAULTS[:timezone]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_merchant(merchant)
|
|
93
|
+
self.api_key = merchant["REVEL_API_KEY"]
|
|
94
|
+
self.api_secret = merchant["REVEL_API_SECRET"]
|
|
95
|
+
self.establishment_id = merchant["REVEL_ESTABLISHMENT_ID"]
|
|
96
|
+
self.base_url = merchant["REVEL_BASE_URL"] if merchant["REVEL_BASE_URL"]
|
|
97
|
+
self.timezone = merchant["TIMEZONE"] if merchant["TIMEZONE"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_logger
|
|
101
|
+
logger = Logger.new($stdout)
|
|
102
|
+
logger.level = Logger.const_get(log_level.upcase)
|
|
103
|
+
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
104
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- #{msg}\n"
|
|
105
|
+
end
|
|
106
|
+
logger
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "Draft Beer", "sort_order": 0, "description": "Beers on tap" },
|
|
3
|
+
{ "name": "Cocktails", "sort_order": 1, "description": "Mixed drinks and cocktails" },
|
|
4
|
+
{ "name": "Wine", "sort_order": 2, "description": "Wine by the glass" },
|
|
5
|
+
{ "name": "Spirits", "sort_order": 3, "description": "Shots and neat pours" },
|
|
6
|
+
{ "name": "Bar Food", "sort_order": 4, "description": "Late night bites" }
|
|
7
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "IPA Pint", "price": 699, "sku": "BAR-IPAPNT", "category": "Draft Beer" },
|
|
3
|
+
{ "name": "Lager Pint", "price": 599, "sku": "BAR-LGRPNT", "category": "Draft Beer" },
|
|
4
|
+
{ "name": "Stout Pint", "price": 749, "sku": "BAR-STTPNT", "category": "Draft Beer" },
|
|
5
|
+
{ "name": "Wheat Beer", "price": 649, "sku": "BAR-WHTBER", "category": "Draft Beer" },
|
|
6
|
+
{ "name": "Pale Ale", "price": 699, "sku": "BAR-PLEALE", "category": "Draft Beer" },
|
|
7
|
+
{ "name": "Margarita", "price": 1299, "sku": "BAR-MARGTA", "category": "Cocktails" },
|
|
8
|
+
{ "name": "Old Fashioned", "price": 1399, "sku": "BAR-OLDFSH", "category": "Cocktails" },
|
|
9
|
+
{ "name": "Mojito", "price": 1199, "sku": "BAR-MOJITO", "category": "Cocktails" },
|
|
10
|
+
{ "name": "Cosmopolitan", "price": 1299, "sku": "BAR-COSMOP", "category": "Cocktails" },
|
|
11
|
+
{ "name": "Manhattan", "price": 1399, "sku": "BAR-MNHTTN", "category": "Cocktails" },
|
|
12
|
+
{ "name": "House Red Wine", "price": 899, "sku": "BAR-HRDWIN", "category": "Wine" },
|
|
13
|
+
{ "name": "House White Wine", "price": 899, "sku": "BAR-HWTWIN", "category": "Wine" },
|
|
14
|
+
{ "name": "Rose", "price": 999, "sku": "BAR-ROSE", "category": "Wine" },
|
|
15
|
+
{ "name": "Prosecco", "price": 1099, "sku": "BAR-PRSECO", "category": "Wine" },
|
|
16
|
+
{ "name": "Pinot Noir", "price": 1199, "sku": "BAR-PNTNOR", "category": "Wine" },
|
|
17
|
+
{ "name": "Whiskey Shot", "price": 799, "sku": "BAR-WHKSHT", "category": "Spirits" },
|
|
18
|
+
{ "name": "Vodka Shot", "price": 699, "sku": "BAR-VDKSHT", "category": "Spirits" },
|
|
19
|
+
{ "name": "Tequila Shot", "price": 799, "sku": "BAR-TQLSHT", "category": "Spirits" },
|
|
20
|
+
{ "name": "Rum Shot", "price": 699, "sku": "BAR-RUMSHT", "category": "Spirits" },
|
|
21
|
+
{ "name": "Gin Shot", "price": 749, "sku": "BAR-GINSHT", "category": "Spirits" },
|
|
22
|
+
{ "name": "Loaded Nachos", "price": 1099, "sku": "BAR-LDNACH", "category": "Bar Food" },
|
|
23
|
+
{ "name": "Chicken Wings", "price": 1299, "sku": "BAR-CHKWNG", "category": "Bar Food" },
|
|
24
|
+
{ "name": "Sliders", "price": 1199, "sku": "BAR-SLIDRS", "category": "Bar Food" },
|
|
25
|
+
{ "name": "Fries Basket", "price": 699, "sku": "BAR-FRYSBK", "category": "Bar Food" },
|
|
26
|
+
{ "name": "Pretzel Bites", "price": 899, "sku": "BAR-PRTZBT", "category": "Bar Food" }
|
|
27
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "Hot Drinks", "sort_order": 0, "description": "Hot coffee and tea" },
|
|
3
|
+
{ "name": "Cold Drinks", "sort_order": 1, "description": "Iced and blended drinks" },
|
|
4
|
+
{ "name": "Pastries", "sort_order": 2, "description": "Fresh baked pastries" },
|
|
5
|
+
{ "name": "Sandwiches", "sort_order": 3, "description": "Made-to-order sandwiches" },
|
|
6
|
+
{ "name": "Cakes", "sort_order": 4, "description": "Cakes and sweet treats" }
|
|
7
|
+
]
|