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
@@ -0,0 +1,27 @@
1
+ [
2
+ { "name": "Espresso", "price": 350, "sku": "CAF-ESPRES", "category": "Hot Drinks" },
3
+ { "name": "Cappuccino", "price": 450, "sku": "CAF-CAPPUC", "category": "Hot Drinks" },
4
+ { "name": "Latte", "price": 499, "sku": "CAF-LATTE", "category": "Hot Drinks" },
5
+ { "name": "Americano", "price": 399, "sku": "CAF-AMRCNO", "category": "Hot Drinks" },
6
+ { "name": "Hot Chocolate", "price": 449, "sku": "CAF-HTCHOC", "category": "Hot Drinks" },
7
+ { "name": "Iced Coffee", "price": 399, "sku": "CAF-ICDCOF", "category": "Cold Drinks" },
8
+ { "name": "Cold Brew", "price": 499, "sku": "CAF-CLDBRW", "category": "Cold Drinks" },
9
+ { "name": "Iced Latte", "price": 549, "sku": "CAF-ICDLAT", "category": "Cold Drinks" },
10
+ { "name": "Smoothie", "price": 649, "sku": "CAF-SMOOTH", "category": "Cold Drinks" },
11
+ { "name": "Lemonade", "price": 399, "sku": "CAF-LMNADE", "category": "Cold Drinks" },
12
+ { "name": "Croissant", "price": 350, "sku": "CAF-CRSSNT", "category": "Pastries" },
13
+ { "name": "Muffin", "price": 399, "sku": "CAF-MUFFIN", "category": "Pastries" },
14
+ { "name": "Scone", "price": 375, "sku": "CAF-SCONE", "category": "Pastries" },
15
+ { "name": "Danish", "price": 425, "sku": "CAF-DANISH", "category": "Pastries" },
16
+ { "name": "Cinnamon Roll", "price": 475, "sku": "CAF-CINROL", "category": "Pastries" },
17
+ { "name": "Avocado Toast", "price": 950, "sku": "CAF-AVOTST", "category": "Sandwiches" },
18
+ { "name": "BLT", "price": 899, "sku": "CAF-BLT", "category": "Sandwiches" },
19
+ { "name": "Turkey Club", "price": 1099, "sku": "CAF-TRKCLB", "category": "Sandwiches" },
20
+ { "name": "Grilled Cheese", "price": 799, "sku": "CAF-GRLCHS", "category": "Sandwiches" },
21
+ { "name": "Veggie Wrap", "price": 899, "sku": "CAF-VGWRAP", "category": "Sandwiches" },
22
+ { "name": "Chocolate Cake", "price": 599, "sku": "CAF-CHKCKE", "category": "Cakes" },
23
+ { "name": "Red Velvet", "price": 649, "sku": "CAF-RDVLVT", "category": "Cakes" },
24
+ { "name": "Carrot Cake", "price": 599, "sku": "CAF-CRTCKE", "category": "Cakes" },
25
+ { "name": "Lemon Tart", "price": 549, "sku": "CAF-LMNTRT", "category": "Cakes" },
26
+ { "name": "Brownie", "price": 449, "sku": "CAF-BROWNE", "category": "Cakes" }
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": "Appetizers", "sort_order": 0, "description": "Starters and small plates" },
3
+ { "name": "Entrees", "sort_order": 1, "description": "Main course dishes" },
4
+ { "name": "Sides", "sort_order": 2, "description": "Side dishes and salads" },
5
+ { "name": "Beverages", "sort_order": 3, "description": "Drinks and refreshments" },
6
+ { "name": "Desserts", "sort_order": 4, "description": "Sweet treats and desserts" }
7
+ ]
@@ -0,0 +1,27 @@
1
+ [
2
+ { "name": "Buffalo Wings", "price": 1299, "sku": "RES-BUFWNG", "category": "Appetizers" },
3
+ { "name": "Mozzarella Sticks", "price": 999, "sku": "RES-MOZSTK", "category": "Appetizers" },
4
+ { "name": "Calamari", "price": 1399, "sku": "RES-CALMRI", "category": "Appetizers" },
5
+ { "name": "Bruschetta", "price": 1099, "sku": "RES-BRUSCH", "category": "Appetizers" },
6
+ { "name": "Spinach Dip", "price": 1199, "sku": "RES-SPNDIP", "category": "Appetizers" },
7
+ { "name": "NY Strip Steak", "price": 2999, "sku": "RES-NYSTK", "category": "Entrees" },
8
+ { "name": "Grilled Salmon", "price": 2499, "sku": "RES-GRLSLM", "category": "Entrees" },
9
+ { "name": "Chicken Parmesan", "price": 1899, "sku": "RES-CHKPRM", "category": "Entrees" },
10
+ { "name": "Lamb Chops", "price": 3299, "sku": "RES-LMBCHP", "category": "Entrees" },
11
+ { "name": "Shrimp Pasta", "price": 2199, "sku": "RES-SHRPST", "category": "Entrees" },
12
+ { "name": "Caesar Salad", "price": 1299, "sku": "RES-CSRSLD", "category": "Sides" },
13
+ { "name": "Mashed Potatoes", "price": 699, "sku": "RES-MSHPOT", "category": "Sides" },
14
+ { "name": "Grilled Vegetables", "price": 799, "sku": "RES-GRLVEG", "category": "Sides" },
15
+ { "name": "French Fries", "price": 599, "sku": "RES-FRNFRY", "category": "Sides" },
16
+ { "name": "Coleslaw", "price": 499, "sku": "RES-COLSLW", "category": "Sides" },
17
+ { "name": "Soft Drink", "price": 299, "sku": "RES-SFTDRK", "category": "Beverages" },
18
+ { "name": "Iced Tea", "price": 349, "sku": "RES-ICDTEA", "category": "Beverages" },
19
+ { "name": "Lemonade", "price": 399, "sku": "RES-LMNADE", "category": "Beverages" },
20
+ { "name": "Coffee", "price": 299, "sku": "RES-COFFEE", "category": "Beverages" },
21
+ { "name": "Sparkling Water", "price": 349, "sku": "RES-SPKWTR", "category": "Beverages" },
22
+ { "name": "Chocolate Cake", "price": 899, "sku": "RES-CHKCKE", "category": "Desserts" },
23
+ { "name": "Tiramisu", "price": 999, "sku": "RES-TRMISU", "category": "Desserts" },
24
+ { "name": "Cheesecake", "price": 899, "sku": "RES-CHSCKE", "category": "Desserts" },
25
+ { "name": "Creme Brulee", "price": 1099, "sku": "RES-CRMBRL", "category": "Desserts" },
26
+ { "name": "Apple Pie", "price": 799, "sku": "RES-APLPIE", "category": "Desserts" }
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": "Electronics", "sort_order": 0, "description": "Tech accessories and gadgets" },
3
+ { "name": "Clothing", "sort_order": 1, "description": "Apparel and accessories" },
4
+ { "name": "Home & Garden", "sort_order": 2, "description": "Home decor and garden items" },
5
+ { "name": "Health & Beauty", "sort_order": 3, "description": "Personal care products" },
6
+ { "name": "Groceries", "sort_order": 4, "description": "Snacks and beverages" }
7
+ ]
@@ -0,0 +1,27 @@
1
+ [
2
+ { "name": "Phone Charger", "price": 1999, "sku": "RTL-PHNCHR", "category": "Electronics" },
3
+ { "name": "USB Cable", "price": 999, "sku": "RTL-USBCBL", "category": "Electronics" },
4
+ { "name": "Earbuds", "price": 2499, "sku": "RTL-EARBD", "category": "Electronics" },
5
+ { "name": "Power Bank", "price": 3499, "sku": "RTL-PWRBNK", "category": "Electronics" },
6
+ { "name": "Screen Protector", "price": 1299, "sku": "RTL-SCNPRT", "category": "Electronics" },
7
+ { "name": "T-Shirt", "price": 2499, "sku": "RTL-TSHIRT", "category": "Clothing" },
8
+ { "name": "Hoodie", "price": 4999, "sku": "RTL-HOODIE", "category": "Clothing" },
9
+ { "name": "Cap", "price": 1999, "sku": "RTL-CAP", "category": "Clothing" },
10
+ { "name": "Socks", "price": 899, "sku": "RTL-SOCKS", "category": "Clothing" },
11
+ { "name": "Belt", "price": 2999, "sku": "RTL-BELT", "category": "Clothing" },
12
+ { "name": "Candle", "price": 1499, "sku": "RTL-CANDLE", "category": "Home & Garden" },
13
+ { "name": "Mug", "price": 1299, "sku": "RTL-MUG", "category": "Home & Garden" },
14
+ { "name": "Plant Pot", "price": 1799, "sku": "RTL-PLTPOT", "category": "Home & Garden" },
15
+ { "name": "Picture Frame", "price": 1999, "sku": "RTL-PICFRM", "category": "Home & Garden" },
16
+ { "name": "Cushion", "price": 2499, "sku": "RTL-CUSHN", "category": "Home & Garden" },
17
+ { "name": "Hand Cream", "price": 899, "sku": "RTL-HNDCRM", "category": "Health & Beauty" },
18
+ { "name": "Lip Balm", "price": 499, "sku": "RTL-LIPBLM", "category": "Health & Beauty" },
19
+ { "name": "Face Mask", "price": 699, "sku": "RTL-FCMSK", "category": "Health & Beauty" },
20
+ { "name": "Shampoo", "price": 1199, "sku": "RTL-SHMPO", "category": "Health & Beauty" },
21
+ { "name": "Body Wash", "price": 999, "sku": "RTL-BDWSH", "category": "Health & Beauty" },
22
+ { "name": "Snack Bar", "price": 299, "sku": "RTL-SNKBAR", "category": "Groceries" },
23
+ { "name": "Energy Drink", "price": 399, "sku": "RTL-ENGDRK", "category": "Groceries" },
24
+ { "name": "Water Bottle", "price": 199, "sku": "RTL-WTRBTL", "category": "Groceries" },
25
+ { "name": "Trail Mix", "price": 549, "sku": "RTL-TRLMIX", "category": "Groceries" },
26
+ { "name": "Protein Bar", "price": 399, "sku": "RTL-PRTBAR", "category": "Groceries" }
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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "logger"
5
+
6
+ module RevelSandboxSimulator
7
+ module Database
8
+ MIGRATIONS_PATH = File.expand_path("db/migrate", __dir__)
9
+
10
+ class << self
11
+ def connect!(url = nil)
12
+ url ||= RevelSandboxSimulator.configuration.database_url
13
+ raise ConfigurationError, "DATABASE_URL is required" if url.nil? || url.empty?
14
+
15
+ ActiveRecord::Base.establish_connection(url)
16
+ ActiveRecord::Base.logger = Logger.new(File::NULL)
17
+ true
18
+ end
19
+
20
+ def connected?
21
+ ActiveRecord::Base.connection_pool.with_connection(&:active?)
22
+ rescue StandardError
23
+ false
24
+ end
25
+
26
+ def disconnect!
27
+ ActiveRecord::Base.connection_handler.clear_active_connections!
28
+ end
29
+
30
+ def create!(url = nil)
31
+ url ||= RevelSandboxSimulator.configuration.database_url
32
+ config = connection_config(url)
33
+ database = config[:database]
34
+
35
+ ActiveRecord::Base.establish_connection(config.merge(database: "postgres"))
36
+ ActiveRecord::Base.connection.create_database(database)
37
+ RevelSandboxSimulator.logger.info("Created database: #{sanitize_url(url)}")
38
+ rescue ActiveRecord::DatabaseAlreadyExists
39
+ RevelSandboxSimulator.logger.info("Database already exists: #{sanitize_url(url)}")
40
+ ensure
41
+ ActiveRecord::Base.connection_handler.clear_active_connections!
42
+ end
43
+
44
+ def drop!(url = nil)
45
+ url ||= RevelSandboxSimulator.configuration.database_url
46
+ config = connection_config(url)
47
+ database = config[:database]
48
+
49
+ ActiveRecord::Base.establish_connection(config.merge(database: "postgres"))
50
+ ActiveRecord::Base.connection.drop_database(database)
51
+ RevelSandboxSimulator.logger.info("Dropped database: #{sanitize_url(url)}")
52
+ rescue ActiveRecord::NoDatabaseError
53
+ RevelSandboxSimulator.logger.info("Database does not exist: #{sanitize_url(url)}")
54
+ ensure
55
+ ActiveRecord::Base.connection_handler.clear_active_connections!
56
+ end
57
+
58
+ def migrate!
59
+ ActiveRecord::MigrationContext.new(MIGRATIONS_PATH).migrate
60
+ RevelSandboxSimulator.logger.info("Migrations complete")
61
+ end
62
+
63
+ def seed!(business_type: nil)
64
+ Seeder.new.seed!(business_type: business_type)
65
+ end
66
+
67
+ def test_database_url(base_url: nil)
68
+ base = base_url || RevelSandboxSimulator.configuration.database_url
69
+ return nil unless base
70
+
71
+ base.sub(%r{/[^/]+$}, "/revel_simulator_test")
72
+ end
73
+
74
+ private
75
+
76
+ def connection_config(url)
77
+ if url
78
+ parsed = ActiveRecord::DatabaseConfigurations::UrlConfig.new("primary", "primary", url)
79
+ parsed.configuration_hash.to_h
80
+ else
81
+ ActiveRecord::Base.connection_db_config.configuration_hash.to_h
82
+ end
83
+ end
84
+
85
+ def sanitize_url(url)
86
+ return "nil" unless url
87
+
88
+ url.gsub(%r{://[^@]+@}, "://***:***@")
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :api_request, class: "RevelSandboxSimulator::Models::ApiRequest" do
5
+ http_method { "GET" }
6
+ url { "https://sandbox.revelup.com/resources/Product/" }
7
+ response_status { 200 }
8
+ duration_ms { rand(50..500) }
9
+ request_payload { nil }
10
+ response_payload { { "objects" => [], "meta" => {} } }
11
+ error_message { nil }
12
+ resource_type { "Product" }
13
+ resource_id { nil }
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :business_type, class: "RevelSandboxSimulator::Models::BusinessType" do
5
+ sequence(:key) { |n| "business_type_#{n}" }
6
+ name { key.to_s.tr("_", " ").split.map(&:capitalize).join(" ") }
7
+ industry { "food" }
8
+ order_profile { {} }
9
+
10
+ trait :restaurant do
11
+ key { "restaurant" }
12
+ name { "Restaurant" }
13
+ industry { "food" }
14
+ end
15
+
16
+ trait :cafe_bakery do
17
+ key { "cafe_bakery" }
18
+ name { "Cafe Bakery" }
19
+ industry { "food" }
20
+ end
21
+
22
+ trait :bar_nightclub do
23
+ key { "bar_nightclub" }
24
+ name { "Bar Nightclub" }
25
+ industry { "food" }
26
+ end
27
+
28
+ trait :retail_general do
29
+ key { "retail_general" }
30
+ name { "Retail General" }
31
+ industry { "retail" }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :category, class: "RevelSandboxSimulator::Models::Category" do
5
+ sequence(:name) { |n| "Category #{n}" }
6
+ sort_order { 1 }
7
+ description { "A test category" }
8
+ association :business_type
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :item, class: "RevelSandboxSimulator::Models::Item" do
5
+ sequence(:name) { |n| "Item #{n}" }
6
+ sequence(:sku) { |n| "SKU-#{n.to_s.rjust(4, '0')}" }
7
+ price { rand(299..2999) }
8
+ metadata { {} }
9
+ association :business_type
10
+ association :category
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :simulated_order, class: "RevelSandboxSimulator::Models::SimulatedOrder" do
5
+ sequence(:revel_order_id) { |n| (10_000 + n).to_s }
6
+ status { "paid" }
7
+ business_date { Date.today }
8
+ dining_option { %w[dine_in takeout delivery].sample }
9
+ meal_period { %w[breakfast lunch happy_hour dinner late_night].sample }
10
+ subtotal { rand(800..5000) }
11
+ discount_amount { 0 }
12
+ tax_amount { (subtotal * 0.0825).round }
13
+ tip_amount { rand(0..500) }
14
+ total { subtotal - discount_amount + tax_amount + tip_amount }
15
+ metadata { { source: "test" } }
16
+
17
+ trait :refunded do
18
+ status { "refunded" }
19
+ end
20
+
21
+ trait :open do
22
+ status { "open" }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :simulated_payment, class: "RevelSandboxSimulator::Models::SimulatedPayment" do
5
+ sequence(:revel_payment_id) { |n| (20_000 + n).to_s }
6
+ association :simulated_order
7
+ tender_name { "Credit Card" }
8
+ amount { simulated_order&.total || rand(800..5000) }
9
+ tip_amount { 0 }
10
+ tax_amount { 0 }
11
+ status { "success" }
12
+ payment_type { "credit_card" }
13
+ end
14
+ 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,16 @@
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, null: false
9
+ t.jsonb :order_profile, default: {}
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :business_types, :key, unique: true
15
+ end
16
+ end
@@ -0,0 +1,16 @@
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.string :name, null: false
7
+ t.string :description
8
+ t.integer :sort_order, null: false, default: 0
9
+ t.references :business_type, type: :uuid, null: false, foreign_key: true
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :categories, %i[business_type_id name], unique: true
15
+ end
16
+ end
@@ -0,0 +1,19 @@
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.string :name, null: false
7
+ t.integer :price, null: false, default: 0
8
+ t.string :sku
9
+ t.string :barcode
10
+ t.jsonb :metadata, default: {}
11
+ t.references :business_type, type: :uuid, null: false, foreign_key: true
12
+ t.references :category, type: :uuid, foreign_key: true
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :items, :sku, unique: true, where: "sku IS NOT NULL"
18
+ end
19
+ end
@@ -0,0 +1,27 @@
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 :revel_order_id, null: false
7
+ t.string :status, null: false, default: "open"
8
+ t.date :business_date, null: false
9
+ t.string :dining_option, null: false
10
+ t.string :meal_period, null: false
11
+ t.integer :subtotal, null: false, default: 0
12
+ t.integer :tax_amount, null: false, default: 0
13
+ t.integer :tip_amount, null: false, default: 0
14
+ t.integer :discount_amount, null: false, default: 0
15
+ t.integer :total, null: false, default: 0
16
+ t.jsonb :metadata, default: {}
17
+
18
+ t.timestamps
19
+ end
20
+
21
+ add_index :simulated_orders, :revel_order_id, unique: true
22
+ add_index :simulated_orders, :status
23
+ add_index :simulated_orders, :business_date
24
+ add_index :simulated_orders, :meal_period
25
+ add_index :simulated_orders, :dining_option
26
+ end
27
+ end
@@ -0,0 +1,22 @@
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: { on_delete: :cascade }
7
+ t.string :revel_payment_id
8
+ t.string :tender_name, null: false
9
+ t.integer :amount, null: false, default: 0
10
+ t.integer :tip_amount, null: false, default: 0
11
+ t.integer :tax_amount, null: false, default: 0
12
+ t.string :status, null: false, default: "success"
13
+ t.string :payment_type
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :simulated_payments, :revel_payment_id, unique: true, where: "revel_payment_id IS NOT NULL"
19
+ add_index :simulated_payments, :tender_name
20
+ add_index :simulated_payments, :status
21
+ end
22
+ end
@@ -0,0 +1,22 @@
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.string :url
8
+ t.jsonb :request_payload
9
+ t.integer :response_status
10
+ t.jsonb :response_payload
11
+ t.integer :duration_ms
12
+ t.string :error_message
13
+ t.string :resource_type
14
+ t.string :resource_id
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :api_requests, :resource_type
20
+ add_index :api_requests, :created_at
21
+ end
22
+ end
@@ -0,0 +1,21 @@
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, null: false, default: 0
8
+ t.integer :payment_count, null: false, default: 0
9
+ t.integer :refund_count, null: false, default: 0
10
+ t.integer :total_revenue, null: false, default: 0
11
+ t.integer :total_tax, null: false, default: 0
12
+ t.integer :total_tips, null: false, default: 0
13
+ t.integer :total_discounts, null: false, default: 0
14
+ t.jsonb :breakdown, default: {}
15
+
16
+ t.timestamps
17
+ end
18
+
19
+ add_index :daily_summaries, :summary_date, unique: true
20
+ end
21
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RevelSandboxSimulator
4
+ module Generators
5
+ class DataLoader
6
+ BUSINESS_TYPES = %i[restaurant cafe_bakery bar_nightclub retail_general].freeze
7
+ DATA_DIR = File.expand_path("../data", __dir__)
8
+
9
+ attr_reader :config
10
+
11
+ def initialize(config: nil)
12
+ @config = config || RevelSandboxSimulator.configuration
13
+ end
14
+
15
+ def load_categories
16
+ if Database.connected?
17
+ load_categories_from_db
18
+ else
19
+ load_categories_from_json
20
+ end
21
+ end
22
+
23
+ def load_items
24
+ if Database.connected?
25
+ load_items_from_db
26
+ else
27
+ load_items_from_json
28
+ end
29
+ end
30
+
31
+ def load_tenders
32
+ load_json_file(config.business_type, "tenders")
33
+ end
34
+
35
+ def load_items_by_category
36
+ items = load_items
37
+ items.group_by { |item| item[:category] }
38
+ end
39
+
40
+ private
41
+
42
+ def load_categories_from_db
43
+ bt = Models::BusinessType.find_by(key: config.business_type.to_s)
44
+ return load_categories_from_json unless bt
45
+
46
+ Models::Category.for_business_type(bt).order(:sort_order).map do |cat|
47
+ { name: cat.name, sort_order: cat.sort_order, description: cat.description }
48
+ end
49
+ end
50
+
51
+ def load_items_from_db
52
+ bt = Models::BusinessType.find_by(key: config.business_type.to_s)
53
+ return load_items_from_json unless bt
54
+
55
+ Models::Item.for_business_type(bt).includes(:category).map do |item|
56
+ { name: item.name, price: item.price, sku: item.sku, category: item.category&.name }
57
+ end
58
+ end
59
+
60
+ def load_categories_from_json
61
+ load_json_file(config.business_type, "categories").map do |cat|
62
+ { name: cat["name"], sort_order: cat["sort_order"] || 0, description: cat["description"] }
63
+ end
64
+ end
65
+
66
+ def load_items_from_json
67
+ load_json_file(config.business_type, "items").map do |item|
68
+ { name: item["name"], price: item["price"], sku: item["sku"], category: item["category"] }
69
+ end
70
+ end
71
+
72
+ def load_json_file(business_type, filename)
73
+ path = File.join(DATA_DIR, business_type.to_s, "#{filename}.json")
74
+ return [] unless File.exist?(path)
75
+
76
+ JSON.parse(File.read(path))
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RevelSandboxSimulator
4
+ module Generators
5
+ class EntityGenerator
6
+ attr_reader :config, :services, :data_loader
7
+
8
+ def initialize(config: nil, services: nil)
9
+ @config = config || RevelSandboxSimulator.configuration
10
+ @services = services || Services::Revel::ServicesManager.new(config: @config)
11
+ @data_loader = DataLoader.new(config: @config)
12
+ end
13
+
14
+ def setup_all
15
+ RevelSandboxSimulator.logger.info("Setting up entities for #{config.business_type}...")
16
+
17
+ categories = setup_categories
18
+ products = setup_products(categories)
19
+ customers = setup_customers
20
+
21
+ stats = { categories: categories.size, products: products.size, customers: customers.size }
22
+ RevelSandboxSimulator.logger.info("Setup complete: #{stats}")
23
+ stats
24
+ end
25
+
26
+ def setup_categories
27
+ categories_data = data_loader.load_categories
28
+ categories_data.map do |cat_data|
29
+ result = services.product.find_or_create_category(
30
+ name: cat_data[:name],
31
+ description: cat_data[:description],
32
+ sort_order: cat_data[:sort_order]
33
+ )
34
+ RevelSandboxSimulator.logger.debug("Category: #{cat_data[:name]} -> #{result['id']}")
35
+ result
36
+ end
37
+ end
38
+
39
+ def setup_products(categories)
40
+ items_data = data_loader.load_items
41
+ category_map = categories.to_h { |c| [c["name"], c["id"]] }
42
+
43
+ items_data.map do |item_data|
44
+ category_id = category_map[item_data[:category]]
45
+ result = services.product.find_or_create_product(
46
+ name: item_data[:name],
47
+ price: item_data[:price],
48
+ category_id: category_id,
49
+ sku: item_data[:sku]
50
+ )
51
+ RevelSandboxSimulator.logger.debug("Product: #{item_data[:name]} -> #{result['id']}")
52
+ result
53
+ end
54
+ end
55
+
56
+ def setup_customers(count: 20)
57
+ services.customer.ensure_customers(count: count)
58
+ end
59
+ end
60
+ end
61
+ end