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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +305 -0
  4. data/bin/simulate +187 -0
  5. data/lib/revel_sandbox_simulator/configuration.rb +109 -0
  6. data/lib/revel_sandbox_simulator/data/bar_nightclub/categories.json +7 -0
  7. data/lib/revel_sandbox_simulator/data/bar_nightclub/items.json +27 -0
  8. data/lib/revel_sandbox_simulator/data/bar_nightclub/tenders.json +7 -0
  9. data/lib/revel_sandbox_simulator/data/cafe_bakery/categories.json +7 -0
  10. data/lib/revel_sandbox_simulator/data/cafe_bakery/items.json +27 -0
  11. data/lib/revel_sandbox_simulator/data/cafe_bakery/tenders.json +7 -0
  12. data/lib/revel_sandbox_simulator/data/restaurant/categories.json +7 -0
  13. data/lib/revel_sandbox_simulator/data/restaurant/items.json +27 -0
  14. data/lib/revel_sandbox_simulator/data/restaurant/tenders.json +7 -0
  15. data/lib/revel_sandbox_simulator/data/retail_general/categories.json +7 -0
  16. data/lib/revel_sandbox_simulator/data/retail_general/items.json +27 -0
  17. data/lib/revel_sandbox_simulator/data/retail_general/tenders.json +7 -0
  18. data/lib/revel_sandbox_simulator/database.rb +92 -0
  19. data/lib/revel_sandbox_simulator/db/factories/api_requests.rb +15 -0
  20. data/lib/revel_sandbox_simulator/db/factories/business_types.rb +34 -0
  21. data/lib/revel_sandbox_simulator/db/factories/categories.rb +10 -0
  22. data/lib/revel_sandbox_simulator/db/factories/items.rb +12 -0
  23. data/lib/revel_sandbox_simulator/db/factories/simulated_orders.rb +25 -0
  24. data/lib/revel_sandbox_simulator/db/factories/simulated_payments.rb +14 -0
  25. data/lib/revel_sandbox_simulator/db/migrate/20260313000001_enable_pgcrypto.rb +7 -0
  26. data/lib/revel_sandbox_simulator/db/migrate/20260313000002_create_business_types.rb +16 -0
  27. data/lib/revel_sandbox_simulator/db/migrate/20260313000003_create_categories.rb +16 -0
  28. data/lib/revel_sandbox_simulator/db/migrate/20260313000004_create_items.rb +19 -0
  29. data/lib/revel_sandbox_simulator/db/migrate/20260313000005_create_simulated_orders.rb +27 -0
  30. data/lib/revel_sandbox_simulator/db/migrate/20260313000006_create_simulated_payments.rb +22 -0
  31. data/lib/revel_sandbox_simulator/db/migrate/20260313000007_create_api_requests.rb +22 -0
  32. data/lib/revel_sandbox_simulator/db/migrate/20260313000008_create_daily_summaries.rb +21 -0
  33. data/lib/revel_sandbox_simulator/generators/data_loader.rb +80 -0
  34. data/lib/revel_sandbox_simulator/generators/entity_generator.rb +61 -0
  35. data/lib/revel_sandbox_simulator/generators/order_generator.rb +260 -0
  36. data/lib/revel_sandbox_simulator/models/api_request.rb +11 -0
  37. data/lib/revel_sandbox_simulator/models/business_type.rb +14 -0
  38. data/lib/revel_sandbox_simulator/models/category.rb +15 -0
  39. data/lib/revel_sandbox_simulator/models/daily_summary.rb +53 -0
  40. data/lib/revel_sandbox_simulator/models/item.rb +18 -0
  41. data/lib/revel_sandbox_simulator/models/record.rb +9 -0
  42. data/lib/revel_sandbox_simulator/models/simulated_order.rb +20 -0
  43. data/lib/revel_sandbox_simulator/models/simulated_payment.rb +16 -0
  44. data/lib/revel_sandbox_simulator/seeder.rb +115 -0
  45. data/lib/revel_sandbox_simulator/services/base_service.rb +143 -0
  46. data/lib/revel_sandbox_simulator/services/revel/customer_service.rb +53 -0
  47. data/lib/revel_sandbox_simulator/services/revel/establishment_service.rb +19 -0
  48. data/lib/revel_sandbox_simulator/services/revel/order_service.rb +77 -0
  49. data/lib/revel_sandbox_simulator/services/revel/payment_service.rb +52 -0
  50. data/lib/revel_sandbox_simulator/services/revel/product_service.rb +87 -0
  51. data/lib/revel_sandbox_simulator/services/revel/services_manager.rb +45 -0
  52. data/lib/revel_sandbox_simulator/version.rb +5 -0
  53. data/lib/revel_sandbox_simulator.rb +38 -0
  54. 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": "Cash", "weight": 25 },
3
+ { "name": "Credit Card", "weight": 45 },
4
+ { "name": "Debit Card", "weight": 20 },
5
+ { "name": "Gift Card", "weight": 5 },
6
+ { "name": "Check", "weight": 5 }
7
+ ]
@@ -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
+ ]