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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +176 -0
- data/bin/simulate +388 -0
- data/lib/square_sandbox_simulator/configuration.rb +193 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/categories.json +54 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/combos.json +33 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/coupon_codes.json +133 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/discounts.json +113 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/items.json +55 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/modifiers.json +73 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/tax_rates.json +26 -0
- data/lib/square_sandbox_simulator/data/cafe_bakery/tenders.json +41 -0
- data/lib/square_sandbox_simulator/data/restaurant/categories.json +54 -0
- data/lib/square_sandbox_simulator/data/restaurant/combos.json +265 -0
- data/lib/square_sandbox_simulator/data/restaurant/coupon_codes.json +266 -0
- data/lib/square_sandbox_simulator/data/restaurant/discounts.json +198 -0
- data/lib/square_sandbox_simulator/data/restaurant/gift_cards.json +82 -0
- data/lib/square_sandbox_simulator/data/restaurant/items.json +388 -0
- data/lib/square_sandbox_simulator/data/restaurant/modifiers.json +62 -0
- data/lib/square_sandbox_simulator/data/restaurant/tax_rates.json +38 -0
- data/lib/square_sandbox_simulator/data/restaurant/tenders.json +41 -0
- data/lib/square_sandbox_simulator/data/salon_spa/categories.json +24 -0
- data/lib/square_sandbox_simulator/data/salon_spa/combos.json +88 -0
- data/lib/square_sandbox_simulator/data/salon_spa/coupon_codes.json +96 -0
- data/lib/square_sandbox_simulator/data/salon_spa/discounts.json +93 -0
- data/lib/square_sandbox_simulator/data/salon_spa/gift_cards.json +47 -0
- data/lib/square_sandbox_simulator/data/salon_spa/items.json +100 -0
- data/lib/square_sandbox_simulator/data/salon_spa/modifiers.json +49 -0
- data/lib/square_sandbox_simulator/data/salon_spa/tax_rates.json +17 -0
- data/lib/square_sandbox_simulator/data/salon_spa/tenders.json +41 -0
- data/lib/square_sandbox_simulator/database.rb +224 -0
- data/lib/square_sandbox_simulator/db/factories/api_requests.rb +95 -0
- data/lib/square_sandbox_simulator/db/factories/business_types.rb +178 -0
- data/lib/square_sandbox_simulator/db/factories/categories.rb +379 -0
- data/lib/square_sandbox_simulator/db/factories/daily_summaries.rb +56 -0
- data/lib/square_sandbox_simulator/db/factories/items.rb +1526 -0
- data/lib/square_sandbox_simulator/db/factories/simulated_orders.rb +112 -0
- data/lib/square_sandbox_simulator/db/factories/simulated_payments.rb +61 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000000_enable_pgcrypto.rb +7 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000001_create_business_types.rb +18 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000002_create_categories.rb +18 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000003_create_items.rb +23 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000004_create_simulated_orders.rb +36 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000005_create_simulated_payments.rb +26 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000006_create_api_requests.rb +27 -0
- data/lib/square_sandbox_simulator/db/migrate/20260312000007_create_daily_summaries.rb +24 -0
- data/lib/square_sandbox_simulator/generators/data_loader.rb +202 -0
- data/lib/square_sandbox_simulator/generators/entity_generator.rb +248 -0
- data/lib/square_sandbox_simulator/generators/order_generator.rb +632 -0
- data/lib/square_sandbox_simulator/models/api_request.rb +43 -0
- data/lib/square_sandbox_simulator/models/business_type.rb +25 -0
- data/lib/square_sandbox_simulator/models/category.rb +18 -0
- data/lib/square_sandbox_simulator/models/daily_summary.rb +68 -0
- data/lib/square_sandbox_simulator/models/item.rb +33 -0
- data/lib/square_sandbox_simulator/models/record.rb +16 -0
- data/lib/square_sandbox_simulator/models/simulated_order.rb +42 -0
- data/lib/square_sandbox_simulator/models/simulated_payment.rb +28 -0
- data/lib/square_sandbox_simulator/seeder.rb +242 -0
- data/lib/square_sandbox_simulator/services/base_service.rb +253 -0
- data/lib/square_sandbox_simulator/services/square/catalog_service.rb +203 -0
- data/lib/square_sandbox_simulator/services/square/customer_service.rb +130 -0
- data/lib/square_sandbox_simulator/services/square/order_service.rb +121 -0
- data/lib/square_sandbox_simulator/services/square/payment_service.rb +136 -0
- data/lib/square_sandbox_simulator/services/square/services_manager.rb +68 -0
- data/lib/square_sandbox_simulator/services/square/team_service.rb +108 -0
- data/lib/square_sandbox_simulator/version.rb +5 -0
- data/lib/square_sandbox_simulator.rb +47 -0
- 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
|
+
}
|