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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +5 -0
  3. data/LICENSE +21 -0
  4. data/README.md +215 -0
  5. data/bin/simulate +162 -0
  6. data/lib/lightspeed_sandbox_simulator/configuration.rb +151 -0
  7. data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/categories.json +9 -0
  8. data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/items.json +26 -0
  9. data/lib/lightspeed_sandbox_simulator/data/bar_nightclub/tenders.json +8 -0
  10. data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/categories.json +9 -0
  11. data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/items.json +28 -0
  12. data/lib/lightspeed_sandbox_simulator/data/cafe_bakery/tenders.json +8 -0
  13. data/lib/lightspeed_sandbox_simulator/data/restaurant/categories.json +9 -0
  14. data/lib/lightspeed_sandbox_simulator/data/restaurant/items.json +29 -0
  15. data/lib/lightspeed_sandbox_simulator/data/restaurant/tenders.json +9 -0
  16. data/lib/lightspeed_sandbox_simulator/data/retail_general/categories.json +9 -0
  17. data/lib/lightspeed_sandbox_simulator/data/retail_general/items.json +17 -0
  18. data/lib/lightspeed_sandbox_simulator/data/retail_general/tenders.json +8 -0
  19. data/lib/lightspeed_sandbox_simulator/database.rb +116 -0
  20. data/lib/lightspeed_sandbox_simulator/db/factories/api_requests.rb +10 -0
  21. data/lib/lightspeed_sandbox_simulator/db/factories/business_types.rb +9 -0
  22. data/lib/lightspeed_sandbox_simulator/db/factories/categories.rb +9 -0
  23. data/lib/lightspeed_sandbox_simulator/db/factories/items.rb +11 -0
  24. data/lib/lightspeed_sandbox_simulator/db/factories/simulated_orders.rb +17 -0
  25. data/lib/lightspeed_sandbox_simulator/db/factories/simulated_payments.rb +13 -0
  26. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000001_enable_pgcrypto.rb +7 -0
  27. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000002_create_business_types.rb +14 -0
  28. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000003_create_categories.rb +13 -0
  29. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000004_create_items.rb +14 -0
  30. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000005_create_simulated_orders.rb +23 -0
  31. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000006_create_simulated_payments.rb +16 -0
  32. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000007_create_api_requests.rb +18 -0
  33. data/lib/lightspeed_sandbox_simulator/db/migrate/20260313000008_create_daily_summaries.rb +20 -0
  34. data/lib/lightspeed_sandbox_simulator/generators/data_loader.rb +75 -0
  35. data/lib/lightspeed_sandbox_simulator/generators/entity_generator.rb +96 -0
  36. data/lib/lightspeed_sandbox_simulator/generators/order_generator.rb +293 -0
  37. data/lib/lightspeed_sandbox_simulator/models/api_request.rb +9 -0
  38. data/lib/lightspeed_sandbox_simulator/models/business_type.rb +12 -0
  39. data/lib/lightspeed_sandbox_simulator/models/category.rb +12 -0
  40. data/lib/lightspeed_sandbox_simulator/models/daily_summary.rb +44 -0
  41. data/lib/lightspeed_sandbox_simulator/models/item.rb +11 -0
  42. data/lib/lightspeed_sandbox_simulator/models/simulated_order.rb +15 -0
  43. data/lib/lightspeed_sandbox_simulator/models/simulated_payment.rb +13 -0
  44. data/lib/lightspeed_sandbox_simulator/seeder.rb +79 -0
  45. data/lib/lightspeed_sandbox_simulator/services/base_service.rb +158 -0
  46. data/lib/lightspeed_sandbox_simulator/services/lightspeed/business_service.rb +21 -0
  47. data/lib/lightspeed_sandbox_simulator/services/lightspeed/menu_service.rb +70 -0
  48. data/lib/lightspeed_sandbox_simulator/services/lightspeed/order_service.rb +74 -0
  49. data/lib/lightspeed_sandbox_simulator/services/lightspeed/payment_method_service.rb +30 -0
  50. data/lib/lightspeed_sandbox_simulator/services/lightspeed/payment_service.rb +55 -0
  51. data/lib/lightspeed_sandbox_simulator/services/lightspeed/services_manager.rb +54 -0
  52. data/lib/lightspeed_sandbox_simulator.rb +30 -0
  53. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
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
+ }