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
@@ -0,0 +1,29 @@
1
+ {
2
+ "items": [
3
+ { "name": "Buffalo Wings", "price": 12.99, "category": "Appetizers", "sku": "REST-APP-001" },
4
+ { "name": "Mozzarella Sticks", "price": 9.99, "category": "Appetizers", "sku": "REST-APP-002" },
5
+ { "name": "Loaded Nachos", "price": 13.99, "category": "Appetizers", "sku": "REST-APP-003" },
6
+ { "name": "Bruschetta", "price": 10.99, "category": "Appetizers", "sku": "REST-APP-004" },
7
+ { "name": "Calamari", "price": 11.99, "category": "Appetizers", "sku": "REST-APP-005" },
8
+ { "name": "Grilled Salmon", "price": 24.99, "category": "Entrees", "sku": "REST-ENT-001" },
9
+ { "name": "NY Strip Steak", "price": 29.99, "category": "Entrees", "sku": "REST-ENT-002" },
10
+ { "name": "Chicken Parmesan", "price": 18.99, "category": "Entrees", "sku": "REST-ENT-003" },
11
+ { "name": "Fish and Chips", "price": 16.99, "category": "Entrees", "sku": "REST-ENT-004" },
12
+ { "name": "Pasta Alfredo", "price": 15.99, "category": "Entrees", "sku": "REST-ENT-005" },
13
+ { "name": "BBQ Ribs", "price": 22.99, "category": "Entrees", "sku": "REST-ENT-006" },
14
+ { "name": "Caesar Salad", "price": 12.99, "category": "Entrees", "sku": "REST-ENT-007" },
15
+ { "name": "French Fries", "price": 5.99, "category": "Sides", "sku": "REST-SID-001" },
16
+ { "name": "Coleslaw", "price": 4.99, "category": "Sides", "sku": "REST-SID-002" },
17
+ { "name": "Mac and Cheese", "price": 6.99, "category": "Sides", "sku": "REST-SID-003" },
18
+ { "name": "Garden Salad", "price": 5.49, "category": "Sides", "sku": "REST-SID-004" },
19
+ { "name": "Onion Rings", "price": 6.49, "category": "Sides", "sku": "REST-SID-005" },
20
+ { "name": "Coca-Cola", "price": 2.99, "category": "Drinks", "sku": "REST-DRK-001" },
21
+ { "name": "Iced Tea", "price": 2.99, "category": "Drinks", "sku": "REST-DRK-002" },
22
+ { "name": "Draft Beer", "price": 6.99, "category": "Drinks", "sku": "REST-DRK-003" },
23
+ { "name": "House Wine", "price": 8.99, "category": "Drinks", "sku": "REST-DRK-004" },
24
+ { "name": "Lemonade", "price": 3.49, "category": "Drinks", "sku": "REST-DRK-005" },
25
+ { "name": "Chocolate Cake", "price": 8.99, "category": "Desserts", "sku": "REST-DES-001" },
26
+ { "name": "Cheesecake", "price": 9.99, "category": "Desserts", "sku": "REST-DES-002" },
27
+ { "name": "Ice Cream Sundae", "price": 7.99, "category": "Desserts", "sku": "REST-DES-003" }
28
+ ]
29
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "tenders": [
3
+ { "name": "Cash", "description": "Cash payment", "weight": 25 },
4
+ { "name": "Credit Card", "description": "Credit card payment", "weight": 45 },
5
+ { "name": "Debit Card", "description": "Debit card payment", "weight": 20 },
6
+ { "name": "Gift Card", "description": "Gift card payment", "weight": 5 },
7
+ { "name": "Check", "description": "Check payment", "weight": 5 }
8
+ ]
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "categories": [
3
+ { "name": "Electronics", "sort_order": 1, "description": "Electronic devices and accessories" },
4
+ { "name": "Clothing", "sort_order": 2, "description": "Apparel and accessories" },
5
+ { "name": "Home & Garden", "sort_order": 3, "description": "Home goods and garden supplies" },
6
+ { "name": "Health & Beauty", "sort_order": 4, "description": "Health and beauty products" },
7
+ { "name": "Groceries", "sort_order": 5, "description": "Food and grocery items" }
8
+ ]
9
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "items": [
3
+ { "name": "Phone Charger", "price": 14.99, "category": "Electronics", "sku": "RET-ELC-001" },
4
+ { "name": "Bluetooth Speaker", "price": 29.99, "category": "Electronics", "sku": "RET-ELC-002" },
5
+ { "name": "USB Cable", "price": 9.99, "category": "Electronics", "sku": "RET-ELC-003" },
6
+ { "name": "T-Shirt", "price": 19.99, "category": "Clothing", "sku": "RET-CLO-001" },
7
+ { "name": "Baseball Cap", "price": 14.99, "category": "Clothing", "sku": "RET-CLO-002" },
8
+ { "name": "Socks Pack", "price": 8.99, "category": "Clothing", "sku": "RET-CLO-003" },
9
+ { "name": "Candle", "price": 12.99, "category": "Home & Garden", "sku": "RET-HOM-001" },
10
+ { "name": "Plant Pot", "price": 9.99, "category": "Home & Garden", "sku": "RET-HOM-002" },
11
+ { "name": "Picture Frame", "price": 11.99, "category": "Home & Garden", "sku": "RET-HOM-003" },
12
+ { "name": "Hand Cream", "price": 7.99, "category": "Health & Beauty", "sku": "RET-HLT-001" },
13
+ { "name": "Lip Balm", "price": 4.99, "category": "Health & Beauty", "sku": "RET-HLT-002" },
14
+ { "name": "Snack Bar", "price": 2.99, "category": "Groceries", "sku": "RET-GRC-001" },
15
+ { "name": "Water Bottle", "price": 1.99, "category": "Groceries", "sku": "RET-GRC-002" }
16
+ ]
17
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tenders": [
3
+ { "name": "Cash", "description": "Cash payment", "weight": 20 },
4
+ { "name": "Credit Card", "description": "Credit card payment", "weight": 50 },
5
+ { "name": "Debit Card", "description": "Debit card payment", "weight": 25 },
6
+ { "name": "Gift Card", "description": "Gift card payment", "weight": 5 }
7
+ ]
8
+ }
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record'
4
+ require 'uri'
5
+
6
+ module LightspeedSandboxSimulator
7
+ class Database
8
+ MIGRATIONS_PATH = File.expand_path('db/migrate', __dir__)
9
+ TEST_DATABASE = 'lightspeed_simulator_test'
10
+
11
+ class << self
12
+ def connect!(url)
13
+ raise ArgumentError, 'Only PostgreSQL URLs are supported' unless url.match?(%r{\Apostgres(ql)?://})
14
+
15
+ ActiveRecord::Base.establish_connection(url)
16
+ ActiveRecord::Base.connection.execute('SELECT 1')
17
+ ActiveRecord::Base.logger = LightspeedSandboxSimulator.logger
18
+ LightspeedSandboxSimulator.logger.info("Connected to #{sanitize_url(url)}")
19
+ end
20
+
21
+ def connected?
22
+ ActiveRecord::Base.connection_pool.connected? && ActiveRecord::Base.connection.active?
23
+ rescue StandardError
24
+ false
25
+ end
26
+
27
+ def disconnect!
28
+ ActiveRecord::Base.connection_pool.disconnect!
29
+ end
30
+
31
+ def create!(url)
32
+ db_name = URI.parse(url).path.delete_prefix('/')
33
+ admin_url = url.sub(%r{/[^/]+\z}, '/postgres')
34
+
35
+ ActiveRecord::Base.establish_connection(admin_url)
36
+ ActiveRecord::Base.connection.create_database(db_name)
37
+ LightspeedSandboxSimulator.logger.info("Created database: #{db_name}")
38
+ rescue ActiveRecord::StatementInvalid => e
39
+ raise unless e.message.include?('already exists')
40
+
41
+ LightspeedSandboxSimulator.logger.info("Database already exists: #{db_name}")
42
+ ensure
43
+ ActiveRecord::Base.connection_pool.disconnect!
44
+ end
45
+
46
+ def drop!(url)
47
+ db_name = URI.parse(url).path.delete_prefix('/')
48
+ admin_url = url.sub(%r{/[^/]+\z}, '/postgres')
49
+
50
+ ActiveRecord::Base.establish_connection(admin_url)
51
+ ActiveRecord::Base.connection.drop_database(db_name)
52
+ LightspeedSandboxSimulator.logger.info("Dropped database: #{db_name}")
53
+ rescue ActiveRecord::StatementInvalid => e
54
+ raise unless e.message.include?('does not exist')
55
+
56
+ LightspeedSandboxSimulator.logger.info("Database does not exist: #{db_name}")
57
+ ensure
58
+ ActiveRecord::Base.connection_pool.disconnect!
59
+ end
60
+
61
+ def migrate!
62
+ raise Error, 'Database not connected' unless connected?
63
+
64
+ ActiveRecord::MigrationContext.new(MIGRATIONS_PATH).migrate
65
+ LightspeedSandboxSimulator.logger.info('Migrations complete')
66
+ end
67
+
68
+ def seed!(business_type: :restaurant)
69
+ raise Error, 'Database not connected' unless connected?
70
+
71
+ load_factories!
72
+ Seeder.seed!(business_type: business_type)
73
+ end
74
+
75
+ def database_url
76
+ url = Configuration.database_url_from_file
77
+ raise Error, 'DATABASE_URL not found in .env.json' unless url
78
+
79
+ url
80
+ end
81
+
82
+ def test_database_url(base_url: nil)
83
+ return "postgres://localhost:5432/#{TEST_DATABASE}" unless base_url
84
+
85
+ uri = URI.parse(base_url)
86
+ uri.path = "/#{TEST_DATABASE}"
87
+ uri.to_s
88
+ rescue URI::InvalidURIError
89
+ "postgres://localhost:5432/#{TEST_DATABASE}"
90
+ end
91
+
92
+ private
93
+
94
+ def sanitize_url(url)
95
+ uri = URI.parse(url)
96
+ has_password = !uri.password.nil?
97
+ uri.user = '***' if uri.user
98
+ uri.password = '***' if has_password
99
+ uri.to_s
100
+ rescue URI::InvalidURIError
101
+ url.gsub(%r{://[^@]+@}, '://***:***@')
102
+ end
103
+
104
+ def load_factories!
105
+ return if @factories_loaded
106
+
107
+ factories_dir = File.expand_path('db/factories', __dir__)
108
+ FactoryBot.definition_file_paths = [factories_dir] if Dir.exist?(factories_dir)
109
+ FactoryBot.find_definitions
110
+ @factories_loaded = true
111
+ rescue StandardError => e
112
+ LightspeedSandboxSimulator.logger.warn("Failed to load factories: #{e.message}")
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :api_request, class: 'LightspeedSandboxSimulator::Models::ApiRequest' do
5
+ http_method { 'GET' }
6
+ url { 'https://api.lsk.lightspeed.app/api/v2/businesses/123/menu/categories' }
7
+ response_status { 200 }
8
+ duration_ms { 150 }
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :business_type, class: 'LightspeedSandboxSimulator::Models::BusinessType' do
5
+ key { 'restaurant' }
6
+ name { 'Restaurant' }
7
+ industry { 'food' }
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :category, class: 'LightspeedSandboxSimulator::Models::Category' do
5
+ association :business_type
6
+ name { 'Appetizers' }
7
+ sort_order { 1 }
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :item, class: 'LightspeedSandboxSimulator::Models::Item' do
5
+ association :category
6
+ name { 'Buffalo Wings' }
7
+ price { 1299 }
8
+ sku { 'WING-001' }
9
+ taxable { true }
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :simulated_order, class: 'LightspeedSandboxSimulator::Models::SimulatedOrder' do
5
+ order_id { rand(1000..9999).to_s }
6
+ order_type { 'local' }
7
+ status { 'paid' }
8
+ meal_period { 'lunch' }
9
+ dining_option { 'eat_in' }
10
+ order_date { Date.today }
11
+ total { 2500 }
12
+ tax_amount { 500 }
13
+ tip_amount { 375 }
14
+ discount_amount { 0 }
15
+ item_count { 3 }
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :simulated_payment, class: 'LightspeedSandboxSimulator::Models::SimulatedPayment' do
5
+ association :simulated_order
6
+ payment_id { rand(1000..9999).to_s }
7
+ tender_name { 'Cash' }
8
+ tender_type { 'CASH' }
9
+ status { 'successful' }
10
+ amount { 2500 }
11
+ tip_amount { 375 }
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EnablePgcrypto < ActiveRecord::Migration[8.0]
4
+ def change
5
+ enable_extension 'pgcrypto'
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBusinessTypes < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :business_types, id: :uuid do |t|
6
+ t.string :key, null: false
7
+ t.string :name, null: false
8
+ t.string :industry, default: 'food'
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :business_types, :key, unique: true
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCategories < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :categories, id: :uuid do |t|
6
+ t.references :business_type, type: :uuid, null: false, foreign_key: true
7
+ t.string :name, null: false
8
+ t.string :description
9
+ t.integer :sort_order, default: 0
10
+ t.timestamps
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateItems < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :items, id: :uuid do |t|
6
+ t.references :category, type: :uuid, null: false, foreign_key: true
7
+ t.string :name, null: false
8
+ t.integer :price, null: false
9
+ t.string :sku
10
+ t.boolean :taxable, default: true
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSimulatedOrders < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :simulated_orders, id: :uuid do |t|
6
+ t.string :order_id
7
+ t.string :order_type
8
+ t.string :status, default: 'paid'
9
+ t.string :meal_period
10
+ t.string :dining_option
11
+ t.date :order_date
12
+ t.integer :total, default: 0
13
+ t.integer :tax_amount, default: 0
14
+ t.integer :tip_amount, default: 0
15
+ t.integer :discount_amount, default: 0
16
+ t.integer :item_count, default: 0
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :simulated_orders, :order_date
21
+ add_index :simulated_orders, :status
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSimulatedPayments < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :simulated_payments, id: :uuid do |t|
6
+ t.references :simulated_order, type: :uuid, null: false, foreign_key: true
7
+ t.string :payment_id
8
+ t.string :tender_name
9
+ t.string :tender_type
10
+ t.string :status, default: 'successful'
11
+ t.integer :amount, default: 0
12
+ t.integer :tip_amount, default: 0
13
+ t.timestamps
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateApiRequests < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :api_requests, id: :uuid do |t|
6
+ t.string :http_method
7
+ t.text :url
8
+ t.text :request_payload
9
+ t.integer :response_status
10
+ t.text :response_payload
11
+ t.integer :duration_ms
12
+ t.string :resource_type
13
+ t.string :resource_id
14
+ t.text :error_message
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDailySummaries < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :daily_summaries, id: :uuid do |t|
6
+ t.date :summary_date, null: false
7
+ t.integer :order_count, default: 0
8
+ t.integer :payment_count, default: 0
9
+ t.integer :refund_count, default: 0
10
+ t.integer :total_revenue, default: 0
11
+ t.integer :total_tax, default: 0
12
+ t.integer :total_tips, default: 0
13
+ t.integer :total_discounts, default: 0
14
+ t.jsonb :breakdown, default: {}
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :daily_summaries, :summary_date, unique: true
19
+ end
20
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module LightspeedSandboxSimulator
6
+ module Generators
7
+ class DataLoader
8
+ DATA_DIR = File.expand_path('../data', __dir__)
9
+ BUSINESS_TYPES = %i[restaurant cafe_bakery bar_nightclub retail_general].freeze
10
+
11
+ attr_reader :business_type
12
+
13
+ def initialize(business_type: :restaurant)
14
+ @business_type = business_type
15
+ end
16
+
17
+ def load_categories
18
+ if Database.connected?
19
+ bt = Models::BusinessType.find_by(key: business_type.to_s)
20
+ return load_categories_from_db(bt) if bt
21
+ end
22
+
23
+ data = load_from_json('categories')
24
+ data.fetch('categories', [])
25
+ end
26
+
27
+ def load_items
28
+ if Database.connected?
29
+ bt = Models::BusinessType.find_by(key: business_type.to_s)
30
+ return load_items_from_db(bt) if bt
31
+ end
32
+
33
+ data = load_from_json('items')
34
+ data.fetch('items', [])
35
+ end
36
+
37
+ def load_tenders
38
+ data = load_from_json('tenders')
39
+ data.fetch('tenders', [])
40
+ end
41
+
42
+ def load_items_by_category
43
+ load_items.group_by { |i| i['category'] }
44
+ end
45
+
46
+ private
47
+
48
+ def load_categories_from_db(btype)
49
+ btype.categories.order(:sort_order).map do |cat|
50
+ { 'name' => cat.name, 'sort_order' => cat.sort_order, 'description' => cat.description }
51
+ end
52
+ end
53
+
54
+ def load_items_from_db(btype)
55
+ btype.items.includes(:category).map do |item|
56
+ {
57
+ 'name' => item.name,
58
+ 'price' => item.price / 100.0,
59
+ 'category' => item.category&.name,
60
+ 'sku' => item.sku
61
+ }
62
+ end
63
+ end
64
+
65
+ def load_from_json(type)
66
+ path = File.join(DATA_DIR, business_type.to_s, "#{type}.json")
67
+ return {} unless File.exist?(path)
68
+
69
+ JSON.parse(File.read(path))
70
+ rescue JSON::ParserError
71
+ {}
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LightspeedSandboxSimulator
4
+ module Generators
5
+ class EntityGenerator
6
+ def initialize(config: nil, business_type: :restaurant)
7
+ @config = config || LightspeedSandboxSimulator.configuration
8
+ @business_type = business_type
9
+ @loader = DataLoader.new(business_type: business_type)
10
+ @manager = Services::Lightspeed::ServicesManager.new(config: @config)
11
+ end
12
+
13
+ def setup_all
14
+ categories = setup_categories
15
+ items = setup_items(categories)
16
+ payment_methods = setup_payment_methods
17
+
18
+ LightspeedSandboxSimulator.logger.info("Setup complete: #{categories.size} categories, " \
19
+ "#{items.size} items, #{payment_methods.size} payment methods")
20
+
21
+ { categories: categories, items: items, payment_methods: payment_methods }
22
+ end
23
+
24
+ def setup_categories
25
+ data = @loader.load_categories
26
+ created = []
27
+
28
+ data.each do |cat|
29
+ existing = @manager.menu.find_category_by_name(cat['name'])
30
+ if existing
31
+ created << existing
32
+ else
33
+ result = @manager.menu.create_category(name: cat['name'], sort_order: cat['sort_order'])
34
+ created << result if result
35
+ end
36
+ end
37
+
38
+ created
39
+ end
40
+
41
+ def setup_items(categories)
42
+ data = @loader.load_items
43
+ category_map = build_category_map(categories)
44
+ created = []
45
+
46
+ data.each do |item|
47
+ existing = @manager.menu.find_item_by_name(item['name'])
48
+ if existing
49
+ created << existing
50
+ else
51
+ category_id = category_map[item['category']]
52
+ result = @manager.menu.create_item(
53
+ name: item['name'],
54
+ price: item['price'],
55
+ category_id: category_id,
56
+ sku: item['sku']
57
+ )
58
+ created << result if result
59
+ end
60
+ end
61
+
62
+ created
63
+ end
64
+
65
+ def setup_payment_methods
66
+ data = @loader.load_tenders
67
+ created = []
68
+
69
+ data.each do |tender|
70
+ existing = @manager.payment_methods.find_payment_method_by_name(tender['name'])
71
+ if existing
72
+ created << existing
73
+ else
74
+ result = @manager.payment_methods.create_payment_method(
75
+ name: tender['name'],
76
+ type: tender.fetch('type', 'OTHER')
77
+ )
78
+ created << result if result
79
+ end
80
+ end
81
+
82
+ created
83
+ end
84
+
85
+ private
86
+
87
+ def build_category_map(categories)
88
+ categories.each_with_object({}) do |cat, map|
89
+ name = cat['name'] || cat[:name]
90
+ id = cat['id'] || cat[:id]
91
+ map[name] = id if name && id
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end