revel_sandbox_simulator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +305 -0
- data/bin/simulate +187 -0
- data/lib/revel_sandbox_simulator/configuration.rb +109 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/bar_nightclub/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/cafe_bakery/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/restaurant/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/restaurant/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/restaurant/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/data/retail_general/categories.json +7 -0
- data/lib/revel_sandbox_simulator/data/retail_general/items.json +27 -0
- data/lib/revel_sandbox_simulator/data/retail_general/tenders.json +7 -0
- data/lib/revel_sandbox_simulator/database.rb +92 -0
- data/lib/revel_sandbox_simulator/db/factories/api_requests.rb +15 -0
- data/lib/revel_sandbox_simulator/db/factories/business_types.rb +34 -0
- data/lib/revel_sandbox_simulator/db/factories/categories.rb +10 -0
- data/lib/revel_sandbox_simulator/db/factories/items.rb +12 -0
- data/lib/revel_sandbox_simulator/db/factories/simulated_orders.rb +25 -0
- data/lib/revel_sandbox_simulator/db/factories/simulated_payments.rb +14 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000001_enable_pgcrypto.rb +7 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000002_create_business_types.rb +16 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000003_create_categories.rb +16 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000004_create_items.rb +19 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000005_create_simulated_orders.rb +27 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000006_create_simulated_payments.rb +22 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000007_create_api_requests.rb +22 -0
- data/lib/revel_sandbox_simulator/db/migrate/20260313000008_create_daily_summaries.rb +21 -0
- data/lib/revel_sandbox_simulator/generators/data_loader.rb +80 -0
- data/lib/revel_sandbox_simulator/generators/entity_generator.rb +61 -0
- data/lib/revel_sandbox_simulator/generators/order_generator.rb +260 -0
- data/lib/revel_sandbox_simulator/models/api_request.rb +11 -0
- data/lib/revel_sandbox_simulator/models/business_type.rb +14 -0
- data/lib/revel_sandbox_simulator/models/category.rb +15 -0
- data/lib/revel_sandbox_simulator/models/daily_summary.rb +53 -0
- data/lib/revel_sandbox_simulator/models/item.rb +18 -0
- data/lib/revel_sandbox_simulator/models/record.rb +9 -0
- data/lib/revel_sandbox_simulator/models/simulated_order.rb +20 -0
- data/lib/revel_sandbox_simulator/models/simulated_payment.rb +16 -0
- data/lib/revel_sandbox_simulator/seeder.rb +115 -0
- data/lib/revel_sandbox_simulator/services/base_service.rb +143 -0
- data/lib/revel_sandbox_simulator/services/revel/customer_service.rb +53 -0
- data/lib/revel_sandbox_simulator/services/revel/establishment_service.rb +19 -0
- data/lib/revel_sandbox_simulator/services/revel/order_service.rb +77 -0
- data/lib/revel_sandbox_simulator/services/revel/payment_service.rb +52 -0
- data/lib/revel_sandbox_simulator/services/revel/product_service.rb +87 -0
- data/lib/revel_sandbox_simulator/services/revel/services_manager.rb +45 -0
- data/lib/revel_sandbox_simulator/version.rb +5 -0
- data/lib/revel_sandbox_simulator.rb +38 -0
- metadata +335 -0
|
@@ -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": "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": "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,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,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
|