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,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SquareSandboxSimulator
4
+ class Configuration
5
+ attr_accessor :location_id, :location_name, :access_token, :environment, :square_version,
6
+ :log_level, :tax_rate, :business_type, :database_url, :location_timezone
7
+
8
+ # Default timezone if not fetched from Square
9
+ DEFAULT_TIMEZONE = "America/Los_Angeles"
10
+
11
+ # Path to locations JSON file
12
+ LOCATIONS_FILE = File.join(File.dirname(__FILE__), "..", "..", ".env.json")
13
+
14
+ def initialize
15
+ @location_id = ENV.fetch("SQUARE_LOCATION_ID", nil)
16
+ @location_name = ENV.fetch("SQUARE_LOCATION_NAME", nil)
17
+ @access_token = ENV.fetch("SQUARE_ACCESS_TOKEN", nil)
18
+ @environment = normalize_url(ENV.fetch("SQUARE_ENVIRONMENT", "https://connect.squareupsandbox.com/"))
19
+ @square_version = ENV.fetch("SQUARE_VERSION", "2024-01-18")
20
+ @log_level = parse_log_level(ENV.fetch("LOG_LEVEL", "INFO"))
21
+ @tax_rate = ENV.fetch("TAX_RATE", "8.25").to_f
22
+ @business_type = ENV.fetch("BUSINESS_TYPE", "restaurant").to_sym
23
+ @database_url = ENV.fetch("DATABASE_URL", nil)
24
+
25
+ # Load from .env.json if location_id not set in ENV
26
+ load_from_locations_file if @location_id.nil? || @location_id.empty?
27
+ end
28
+
29
+ # Load configuration for a specific location from .env.json
30
+ #
31
+ # @param location_id [String, nil] Location ID to load (nil for first location)
32
+ # @param index [Integer, nil] Index of location in the list (0-based)
33
+ # @return [self]
34
+ def load_location(location_id: nil, index: nil)
35
+ locations = load_locations_file
36
+ return self if locations.empty?
37
+
38
+ location = if location_id
39
+ locations.find { |l| l["SQUARE_LOCATION_ID"] == location_id }
40
+ elsif index
41
+ locations[index]
42
+ else
43
+ locations.first
44
+ end
45
+
46
+ if location
47
+ apply_location_config(location)
48
+ logger.info "Loaded location: #{@location_name} (#{@location_id})"
49
+ else
50
+ logger.warn "Location not found: #{location_id || "index #{index}"}"
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ # List all available locations from .env.json
57
+ #
58
+ # @return [Array<Hash>] Array of location configs
59
+ def available_locations
60
+ load_locations_file.map do |l|
61
+ {
62
+ id: l["SQUARE_LOCATION_ID"],
63
+ name: l["SQUARE_LOCATION_NAME"],
64
+ }
65
+ end
66
+ end
67
+
68
+ def validate!
69
+ raise ConfigurationError, "SQUARE_LOCATION_ID is required" if location_id.nil? || location_id.empty?
70
+ raise ConfigurationError, "SQUARE_ACCESS_TOKEN is required" if access_token.nil? || access_token.empty?
71
+
72
+ true
73
+ end
74
+
75
+ def logger
76
+ @logger ||= Logger.new($stdout).tap do |log|
77
+ log.level = @log_level
78
+ log.formatter = proc do |severity, datetime, _progname, msg|
79
+ timestamp = datetime.strftime("%Y-%m-%d %H:%M:%S")
80
+ "[#{timestamp}] #{severity.ljust(5)} | #{msg}\n"
81
+ end
82
+ end
83
+ end
84
+
85
+ # Fetch location timezone from Square API
86
+ # @return [String] IANA timezone identifier (e.g., "America/Los_Angeles")
87
+ def fetch_location_timezone
88
+ return @location_timezone if @location_timezone
89
+
90
+ require "rest-client"
91
+ require "json"
92
+
93
+ url = "#{environment}v2/locations/#{location_id}"
94
+ response = RestClient.get(url, {
95
+ Authorization: "Bearer #{access_token}",
96
+ "Square-Version": square_version,
97
+ "Content-Type": "application/json",
98
+ })
99
+ data = JSON.parse(response.body)
100
+
101
+ @location_timezone = data.dig("location", "timezone") || DEFAULT_TIMEZONE
102
+ logger.info "Location timezone: #{@location_timezone}"
103
+ @location_timezone
104
+ rescue StandardError => e
105
+ logger.warn "Failed to fetch location timezone: #{e.message}. Using default: #{DEFAULT_TIMEZONE}"
106
+ @location_timezone = DEFAULT_TIMEZONE
107
+ end
108
+
109
+ # Get current time in location's timezone
110
+ # @return [Time] Current time in location timezone
111
+ def merchant_time_now
112
+ require "time"
113
+ tz = fetch_location_timezone
114
+ begin
115
+ require "tzinfo"
116
+ TZInfo::Timezone.get(tz).now
117
+ rescue LoadError
118
+ old_tz = ENV.fetch("TZ", nil)
119
+ ENV["TZ"] = tz
120
+ time = Time.now
121
+ ENV["TZ"] = old_tz
122
+ time
123
+ end
124
+ end
125
+
126
+ # Get today's date in location's timezone
127
+ # @return [Date] Today's date in location timezone
128
+ def merchant_date_today
129
+ merchant_time_now.to_date
130
+ end
131
+
132
+ private
133
+
134
+ def load_from_locations_file
135
+ locations = load_locations_file
136
+ return if locations.empty?
137
+
138
+ # Use first location by default
139
+ apply_location_config(locations.first)
140
+ end
141
+
142
+ # Parse .env.json, supporting both the legacy array format and the
143
+ # new object format: { "DATABASE_URL": "...", "locations": [...] }
144
+ def load_locations_file
145
+ return [] unless File.exist?(LOCATIONS_FILE)
146
+
147
+ data = JSON.parse(File.read(LOCATIONS_FILE))
148
+ return data if data.is_a?(Array) # legacy format
149
+
150
+ # New object format -- extract locations list
151
+ data.fetch("locations", [])
152
+ rescue JSON::ParserError => e
153
+ warn "Failed to parse #{LOCATIONS_FILE}: #{e.message}"
154
+ []
155
+ end
156
+
157
+ # Read the top-level DATABASE_URL from .env.json (new object format only).
158
+ # Returns nil when the file uses the legacy array format or has no key.
159
+ #
160
+ # @return [String, nil] The DATABASE_URL or nil
161
+ def self.database_url_from_file
162
+ return nil unless File.exist?(LOCATIONS_FILE)
163
+
164
+ data = JSON.parse(File.read(LOCATIONS_FILE))
165
+ return nil if data.is_a?(Array)
166
+
167
+ data["DATABASE_URL"]
168
+ rescue JSON::ParserError
169
+ nil
170
+ end
171
+
172
+ def apply_location_config(location)
173
+ @location_id = location["SQUARE_LOCATION_ID"]
174
+ @location_name = location["SQUARE_LOCATION_NAME"]
175
+ @access_token = location["SQUARE_ACCESS_TOKEN"] unless location["SQUARE_ACCESS_TOKEN"].to_s.empty?
176
+ end
177
+
178
+ def normalize_url(url)
179
+ url = url.strip
180
+ url.end_with?("/") ? url : "#{url}/"
181
+ end
182
+
183
+ def parse_log_level(level)
184
+ case level.to_s.upcase
185
+ when "DEBUG" then Logger::DEBUG
186
+ when "WARN" then Logger::WARN
187
+ when "ERROR" then Logger::ERROR
188
+ when "FATAL" then Logger::FATAL
189
+ else Logger::INFO # default for "INFO" and unrecognized levels
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,54 @@
1
+ {
2
+ "categories": [
3
+ {
4
+ "name": "Coffee & Espresso",
5
+ "sort_order": 1,
6
+ "description": "Hot and cold coffee drinks"
7
+ },
8
+ {
9
+ "name": "Pastries & Baked Goods",
10
+ "sort_order": 2,
11
+ "description": "Freshly baked daily"
12
+ },
13
+ {
14
+ "name": "Breakfast",
15
+ "sort_order": 3,
16
+ "description": "Morning plates and bowls"
17
+ },
18
+ {
19
+ "name": "Sandwiches & Wraps",
20
+ "sort_order": 4,
21
+ "description": "Lunch staples"
22
+ },
23
+ {
24
+ "name": "Smoothies & Juices",
25
+ "sort_order": 5,
26
+ "description": "Blended drinks and fresh-pressed juices"
27
+ },
28
+ {
29
+ "name": "Coffee & Espresso",
30
+ "sort_order": 6,
31
+ "description": "More coffee drinks (duplicate category)"
32
+ },
33
+ {
34
+ "name": "Pastries & Baked Goods",
35
+ "sort_order": 7,
36
+ "description": "Pastries imported from integration"
37
+ },
38
+ {
39
+ "name": "Breakfast",
40
+ "sort_order": 8,
41
+ "description": "Weekend brunch specials (stale copy)"
42
+ },
43
+ {
44
+ "name": "Drinks",
45
+ "sort_order": 9,
46
+ "description": "All drinks catch-all"
47
+ },
48
+ {
49
+ "name": "Grab & Go",
50
+ "sort_order": 10,
51
+ "description": "Pre-packaged items for takeout"
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "combos": [
3
+ {
4
+ "id": "coffee_pastry_combo",
5
+ "name": "Coffee & Pastry Combo",
6
+ "description": "Any coffee + any pastry for $5.99",
7
+ "combo_price": 599,
8
+ "required_categories": ["Coffee & Espresso", "Pastries & Baked Goods"],
9
+ "max_items_per_category": 1,
10
+ "active": true
11
+ },
12
+ {
13
+ "id": "breakfast_combo",
14
+ "name": "Breakfast Combo",
15
+ "description": "Any breakfast plate + any coffee or juice for $12.99",
16
+ "combo_price": 1299,
17
+ "required_categories": ["Breakfast", "Coffee & Espresso"],
18
+ "alternative_categories": { "Coffee & Espresso": ["Smoothies & Juices"] },
19
+ "max_items_per_category": 1,
20
+ "active": true
21
+ },
22
+ {
23
+ "id": "lunch_combo",
24
+ "name": "Lunch Deal",
25
+ "description": "Any sandwich + any drink for $13.99",
26
+ "combo_price": 1399,
27
+ "required_categories": ["Sandwiches & Wraps", "Drinks"],
28
+ "alternative_categories": { "Drinks": ["Smoothies & Juices", "Coffee & Espresso"] },
29
+ "max_items_per_category": 1,
30
+ "active": true
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "coupon_codes": [
3
+ {
4
+ "code": "COFFEE10",
5
+ "name": "Coffee Lover",
6
+ "description": "10% off any coffee drink",
7
+ "discount_type": "percentage",
8
+ "discount_value": 10,
9
+ "min_order_amount": 0,
10
+ "max_discount_amount": null,
11
+ "valid_from": "2024-01-01T00:00:00Z",
12
+ "valid_until": "2027-12-31T23:59:59Z",
13
+ "usage_limit": null,
14
+ "usage_count": 0,
15
+ "single_use_per_customer": false,
16
+ "stackable": false,
17
+ "applicable_categories": ["Coffee & Espresso"],
18
+ "excluded_categories": null,
19
+ "active": true
20
+ },
21
+ {
22
+ "code": "WELCOME20",
23
+ "name": "New Customer Welcome",
24
+ "description": "20% off for new customers",
25
+ "discount_type": "percentage",
26
+ "discount_value": 20,
27
+ "min_order_amount": 0,
28
+ "max_discount_amount": 1000,
29
+ "valid_from": "2024-01-01T00:00:00Z",
30
+ "valid_until": "2027-12-31T23:59:59Z",
31
+ "usage_limit": null,
32
+ "usage_count": 0,
33
+ "single_use_per_customer": true,
34
+ "new_customers_only": true,
35
+ "stackable": false,
36
+ "applicable_categories": null,
37
+ "excluded_categories": null,
38
+ "active": true
39
+ },
40
+ {
41
+ "code": "FIVER",
42
+ "name": "$5 Off",
43
+ "description": "$5 off orders $20 or more",
44
+ "discount_type": "fixed",
45
+ "discount_value": 500,
46
+ "min_order_amount": 2000,
47
+ "max_discount_amount": null,
48
+ "valid_from": "2024-01-01T00:00:00Z",
49
+ "valid_until": "2027-12-31T23:59:59Z",
50
+ "usage_limit": null,
51
+ "usage_count": 0,
52
+ "single_use_per_customer": false,
53
+ "stackable": false,
54
+ "applicable_categories": null,
55
+ "excluded_categories": null,
56
+ "active": true
57
+ },
58
+ {
59
+ "code": "BRUNCH15",
60
+ "name": "Brunch Special",
61
+ "description": "15% off breakfast items on weekends",
62
+ "discount_type": "percentage",
63
+ "discount_value": 15,
64
+ "min_order_amount": 0,
65
+ "max_discount_amount": null,
66
+ "valid_from": "2024-01-01T00:00:00Z",
67
+ "valid_until": "2027-12-31T23:59:59Z",
68
+ "usage_limit": null,
69
+ "usage_count": 0,
70
+ "single_use_per_customer": false,
71
+ "day_restricted": true,
72
+ "valid_days": [0, 6],
73
+ "stackable": false,
74
+ "applicable_categories": ["Breakfast"],
75
+ "excluded_categories": null,
76
+ "active": true
77
+ },
78
+ {
79
+ "code": "FREEPASTRY",
80
+ "name": "Free Pastry",
81
+ "description": "Free pastry with any coffee order over $5",
82
+ "discount_type": "percentage",
83
+ "discount_value": 100,
84
+ "min_order_amount": 500,
85
+ "max_discount_amount": 499,
86
+ "valid_from": "2024-01-01T00:00:00Z",
87
+ "valid_until": "2027-12-31T23:59:59Z",
88
+ "usage_limit": 200,
89
+ "usage_count": 34,
90
+ "single_use_per_customer": true,
91
+ "stackable": false,
92
+ "applicable_categories": ["Pastries & Baked Goods"],
93
+ "excluded_categories": null,
94
+ "active": true
95
+ },
96
+ {
97
+ "code": "EXPIRED10",
98
+ "name": "Expired Code",
99
+ "description": "This code has expired (for testing)",
100
+ "discount_type": "percentage",
101
+ "discount_value": 10,
102
+ "min_order_amount": 0,
103
+ "max_discount_amount": null,
104
+ "valid_from": "2023-01-01T00:00:00Z",
105
+ "valid_until": "2023-12-31T23:59:59Z",
106
+ "usage_limit": null,
107
+ "usage_count": 0,
108
+ "single_use_per_customer": false,
109
+ "stackable": false,
110
+ "applicable_categories": null,
111
+ "excluded_categories": null,
112
+ "active": true
113
+ },
114
+ {
115
+ "code": "INACTIVE",
116
+ "name": "Inactive Code",
117
+ "description": "This code is inactive (for testing)",
118
+ "discount_type": "percentage",
119
+ "discount_value": 10,
120
+ "min_order_amount": 0,
121
+ "max_discount_amount": null,
122
+ "valid_from": "2024-01-01T00:00:00Z",
123
+ "valid_until": "2027-12-31T23:59:59Z",
124
+ "usage_limit": null,
125
+ "usage_count": 0,
126
+ "single_use_per_customer": false,
127
+ "stackable": false,
128
+ "applicable_categories": null,
129
+ "excluded_categories": null,
130
+ "active": false
131
+ }
132
+ ]
133
+ }
@@ -0,0 +1,113 @@
1
+ {
2
+ "discounts": [
3
+ {
4
+ "id": "morning_rush",
5
+ "name": "Morning Rush",
6
+ "percentage": 10,
7
+ "description": "10% off before 8 AM",
8
+ "type": "time_based",
9
+ "time_rules": { "start_hour": 6, "end_hour": 8 },
10
+ "auto_apply": true
11
+ },
12
+ {
13
+ "id": "happy_hour_cafe",
14
+ "name": "Afternoon Happy Hour",
15
+ "percentage": 15,
16
+ "description": "15% off drinks from 2-4 PM",
17
+ "type": "time_based",
18
+ "time_rules": { "start_hour": 14, "end_hour": 16 },
19
+ "auto_apply": true
20
+ },
21
+ {
22
+ "id": "student_discount",
23
+ "name": "Student Discount",
24
+ "percentage": 10,
25
+ "description": "10% off with student ID",
26
+ "type": "customer",
27
+ "auto_apply": false
28
+ },
29
+ {
30
+ "id": "employee_discount",
31
+ "name": "Employee Discount",
32
+ "percentage": 30,
33
+ "description": "30% off for employees",
34
+ "type": "customer",
35
+ "auto_apply": false
36
+ },
37
+ {
38
+ "id": "two_off_pastry",
39
+ "name": "$2 Off Pastry",
40
+ "amount": 200,
41
+ "description": "$2 off any pastry with coffee purchase",
42
+ "type": "line_item",
43
+ "applicable_categories": ["Pastries & Baked Goods"],
44
+ "requires_category": ["Coffee & Espresso"],
45
+ "auto_apply": false
46
+ },
47
+ {
48
+ "id": "five_off",
49
+ "name": "$5 Off",
50
+ "amount": 500,
51
+ "description": "$5 off orders over $25",
52
+ "type": "threshold",
53
+ "min_order_amount": 2500,
54
+ "auto_apply": false
55
+ },
56
+ {
57
+ "id": "ten_off",
58
+ "name": "$10 Off",
59
+ "amount": 1000,
60
+ "description": "$10 off orders over $50",
61
+ "type": "threshold",
62
+ "min_order_amount": 5000,
63
+ "auto_apply": false
64
+ },
65
+ {
66
+ "id": "loyalty_bronze",
67
+ "name": "Loyalty - Bronze",
68
+ "percentage": 5,
69
+ "description": "5% off for 10+ visits",
70
+ "type": "loyalty",
71
+ "min_visits": 10,
72
+ "auto_apply": true
73
+ },
74
+ {
75
+ "id": "loyalty_silver",
76
+ "name": "Loyalty - Silver",
77
+ "percentage": 10,
78
+ "description": "10% off for 25+ visits",
79
+ "type": "loyalty",
80
+ "min_visits": 25,
81
+ "auto_apply": true
82
+ },
83
+ {
84
+ "id": "loyalty_gold",
85
+ "name": "Loyalty - Gold",
86
+ "percentage": 15,
87
+ "description": "15% off for 50+ visits",
88
+ "type": "loyalty",
89
+ "min_visits": 50,
90
+ "auto_apply": true
91
+ },
92
+ {
93
+ "id": "first_order",
94
+ "name": "First Order Discount",
95
+ "percentage": 15,
96
+ "description": "15% off your first order",
97
+ "type": "loyalty",
98
+ "min_visits": 0,
99
+ "max_visits": 1,
100
+ "auto_apply": true
101
+ },
102
+ {
103
+ "id": "smoothie_half_off",
104
+ "name": "Smoothie 50% Off",
105
+ "percentage": 50,
106
+ "description": "50% off any smoothie with breakfast purchase",
107
+ "type": "line_item",
108
+ "applicable_categories": ["Smoothies & Juices"],
109
+ "requires_category": ["Breakfast"],
110
+ "auto_apply": false
111
+ }
112
+ ]
113
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "items": [
3
+ { "name": "House Drip Coffee", "price": 299, "category": "Coffee & Espresso", "category_sort_order": 1, "sku": "CAFE-COF-001" },
4
+ { "name": "Espresso", "price": 349, "category": "Coffee & Espresso", "category_sort_order": 1, "sku": "CAFE-COF-002" },
5
+ { "name": "Cappuccino", "price": 499, "category": "Coffee & Espresso", "category_sort_order": 1, "sku": "CAFE-COF-003" },
6
+ { "name": "Latte", "price": 549, "category": "Coffee & Espresso", "category_sort_order": 1, "sku": "CAFE-COF-004" },
7
+ { "name": "Cold Brew", "price": 499, "category": "Coffee & Espresso", "category_sort_order": 1, "sku": "CAFE-COF-005" },
8
+
9
+ { "name": "Butter Croissant", "price": 399, "category": "Pastries & Baked Goods", "category_sort_order": 2, "sku": "CAFE-PAS-001" },
10
+ { "name": "Blueberry Muffin", "price": 349, "category": "Pastries & Baked Goods", "category_sort_order": 2, "sku": "CAFE-PAS-002" },
11
+ { "name": "Cinnamon Roll", "price": 449, "category": "Pastries & Baked Goods", "category_sort_order": 2, "sku": "CAFE-PAS-003" },
12
+ { "name": "Chocolate Chip Cookie", "price": 299, "category": "Pastries & Baked Goods", "category_sort_order": 2, "sku": "CAFE-PAS-004" },
13
+
14
+ { "name": "Avocado Toast", "price": 999, "category": "Breakfast", "category_sort_order": 3, "sku": "CAFE-BRK-001" },
15
+ { "name": "Breakfast Burrito", "price": 899, "category": "Breakfast", "category_sort_order": 3, "sku": "CAFE-BRK-002" },
16
+ { "name": "Açaí Bowl", "price": 1199, "category": "Breakfast", "category_sort_order": 3, "sku": "CAFE-BRK-003" },
17
+ { "name": "Yogurt Parfait", "price": 699, "category": "Breakfast", "category_sort_order": 3, "sku": "CAFE-BRK-004" },
18
+
19
+ { "name": "Turkey Club", "price": 1099, "category": "Sandwiches & Wraps", "category_sort_order": 4, "sku": "CAFE-SAN-001" },
20
+ { "name": "Caprese Panini", "price": 999, "category": "Sandwiches & Wraps", "category_sort_order": 4, "sku": "CAFE-SAN-002" },
21
+ { "name": "Chicken Caesar Wrap", "price": 1049, "category": "Sandwiches & Wraps", "category_sort_order": 4, "sku": "CAFE-SAN-003" },
22
+ { "name": "BLT", "price": 899, "category": "Sandwiches & Wraps", "category_sort_order": 4, "sku": "CAFE-SAN-004" },
23
+
24
+ { "name": "Berry Blast Smoothie", "price": 699, "category": "Smoothies & Juices", "category_sort_order": 5, "sku": "CAFE-SMO-001" },
25
+ { "name": "Green Detox Juice", "price": 749, "category": "Smoothies & Juices", "category_sort_order": 5, "sku": "CAFE-SMO-002" },
26
+ { "name": "Mango Tango Smoothie", "price": 699, "category": "Smoothies & Juices", "category_sort_order": 5, "sku": "CAFE-SMO-003" },
27
+ { "name": "Fresh-Squeezed OJ", "price": 499, "category": "Smoothies & Juices", "category_sort_order": 5, "sku": "CAFE-SMO-004" },
28
+
29
+ { "name": "House Drip Coffee", "price": 350, "category": "Coffee & Espresso", "category_sort_order": 6, "sku": "CAFE-COF2-001" },
30
+ { "name": "Latte", "price": 599, "category": "Coffee & Espresso", "category_sort_order": 6, "sku": "CAFE-COF2-002" },
31
+ { "name": "Iced Latte", "price": 599, "category": "Coffee & Espresso", "category_sort_order": 6, "sku": "CAFE-COF2-003" },
32
+ { "name": "Matcha Latte", "price": 649, "category": "Coffee & Espresso", "category_sort_order": 6, "sku": "CAFE-COF2-004" },
33
+
34
+ { "name": "Butter Croissant", "price": 450, "category": "Pastries & Baked Goods", "category_sort_order": 7, "sku": "CAFE-PAS2-001" },
35
+ { "name": "Blueberry Muffin", "price": 349, "category": "Pastries & Baked Goods", "category_sort_order": 7, "sku": "CAFE-PAS2-002" },
36
+ { "name": "Almond Croissant", "price": 499, "category": "Pastries & Baked Goods", "category_sort_order": 7, "sku": "CAFE-PAS2-003" },
37
+ { "name": "Banana Bread", "price": 399, "category": "Pastries & Baked Goods", "category_sort_order": 7, "sku": "CAFE-PAS2-004" },
38
+
39
+ { "name": "Avocado Toast", "price": 1099, "category": "Breakfast", "category_sort_order": 8, "sku": "CAFE-BRK2-001" },
40
+ { "name": "Eggs Benedict", "price": 1399, "category": "Breakfast", "category_sort_order": 8, "sku": "CAFE-BRK2-002" },
41
+ { "name": "French Toast", "price": 1199, "category": "Breakfast", "category_sort_order": 8, "sku": "CAFE-BRK2-003" },
42
+
43
+ { "name": "House Drip Coffee", "price": 275, "category": "Drinks", "category_sort_order": 9, "sku": "CAFE-DRK-001" },
44
+ { "name": "Iced Tea", "price": 349, "category": "Drinks", "category_sort_order": 9, "sku": "CAFE-DRK-002" },
45
+ { "name": "Lemonade", "price": 399, "category": "Drinks", "category_sort_order": 9, "sku": "CAFE-DRK-003" },
46
+ { "name": "Hot Chocolate", "price": 449, "category": "Drinks", "category_sort_order": 9, "sku": "CAFE-DRK-004" },
47
+ { "name": "Berry Blast Smoothie", "price": 749, "category": "Drinks", "category_sort_order": 9, "sku": "CAFE-DRK-005" },
48
+
49
+ { "name": "Butter Croissant", "price": 399, "category": "Grab & Go", "category_sort_order": 10, "sku": "CAFE-GNG-001" },
50
+ { "name": "Blueberry Muffin", "price": 399, "category": "Grab & Go", "category_sort_order": 10, "sku": "CAFE-GNG-002" },
51
+ { "name": "BLT", "price": 899, "category": "Grab & Go", "category_sort_order": 10, "sku": "CAFE-GNG-003" },
52
+ { "name": "Yogurt Parfait", "price": 599, "category": "Grab & Go", "category_sort_order": 10, "sku": "CAFE-GNG-004" },
53
+ { "name": "Cold Brew", "price": 499, "category": "Grab & Go", "category_sort_order": 10, "sku": "CAFE-GNG-005" }
54
+ ]
55
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "modifier_groups": [
3
+ {
4
+ "name": "Milk Choice",
5
+ "min_required": 0,
6
+ "max_allowed": 1,
7
+ "modifiers": [
8
+ { "name": "Whole Milk", "price": 0 },
9
+ { "name": "Oat Milk", "price": 75 },
10
+ { "name": "Almond Milk", "price": 75 },
11
+ { "name": "Soy Milk", "price": 50 },
12
+ { "name": "Coconut Milk", "price": 75 }
13
+ ]
14
+ },
15
+ {
16
+ "name": "Espresso Shots",
17
+ "min_required": 0,
18
+ "max_allowed": 3,
19
+ "modifiers": [
20
+ { "name": "Extra Shot", "price": 100 },
21
+ { "name": "Double Shot", "price": 200 },
22
+ { "name": "Decaf Shot", "price": 0 }
23
+ ]
24
+ },
25
+ {
26
+ "name": "Flavor Add-Ins",
27
+ "min_required": 0,
28
+ "max_allowed": 3,
29
+ "modifiers": [
30
+ { "name": "Vanilla Syrup", "price": 75 },
31
+ { "name": "Caramel Syrup", "price": 75 },
32
+ { "name": "Hazelnut Syrup", "price": 75 },
33
+ { "name": "Mocha Syrup", "price": 75 },
34
+ { "name": "Lavender Syrup", "price": 100 },
35
+ { "name": "Sugar-Free Vanilla", "price": 75 }
36
+ ]
37
+ },
38
+ {
39
+ "name": "Size",
40
+ "min_required": 1,
41
+ "max_allowed": 1,
42
+ "modifiers": [
43
+ { "name": "Small (8oz)", "price": 0 },
44
+ { "name": "Medium (12oz)", "price": 50 },
45
+ { "name": "Large (16oz)", "price": 100 }
46
+ ]
47
+ },
48
+ {
49
+ "name": "Bread Choice",
50
+ "min_required": 1,
51
+ "max_allowed": 1,
52
+ "modifiers": [
53
+ { "name": "Sourdough", "price": 0 },
54
+ { "name": "Multigrain", "price": 0 },
55
+ { "name": "Ciabatta", "price": 0 },
56
+ { "name": "Gluten-Free Bread", "price": 150 },
57
+ { "name": "Croissant", "price": 100 }
58
+ ]
59
+ },
60
+ {
61
+ "name": "Smoothie Boosts",
62
+ "min_required": 0,
63
+ "max_allowed": 3,
64
+ "modifiers": [
65
+ { "name": "Protein Powder", "price": 150 },
66
+ { "name": "Chia Seeds", "price": 75 },
67
+ { "name": "Flax Seeds", "price": 75 },
68
+ { "name": "Spirulina", "price": 100 },
69
+ { "name": "Collagen Peptides", "price": 150 }
70
+ ]
71
+ }
72
+ ]
73
+ }