square_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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +176 -0
  4. data/bin/simulate +388 -0
  5. data/lib/square_sandbox_simulator/configuration.rb +193 -0
  6. data/lib/square_sandbox_simulator/data/cafe_bakery/categories.json +54 -0
  7. data/lib/square_sandbox_simulator/data/cafe_bakery/combos.json +33 -0
  8. data/lib/square_sandbox_simulator/data/cafe_bakery/coupon_codes.json +133 -0
  9. data/lib/square_sandbox_simulator/data/cafe_bakery/discounts.json +113 -0
  10. data/lib/square_sandbox_simulator/data/cafe_bakery/items.json +55 -0
  11. data/lib/square_sandbox_simulator/data/cafe_bakery/modifiers.json +73 -0
  12. data/lib/square_sandbox_simulator/data/cafe_bakery/tax_rates.json +26 -0
  13. data/lib/square_sandbox_simulator/data/cafe_bakery/tenders.json +41 -0
  14. data/lib/square_sandbox_simulator/data/restaurant/categories.json +54 -0
  15. data/lib/square_sandbox_simulator/data/restaurant/combos.json +265 -0
  16. data/lib/square_sandbox_simulator/data/restaurant/coupon_codes.json +266 -0
  17. data/lib/square_sandbox_simulator/data/restaurant/discounts.json +198 -0
  18. data/lib/square_sandbox_simulator/data/restaurant/gift_cards.json +82 -0
  19. data/lib/square_sandbox_simulator/data/restaurant/items.json +388 -0
  20. data/lib/square_sandbox_simulator/data/restaurant/modifiers.json +62 -0
  21. data/lib/square_sandbox_simulator/data/restaurant/tax_rates.json +38 -0
  22. data/lib/square_sandbox_simulator/data/restaurant/tenders.json +41 -0
  23. data/lib/square_sandbox_simulator/data/salon_spa/categories.json +24 -0
  24. data/lib/square_sandbox_simulator/data/salon_spa/combos.json +88 -0
  25. data/lib/square_sandbox_simulator/data/salon_spa/coupon_codes.json +96 -0
  26. data/lib/square_sandbox_simulator/data/salon_spa/discounts.json +93 -0
  27. data/lib/square_sandbox_simulator/data/salon_spa/gift_cards.json +47 -0
  28. data/lib/square_sandbox_simulator/data/salon_spa/items.json +100 -0
  29. data/lib/square_sandbox_simulator/data/salon_spa/modifiers.json +49 -0
  30. data/lib/square_sandbox_simulator/data/salon_spa/tax_rates.json +17 -0
  31. data/lib/square_sandbox_simulator/data/salon_spa/tenders.json +41 -0
  32. data/lib/square_sandbox_simulator/database.rb +224 -0
  33. data/lib/square_sandbox_simulator/db/factories/api_requests.rb +95 -0
  34. data/lib/square_sandbox_simulator/db/factories/business_types.rb +178 -0
  35. data/lib/square_sandbox_simulator/db/factories/categories.rb +379 -0
  36. data/lib/square_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
  37. data/lib/square_sandbox_simulator/db/factories/items.rb +1526 -0
  38. data/lib/square_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
  39. data/lib/square_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
  40. data/lib/square_sandbox_simulator/db/migrate/20260312000000_enable_pgcrypto.rb +7 -0
  41. data/lib/square_sandbox_simulator/db/migrate/20260312000001_create_business_types.rb +18 -0
  42. data/lib/square_sandbox_simulator/db/migrate/20260312000002_create_categories.rb +18 -0
  43. data/lib/square_sandbox_simulator/db/migrate/20260312000003_create_items.rb +23 -0
  44. data/lib/square_sandbox_simulator/db/migrate/20260312000004_create_simulated_orders.rb +36 -0
  45. data/lib/square_sandbox_simulator/db/migrate/20260312000005_create_simulated_payments.rb +26 -0
  46. data/lib/square_sandbox_simulator/db/migrate/20260312000006_create_api_requests.rb +27 -0
  47. data/lib/square_sandbox_simulator/db/migrate/20260312000007_create_daily_summaries.rb +24 -0
  48. data/lib/square_sandbox_simulator/generators/data_loader.rb +202 -0
  49. data/lib/square_sandbox_simulator/generators/entity_generator.rb +248 -0
  50. data/lib/square_sandbox_simulator/generators/order_generator.rb +632 -0
  51. data/lib/square_sandbox_simulator/models/api_request.rb +43 -0
  52. data/lib/square_sandbox_simulator/models/business_type.rb +25 -0
  53. data/lib/square_sandbox_simulator/models/category.rb +18 -0
  54. data/lib/square_sandbox_simulator/models/daily_summary.rb +68 -0
  55. data/lib/square_sandbox_simulator/models/item.rb +33 -0
  56. data/lib/square_sandbox_simulator/models/record.rb +16 -0
  57. data/lib/square_sandbox_simulator/models/simulated_order.rb +42 -0
  58. data/lib/square_sandbox_simulator/models/simulated_payment.rb +28 -0
  59. data/lib/square_sandbox_simulator/seeder.rb +242 -0
  60. data/lib/square_sandbox_simulator/services/base_service.rb +253 -0
  61. data/lib/square_sandbox_simulator/services/square/catalog_service.rb +203 -0
  62. data/lib/square_sandbox_simulator/services/square/customer_service.rb +130 -0
  63. data/lib/square_sandbox_simulator/services/square/order_service.rb +121 -0
  64. data/lib/square_sandbox_simulator/services/square/payment_service.rb +136 -0
  65. data/lib/square_sandbox_simulator/services/square/services_manager.rb +68 -0
  66. data/lib/square_sandbox_simulator/services/square/team_service.rb +108 -0
  67. data/lib/square_sandbox_simulator/version.rb +5 -0
  68. data/lib/square_sandbox_simulator.rb +47 -0
  69. metadata +348 -0
@@ -0,0 +1,41 @@
1
+ {
2
+ "tenders": [
3
+ {
4
+ "label": "Cash",
5
+ "label_key": "com.clover.tender.cash",
6
+ "opens_cash_drawer": true,
7
+ "weight": 25
8
+ },
9
+ {
10
+ "label": "Check",
11
+ "label_key": "com.clover.tender.check",
12
+ "opens_cash_drawer": true,
13
+ "weight": 5
14
+ },
15
+ {
16
+ "label": "Gift Card",
17
+ "label_key": "com.clover.tender.external_gift_card",
18
+ "opens_cash_drawer": false,
19
+ "weight": 20
20
+ },
21
+ {
22
+ "label": "External Payment",
23
+ "label_key": "com.clover.tender.external_payment",
24
+ "opens_cash_drawer": false,
25
+ "weight": 10
26
+ },
27
+ {
28
+ "label": "Mobile Payment",
29
+ "label_key": "com.clover.tender.mobile_payment",
30
+ "opens_cash_drawer": false,
31
+ "weight": 20
32
+ },
33
+ {
34
+ "label": "Store Credit",
35
+ "label_key": "com.clover.tender.store_credit",
36
+ "opens_cash_drawer": false,
37
+ "weight": 10
38
+ }
39
+ ],
40
+ "note": "Credit Card and Debit Card are intentionally excluded - they are broken in Clover sandbox"
41
+ }
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "logger"
5
+
6
+ module SquareSandboxSimulator
7
+ # Standalone ActiveRecord connection manager for PostgreSQL.
8
+ #
9
+ # Provides database connectivity without requiring Rails.
10
+ # Used for persisting Square sandbox data (locations, orders, etc.)
11
+ # alongside the existing JSON-file and API-based workflows.
12
+ #
13
+ # When no DATABASE_URL is configured, the simulator falls back
14
+ # to its original JSON-file behaviour -- call Database.connected?
15
+ # to check before attempting DB operations.
16
+ #
17
+ # @example Connect and run migrations
18
+ # SquareSandboxSimulator::Database.connect!("postgres://localhost:5432/square_simulator_development")
19
+ # SquareSandboxSimulator::Database.migrate!
20
+ #
21
+ # @example Check availability for JSON fallback
22
+ # if SquareSandboxSimulator::Database.connected?
23
+ # # use ActiveRecord models
24
+ # else
25
+ # # fall back to JSON files
26
+ # end
27
+ module Database
28
+ # Directory containing ActiveRecord migration files
29
+ MIGRATIONS_PATH = File.expand_path("db/migrate", __dir__).freeze
30
+
31
+ # Default test database name
32
+ TEST_DATABASE = "square_simulator_test"
33
+
34
+ class << self
35
+ # Create the database specified in the connection URL.
36
+ #
37
+ # Connects to the `postgres` maintenance database, issues
38
+ # CREATE DATABASE, then disconnects.
39
+ #
40
+ # @param url [String] PostgreSQL connection URL
41
+ # @return [void]
42
+ def create!(url)
43
+ db_name = URI.parse(url).path.delete_prefix("/")
44
+ maintenance_url = url.sub(%r{/[^/]+\z}, "/postgres")
45
+
46
+ ActiveRecord::Base.establish_connection(maintenance_url)
47
+ ActiveRecord::Base.connection.create_database(db_name)
48
+ SquareSandboxSimulator.logger.info("Database created: #{db_name}")
49
+ rescue ActiveRecord::DatabaseAlreadyExists, ActiveRecord::StatementInvalid => e
50
+ raise unless e.message.include?("already exists")
51
+
52
+ SquareSandboxSimulator.logger.info("Database already exists: #{db_name}")
53
+ ensure
54
+ ActiveRecord::Base.connection_pool.disconnect!
55
+ end
56
+
57
+ # Drop the database specified in the connection URL.
58
+ #
59
+ # @param url [String] PostgreSQL connection URL
60
+ # @return [void]
61
+ def drop!(url)
62
+ db_name = URI.parse(url).path.delete_prefix("/")
63
+ maintenance_url = url.sub(%r{/[^/]+\z}, "/postgres")
64
+
65
+ ActiveRecord::Base.establish_connection(maintenance_url)
66
+ ActiveRecord::Base.connection.drop_database(db_name)
67
+ SquareSandboxSimulator.logger.info("Database dropped: #{db_name}")
68
+ rescue ActiveRecord::StatementInvalid => e
69
+ raise unless e.message.include?("does not exist")
70
+
71
+ SquareSandboxSimulator.logger.info("Database does not exist: #{db_name}")
72
+ ensure
73
+ ActiveRecord::Base.connection_pool.disconnect!
74
+ end
75
+
76
+ # Return the configured database URL from .env.json.
77
+ #
78
+ # @return [String] PostgreSQL URL
79
+ # @raise [Error] if no DATABASE_URL is configured
80
+ def database_url
81
+ url = Configuration.database_url_from_file
82
+ raise Error, "No DATABASE_URL found in .env.json" unless url
83
+
84
+ url
85
+ end
86
+
87
+ # Establish a standalone ActiveRecord connection to PostgreSQL.
88
+ #
89
+ # @param url [String] A PostgreSQL connection URL
90
+ # (e.g. "postgres://user:pass@localhost:5432/square_simulator_development")
91
+ # @return [void]
92
+ # @raise [ArgumentError] if the URL is not a PostgreSQL URL
93
+ # @raise [ActiveRecord::ConnectionNotEstablished] if the connection fails
94
+ def connect!(url)
95
+ unless url.match?(%r{\Apostgres(ql)?://}i)
96
+ raise ArgumentError, "Expected a PostgreSQL URL (postgres:// or postgresql://), got: #{url.split("://").first}://"
97
+ end
98
+
99
+ ActiveRecord::Base.establish_connection(url)
100
+
101
+ # Verify the connection is actually usable
102
+ ActiveRecord::Base.connection.execute("SELECT 1")
103
+
104
+ ActiveRecord::Base.logger = SquareSandboxSimulator.logger
105
+
106
+ SquareSandboxSimulator.logger.info("Database connected: #{sanitize_url(url)}")
107
+ end
108
+
109
+ # Run pending migrations from lib/square_sandbox_simulator/db/migrate/.
110
+ #
111
+ # Uses ActiveRecord::MigrationContext for standalone (non-Rails) usage.
112
+ #
113
+ # @return [void]
114
+ def migrate!
115
+ ensure_connected!
116
+
117
+ SquareSandboxSimulator.logger.info("Running migrations from #{MIGRATIONS_PATH}")
118
+
119
+ context = ActiveRecord::MigrationContext.new(MIGRATIONS_PATH)
120
+ context.migrate
121
+
122
+ SquareSandboxSimulator.logger.info("Migrations complete")
123
+ end
124
+
125
+ # Seed the database with realistic Square data using FactoryBot.
126
+ #
127
+ # Delegates to {Seeder} which creates BusinessTypes, Categories,
128
+ # and Items using factory attributes. Idempotent -- safe to call
129
+ # multiple times without creating duplicates.
130
+ #
131
+ # @param business_type [Symbol, String, nil] Optional business type
132
+ # (e.g. :restaurant, :retail_clothing). Seeds all types if nil.
133
+ # @return [Hash] Summary counts (:business_types, :categories, :items)
134
+ def seed!(business_type: nil)
135
+ ensure_connected!
136
+ load_factories!
137
+
138
+ # Default to configured business_type if none specified;
139
+ # pass nil to Seeder to seed ALL types when config is also nil.
140
+ business_type ||= SquareSandboxSimulator.configuration.business_type
141
+ Seeder.seed!(business_type: business_type)
142
+ end
143
+
144
+ # Check whether a database connection is established and usable.
145
+ #
146
+ # Safe to call at any time -- returns false rather than raising
147
+ # so callers can decide to fall back to JSON files.
148
+ #
149
+ # @return [Boolean]
150
+ def connected?
151
+ ActiveRecord::Base.connection_pool.with_connection(&:active?)
152
+ rescue StandardError
153
+ false
154
+ end
155
+
156
+ # Disconnect from the database and clean up the connection pool.
157
+ #
158
+ # @return [void]
159
+ def disconnect!
160
+ ActiveRecord::Base.connection_pool.disconnect!
161
+ SquareSandboxSimulator.logger.info("Database disconnected")
162
+ end
163
+
164
+ # Build the test database URL.
165
+ #
166
+ # @param base_url [String, nil] Base URL to derive test URL from.
167
+ # If nil, reads DATABASE_URL from .env.json and swaps the DB name.
168
+ # @return [String] PostgreSQL URL pointing to the test database
169
+ def test_database_url(base_url: nil)
170
+ url = base_url || Configuration.database_url_from_file
171
+ return "postgres://localhost:5432/#{TEST_DATABASE}" if url.nil?
172
+
173
+ # Replace the database name in the URL with the test database name
174
+ uri = URI.parse(url)
175
+ uri.path = "/#{TEST_DATABASE}"
176
+ uri.to_s
177
+ rescue URI::InvalidURIError
178
+ "postgres://localhost:5432/#{TEST_DATABASE}"
179
+ end
180
+
181
+ private
182
+
183
+ # Raise if no database connection has been established yet.
184
+ #
185
+ # @raise [SquareSandboxSimulator::Error] when not connected
186
+ def ensure_connected!
187
+ return if connected?
188
+
189
+ raise SquareSandboxSimulator::Error,
190
+ "Database not connected. Call Database.connect!(url) first."
191
+ end
192
+
193
+ # Load FactoryBot factory definitions from the factories directory.
194
+ # Guarded against repeated calls to avoid "Factory already registered" errors.
195
+ #
196
+ # @return [void]
197
+ def load_factories!
198
+ return if @factories_loaded
199
+
200
+ require "factory_bot"
201
+
202
+ factories_path = File.expand_path("db/factories", __dir__)
203
+ FactoryBot.definition_file_paths = [factories_path] if Dir.exist?(factories_path)
204
+ FactoryBot.find_definitions
205
+ @factories_loaded = true
206
+ rescue StandardError => e
207
+ SquareSandboxSimulator.logger.warn("Could not load factories: #{e.message}")
208
+ end
209
+
210
+ # Strip credentials from a database URL for safe logging.
211
+ #
212
+ # @param url [String]
213
+ # @return [String]
214
+ def sanitize_url(url)
215
+ uri = URI.parse(url)
216
+ uri.password = "***" if uri.password
217
+ uri.user = "***" if uri.user
218
+ uri.to_s
219
+ rescue URI::InvalidURIError
220
+ url.gsub(%r{://[^@]+@}, "://***:***@")
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :api_request, class: "SquareSandboxSimulator::Models::ApiRequest" do
5
+ http_method { "GET" }
6
+ sequence(:url) { |n| "https://connect.squareupsandbox.com/v2/orders/ORDER#{n}" }
7
+ response_status { 200 }
8
+ duration_ms { 150 }
9
+ resource_type { "Order" }
10
+ request_payload { {} }
11
+ response_payload { {} }
12
+
13
+ # ── HTTP method traits ─────────────────────────────────────
14
+
15
+ trait :get do
16
+ http_method { "GET" }
17
+ response_status { 200 }
18
+ end
19
+
20
+ trait :post do
21
+ http_method { "POST" }
22
+ response_status { 201 }
23
+ request_payload { { name: "New Item", price: 999 } }
24
+ end
25
+
26
+ trait :put do
27
+ http_method { "PUT" }
28
+ response_status { 200 }
29
+ request_payload { { name: "Updated Item" } }
30
+ end
31
+
32
+ trait :delete do
33
+ http_method { "DELETE" }
34
+ response_status { 204 }
35
+ response_payload { {} }
36
+ end
37
+
38
+ # ── Status traits ──────────────────────────────────────────
39
+
40
+ trait :error do
41
+ http_method { "POST" }
42
+ response_status { 500 }
43
+ error_message { "Internal Server Error" }
44
+ response_payload { { message: "Internal Server Error" } }
45
+ end
46
+
47
+ trait :not_found do
48
+ response_status { 404 }
49
+ error_message { "Not Found" }
50
+ response_payload { { message: "Not Found" } }
51
+ end
52
+
53
+ trait :rate_limited do
54
+ response_status { 429 }
55
+ error_message { "Too Many Requests" }
56
+ response_payload { { message: "Rate limit exceeded. Retry after 60s." } }
57
+ end
58
+
59
+ trait :unauthorized do
60
+ response_status { 401 }
61
+ error_message { "Unauthorized" }
62
+ response_payload { { message: "Invalid API token" } }
63
+ end
64
+
65
+ # ── Performance traits ─────────────────────────────────────
66
+
67
+ trait :slow do
68
+ duration_ms { 2500 }
69
+ end
70
+
71
+ trait :fast do
72
+ duration_ms { 25 }
73
+ end
74
+
75
+ # ── Resource traits ────────────────────────────────────────
76
+
77
+ trait :order_resource do
78
+ resource_type { "Order" }
79
+ sequence(:resource_id) { |n| "ORDER#{n}" }
80
+ sequence(:url) { |n| "https://connect.squareupsandbox.com/v2/orders/ORDER#{n}" }
81
+ end
82
+
83
+ trait :item_resource do
84
+ resource_type { "Item" }
85
+ sequence(:resource_id) { |n| "ITEM#{n}" }
86
+ sequence(:url) { |n| "https://connect.squareupsandbox.com/v2/catalog/object/ITEM#{n}" }
87
+ end
88
+
89
+ trait :payment_resource do
90
+ resource_type { "Payment" }
91
+ sequence(:resource_id) { |n| "PAY#{n}" }
92
+ sequence(:url) { |n| "https://connect.squareupsandbox.com/v2/payments/PAY#{n}" }
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :business_type, class: "SquareSandboxSimulator::Models::BusinessType" do
5
+ sequence(:key) { |n| "business_type_#{n}" }
6
+ name { "Test Business Type" }
7
+ industry { "food" }
8
+ order_profile { {} }
9
+
10
+ # ── Food (6) ──────────────────────────────────────────────
11
+
12
+ trait :restaurant do
13
+ key { "restaurant" }
14
+ name { "Restaurant" }
15
+ industry { "food" }
16
+ description { "Full-service casual dining restaurant" }
17
+ order_profile do
18
+ {
19
+ "avg_order_value_cents" => 2500,
20
+ "avg_items_per_order" => 3,
21
+ "peak_hours" => %w[11:30 12:30 18:00 19:30],
22
+ "meal_periods" => %w[breakfast lunch dinner],
23
+ "dining_options" => %w[HERE TO_GO DELIVERY],
24
+ "tip_percentage_range" => [15, 25],
25
+ "tax_rate" => 8.875,
26
+ }
27
+ end
28
+ end
29
+
30
+ trait :cafe_bakery do
31
+ key { "cafe_bakery" }
32
+ name { "Cafe & Bakery" }
33
+ industry { "food" }
34
+ description { "Coffee shop with fresh-baked pastries and light fare" }
35
+ order_profile do
36
+ {
37
+ "avg_order_value_cents" => 1200,
38
+ "avg_items_per_order" => 2,
39
+ "peak_hours" => %w[07:00 08:30 12:00],
40
+ "meal_periods" => %w[breakfast lunch],
41
+ "dining_options" => %w[HERE TO_GO],
42
+ "tip_percentage_range" => [10, 20],
43
+ "tax_rate" => 8.875,
44
+ }
45
+ end
46
+ end
47
+
48
+ trait :bar_nightclub do
49
+ key { "bar_nightclub" }
50
+ name { "Bar & Nightclub" }
51
+ industry { "food" }
52
+ description { "Full bar with craft cocktails, draft beer, and late-night bites" }
53
+ order_profile do
54
+ {
55
+ "avg_order_value_cents" => 3500,
56
+ "avg_items_per_order" => 4,
57
+ "peak_hours" => %w[17:00 21:00 23:00],
58
+ "meal_periods" => %w[dinner late_night],
59
+ "dining_options" => %w[HERE],
60
+ "tip_percentage_range" => [18, 25],
61
+ "tax_rate" => 8.875,
62
+ }
63
+ end
64
+ end
65
+
66
+ trait :food_truck do
67
+ key { "food_truck" }
68
+ name { "Food Truck" }
69
+ industry { "food" }
70
+ description { "Mobile street food — tacos, burritos, and Mexican fare" }
71
+ order_profile do
72
+ {
73
+ "avg_order_value_cents" => 1400,
74
+ "avg_items_per_order" => 3,
75
+ "peak_hours" => %w[11:30 12:30 18:00],
76
+ "meal_periods" => %w[lunch dinner],
77
+ "dining_options" => %w[TO_GO],
78
+ "tip_percentage_range" => [10, 20],
79
+ "tax_rate" => 8.875,
80
+ }
81
+ end
82
+ end
83
+
84
+ trait :fine_dining do
85
+ key { "fine_dining" }
86
+ name { "Fine Dining" }
87
+ industry { "food" }
88
+ description { "Upscale prix-fixe and a la carte dining experience" }
89
+ order_profile do
90
+ {
91
+ "avg_order_value_cents" => 12_000,
92
+ "avg_items_per_order" => 4,
93
+ "peak_hours" => %w[18:00 19:30 20:30],
94
+ "meal_periods" => %w[dinner],
95
+ "dining_options" => %w[HERE],
96
+ "tip_percentage_range" => [20, 30],
97
+ "tax_rate" => 8.875,
98
+ }
99
+ end
100
+ end
101
+
102
+ trait :pizzeria do
103
+ key { "pizzeria" }
104
+ name { "Pizzeria" }
105
+ industry { "food" }
106
+ description { "Pizza shop with classic and specialty pies, calzones, and sides" }
107
+ order_profile do
108
+ {
109
+ "avg_order_value_cents" => 2200,
110
+ "avg_items_per_order" => 3,
111
+ "peak_hours" => %w[12:00 18:00 20:00],
112
+ "meal_periods" => %w[lunch dinner],
113
+ "dining_options" => %w[HERE TO_GO DELIVERY],
114
+ "tip_percentage_range" => [12, 20],
115
+ "tax_rate" => 8.875,
116
+ }
117
+ end
118
+ end
119
+
120
+ # ── Retail (2) ────────────────────────────────────────────
121
+
122
+ trait :retail_clothing do
123
+ key { "retail_clothing" }
124
+ name { "Clothing Store" }
125
+ industry { "retail" }
126
+ description { "Casual wear and accessories with size/color variants" }
127
+ order_profile do
128
+ {
129
+ "avg_order_value_cents" => 7500,
130
+ "avg_items_per_order" => 2,
131
+ "peak_hours" => %w[11:00 14:00 17:00],
132
+ "meal_periods" => [],
133
+ "dining_options" => [],
134
+ "tip_percentage_range" => [0, 0],
135
+ "tax_rate" => 8.875,
136
+ }
137
+ end
138
+ end
139
+
140
+ trait :retail_general do
141
+ key { "retail_general" }
142
+ name { "General Store" }
143
+ industry { "retail" }
144
+ description { "Everyday essentials — electronics, home goods, personal care" }
145
+ order_profile do
146
+ {
147
+ "avg_order_value_cents" => 3500,
148
+ "avg_items_per_order" => 3,
149
+ "peak_hours" => %w[10:00 13:00 17:00],
150
+ "meal_periods" => [],
151
+ "dining_options" => [],
152
+ "tip_percentage_range" => [0, 0],
153
+ "tax_rate" => 8.875,
154
+ }
155
+ end
156
+ end
157
+
158
+ # ── Services (1) ──────────────────────────────────────────
159
+
160
+ trait :salon_spa do
161
+ key { "salon_spa" }
162
+ name { "Salon & Spa" }
163
+ industry { "service" }
164
+ description { "Full-service hair salon, spa treatments, and nail services" }
165
+ order_profile do
166
+ {
167
+ "avg_order_value_cents" => 8500,
168
+ "avg_items_per_order" => 2,
169
+ "peak_hours" => %w[10:00 13:00 16:00],
170
+ "meal_periods" => [],
171
+ "dining_options" => [],
172
+ "tip_percentage_range" => [15, 25],
173
+ "tax_rate" => 0.0,
174
+ }
175
+ end
176
+ end
177
+ end
178
+ end