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,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