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,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SquareSandboxSimulator
|
|
4
|
+
module Generators
|
|
5
|
+
# Creates entities in Square (categories, items, discounts, taxes, team members, customers)
|
|
6
|
+
class EntityGenerator
|
|
7
|
+
# Configuration constants
|
|
8
|
+
DEFAULT_TEAM_MEMBER_COUNT = 5
|
|
9
|
+
DEFAULT_CUSTOMER_COUNT = 20
|
|
10
|
+
LOG_SEPARATOR = "=" * 60
|
|
11
|
+
|
|
12
|
+
attr_reader :services, :data, :logger
|
|
13
|
+
|
|
14
|
+
def initialize(services: nil, business_type: :restaurant)
|
|
15
|
+
@services = services || Services::Square::ServicesManager.new
|
|
16
|
+
@data = DataLoader.new(business_type: business_type)
|
|
17
|
+
@logger = SquareSandboxSimulator.logger
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Set up all entities (categories, items, discounts, etc.)
|
|
21
|
+
# @return [Hash] Hash of created entities with their Square IDs
|
|
22
|
+
def setup_all
|
|
23
|
+
logger.info LOG_SEPARATOR
|
|
24
|
+
logger.info "Setting up entities in Square..."
|
|
25
|
+
logger.info LOG_SEPARATOR
|
|
26
|
+
|
|
27
|
+
results = {
|
|
28
|
+
categories: setup_categories,
|
|
29
|
+
items: setup_items,
|
|
30
|
+
discounts: setup_discounts,
|
|
31
|
+
tax_rates: setup_tax_rates,
|
|
32
|
+
team_members: setup_team_members,
|
|
33
|
+
customers: setup_customers,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
logger.info LOG_SEPARATOR
|
|
37
|
+
logger.info "Entity setup complete!"
|
|
38
|
+
logger.info " Categories: #{results[:categories].size}"
|
|
39
|
+
logger.info " Items: #{results[:items].size}"
|
|
40
|
+
logger.info " Discounts: #{results[:discounts].size}"
|
|
41
|
+
logger.info " Tax Rates: #{results[:tax_rates].size}"
|
|
42
|
+
logger.info " Team Members: #{results[:team_members].size}"
|
|
43
|
+
logger.info " Customers: #{results[:customers].size}"
|
|
44
|
+
logger.info LOG_SEPARATOR
|
|
45
|
+
|
|
46
|
+
results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Create categories from data file (idempotent via name matching).
|
|
50
|
+
def setup_categories
|
|
51
|
+
logger.info "Setting up categories..."
|
|
52
|
+
|
|
53
|
+
existing = services.catalog.list_catalog(types: "CATEGORY")
|
|
54
|
+
existing_names = existing.map { |c| c.dig("category_data", "name")&.downcase }.compact.to_set
|
|
55
|
+
|
|
56
|
+
created = []
|
|
57
|
+
data.categories.each do |cat_data|
|
|
58
|
+
name = cat_data["name"]
|
|
59
|
+
|
|
60
|
+
if existing_names.include?(name&.downcase)
|
|
61
|
+
logger.debug "Category '#{name}' already exists, skipping"
|
|
62
|
+
match = existing.find { |c| c.dig("category_data", "name")&.downcase == name&.downcase }
|
|
63
|
+
created << match if match
|
|
64
|
+
else
|
|
65
|
+
result = services.catalog.upsert_category(name: name)
|
|
66
|
+
obj = result&.dig("catalog_object")
|
|
67
|
+
created << obj if obj
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
logger.info "Categories ready: #{created.size}"
|
|
72
|
+
created
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Create items from data file.
|
|
76
|
+
# Each item is created with a single "Regular" variation containing the price.
|
|
77
|
+
# Tracks variation catalog_object_ids (needed for orders).
|
|
78
|
+
def setup_items
|
|
79
|
+
logger.info "Setting up items..."
|
|
80
|
+
|
|
81
|
+
# Fetch current categories from Square for association
|
|
82
|
+
categories = services.catalog.list_catalog(types: "CATEGORY")
|
|
83
|
+
category_by_name = categories.each_with_object({}) do |cat, hash|
|
|
84
|
+
name = cat.dig("category_data", "name")
|
|
85
|
+
hash[name.downcase] = cat["id"] if name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Fetch existing items for idempotent matching
|
|
89
|
+
existing = services.catalog.list_catalog(types: "ITEM")
|
|
90
|
+
existing_names = existing.map { |i| i.dig("item_data", "name")&.downcase }.compact.to_set
|
|
91
|
+
|
|
92
|
+
created = []
|
|
93
|
+
data.items.each do |item_data|
|
|
94
|
+
name = item_data["name"]
|
|
95
|
+
|
|
96
|
+
if existing_names.include?(name&.downcase)
|
|
97
|
+
logger.debug "Item '#{name}' already exists, skipping"
|
|
98
|
+
match = existing.find { |i| i.dig("item_data", "name")&.downcase == name&.downcase }
|
|
99
|
+
created << match if match
|
|
100
|
+
else
|
|
101
|
+
category_id = category_by_name[item_data["category"]&.downcase]
|
|
102
|
+
price_cents = item_data["price"] || 0
|
|
103
|
+
|
|
104
|
+
result = services.catalog.upsert_item(
|
|
105
|
+
name: name,
|
|
106
|
+
price_cents: price_cents,
|
|
107
|
+
category_id: category_id,
|
|
108
|
+
)
|
|
109
|
+
obj = result&.dig("catalog_object")
|
|
110
|
+
created << obj if obj
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
logger.info "Items ready: #{created.size}"
|
|
115
|
+
created
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Create discounts from data file (idempotent via name matching).
|
|
119
|
+
def setup_discounts
|
|
120
|
+
logger.info "Setting up discounts..."
|
|
121
|
+
|
|
122
|
+
existing = services.catalog.list_catalog(types: "DISCOUNT")
|
|
123
|
+
existing_names = existing.map { |d| d.dig("discount_data", "name")&.downcase }.compact.to_set
|
|
124
|
+
|
|
125
|
+
created = []
|
|
126
|
+
data.discounts.each do |disc_data|
|
|
127
|
+
name = disc_data["name"]
|
|
128
|
+
|
|
129
|
+
if existing_names.include?(name&.downcase)
|
|
130
|
+
logger.debug "Discount '#{name}' already exists, skipping"
|
|
131
|
+
match = existing.find { |d| d.dig("discount_data", "name")&.downcase == name&.downcase }
|
|
132
|
+
created << match if match
|
|
133
|
+
else
|
|
134
|
+
result = if disc_data["percentage"]
|
|
135
|
+
services.catalog.upsert_discount(
|
|
136
|
+
name: name,
|
|
137
|
+
percentage: disc_data["percentage"],
|
|
138
|
+
)
|
|
139
|
+
else
|
|
140
|
+
services.catalog.upsert_discount(
|
|
141
|
+
name: name,
|
|
142
|
+
amount_cents: disc_data["amount"] || 0,
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
obj = result&.dig("catalog_object")
|
|
146
|
+
created << obj if obj
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
logger.info "Discounts ready: #{created.size}"
|
|
151
|
+
created
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Create tax rates from data file (idempotent via name matching).
|
|
155
|
+
def setup_tax_rates
|
|
156
|
+
logger.info "Setting up tax rates..."
|
|
157
|
+
|
|
158
|
+
existing = services.catalog.list_catalog(types: "TAX")
|
|
159
|
+
existing_names = existing.map { |t| t.dig("tax_data", "name")&.downcase }.compact.to_set
|
|
160
|
+
|
|
161
|
+
created = []
|
|
162
|
+
data.tax_rates.each do |rate_data|
|
|
163
|
+
name = rate_data["name"]
|
|
164
|
+
|
|
165
|
+
if existing_names.include?(name&.downcase)
|
|
166
|
+
logger.debug "Tax rate '#{name}' already exists, skipping"
|
|
167
|
+
match = existing.find { |t| t.dig("tax_data", "name")&.downcase == name&.downcase }
|
|
168
|
+
created << match if match
|
|
169
|
+
else
|
|
170
|
+
# Square expects percentage as string (e.g. "8.25")
|
|
171
|
+
# Clover stores rate as integer (e.g. 825000 = 8.25%)
|
|
172
|
+
percentage = if rate_data["rate"]
|
|
173
|
+
(rate_data["rate"] / 100_000.0).to_s
|
|
174
|
+
else
|
|
175
|
+
rate_data["percentage"]&.to_s || "0"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
result = services.catalog.upsert_tax(
|
|
179
|
+
name: name,
|
|
180
|
+
percentage: percentage,
|
|
181
|
+
)
|
|
182
|
+
obj = result&.dig("catalog_object")
|
|
183
|
+
created << obj if obj
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
logger.info "Tax rates ready: #{created.size}"
|
|
188
|
+
created
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Ensure team members exist (idempotent).
|
|
192
|
+
def setup_team_members
|
|
193
|
+
logger.info "Setting up team members..."
|
|
194
|
+
members = services.team.ensure_team_members(count: DEFAULT_TEAM_MEMBER_COUNT)
|
|
195
|
+
logger.info "Team members ready: #{members.size}"
|
|
196
|
+
members
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Ensure customers exist (idempotent).
|
|
200
|
+
def setup_customers
|
|
201
|
+
logger.info "Setting up customers..."
|
|
202
|
+
customers = services.customer.ensure_customers(count: DEFAULT_CUSTOMER_COUNT)
|
|
203
|
+
logger.info "Customers ready: #{customers.size}"
|
|
204
|
+
customers
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Delete all entities (for clean slate)
|
|
208
|
+
def delete_all
|
|
209
|
+
logger.warn "=" * 60
|
|
210
|
+
logger.warn "DELETING ALL ENTITIES..."
|
|
211
|
+
logger.warn "=" * 60
|
|
212
|
+
|
|
213
|
+
# Delete catalog objects (items, categories, discounts, taxes)
|
|
214
|
+
services.catalog.delete_all
|
|
215
|
+
|
|
216
|
+
# Delete customers
|
|
217
|
+
customers = services.customer.list_customers
|
|
218
|
+
customers.each do |c|
|
|
219
|
+
services.customer.delete_customer(c["id"])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
logger.info "All entities deleted"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Extract variation IDs from catalog items (needed for creating orders).
|
|
226
|
+
# Square orders reference item variation IDs, not item IDs.
|
|
227
|
+
#
|
|
228
|
+
# @param items [Array<Hash>] Array of catalog item objects
|
|
229
|
+
# @return [Array<Hash>] Array of { item_id:, variation_id:, name:, price_cents: }
|
|
230
|
+
def self.extract_variation_ids(items)
|
|
231
|
+
items.filter_map do |item|
|
|
232
|
+
variations = item.dig("item_data", "variations") || []
|
|
233
|
+
next if variations.empty?
|
|
234
|
+
|
|
235
|
+
variation = variations.first
|
|
236
|
+
price = variation.dig("item_variation_data", "price_money", "amount") || 0
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
item_id: item["id"],
|
|
240
|
+
variation_id: variation["id"],
|
|
241
|
+
name: item.dig("item_data", "name"),
|
|
242
|
+
price_cents: price,
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|