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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ FactoryBot.define do
6
+ factory :simulated_order, class: "SquareSandboxSimulator::Models::SimulatedOrder" do
7
+ sequence(:square_location_id) { |n| "LOCATION#{n}" }
8
+ status { "open" }
9
+ business_date { Date.today }
10
+ subtotal { 0 }
11
+ tax_amount { 0 }
12
+ tip_amount { 0 }
13
+ discount_amount { 0 }
14
+ total { 0 }
15
+ metadata { {} }
16
+
17
+ trait :paid do
18
+ status { "paid" }
19
+ sequence(:square_order_id) { |n| "ORD#{n}#{SecureRandom.hex(4).upcase}" }
20
+ subtotal { 2500 } # cents
21
+ tax_amount { 222 } # cents
22
+ tip_amount { 450 } # cents
23
+ discount_amount { 0 } # cents
24
+ total { 3172 } # cents — subtotal + tax + tip - discount
25
+ meal_period { "lunch" }
26
+ dining_option { "HERE" }
27
+ end
28
+
29
+ trait :refunded do
30
+ status { "refunded" }
31
+ sequence(:square_order_id) { |n| "ORD#{n}#{SecureRandom.hex(4).upcase}" }
32
+ subtotal { 1800 }
33
+ tax_amount { 160 }
34
+ tip_amount { 0 }
35
+ discount_amount { 0 }
36
+ total { 1960 }
37
+ meal_period { "dinner" }
38
+ dining_option { "HERE" }
39
+ end
40
+
41
+ trait :failed do
42
+ status { "failed" }
43
+ subtotal { 0 }
44
+ tax_amount { 0 }
45
+ tip_amount { 0 }
46
+ total { 0 }
47
+ metadata { { "error" => "Payment declined" } }
48
+ end
49
+
50
+ trait :with_business_type do
51
+ association :business_type, factory: %i[business_type restaurant]
52
+ end
53
+
54
+ # Convenience: a paid order with associated payment
55
+ trait :with_payment do
56
+ paid
57
+
58
+ after(:create) do |order|
59
+ create(:simulated_payment, :success, simulated_order: order, amount: order.total)
60
+ end
61
+ end
62
+
63
+ # Convenience: a paid order with a split payment (cash + card)
64
+ trait :with_split_payment do
65
+ paid
66
+ total { 4000 }
67
+ subtotal { 3400 }
68
+ tax_amount { 302 }
69
+ tip_amount { 298 }
70
+
71
+ after(:create) do |order|
72
+ half = order.total / 2
73
+ create(:simulated_payment, :success, :cash_tender, simulated_order: order, amount: half)
74
+ create(:simulated_payment, :success, :credit_tender, simulated_order: order, amount: order.total - half)
75
+ end
76
+ end
77
+
78
+ # Meal-period traits
79
+ trait :breakfast_order do
80
+ meal_period { "breakfast" }
81
+ dining_option { "HERE" }
82
+ end
83
+
84
+ trait :lunch_order do
85
+ meal_period { "lunch" }
86
+ dining_option { "HERE" }
87
+ end
88
+
89
+ trait :dinner_order do
90
+ meal_period { "dinner" }
91
+ dining_option { "HERE" }
92
+ end
93
+
94
+ trait :late_night_order do
95
+ meal_period { "late_night" }
96
+ dining_option { "HERE" }
97
+ end
98
+
99
+ # Dining-option traits
100
+ trait :dine_in do
101
+ dining_option { "HERE" }
102
+ end
103
+
104
+ trait :takeout do
105
+ dining_option { "TO_GO" }
106
+ end
107
+
108
+ trait :delivery do
109
+ dining_option { "DELIVERY" }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ FactoryBot.define do
6
+ factory :simulated_payment, class: "SquareSandboxSimulator::Models::SimulatedPayment" do
7
+ association :simulated_order
8
+ tender_name { "Cash" }
9
+ amount { 1500 } # cents
10
+ tip_amount { 0 } # cents
11
+ tax_amount { 0 } # cents
12
+ status { "pending" }
13
+ payment_type { nil }
14
+
15
+ # ── Status traits ──────────────────────────────────────────
16
+
17
+ trait :success do
18
+ status { "SUCCESS" }
19
+ sequence(:square_payment_id) { |n| "PAY#{n}#{SecureRandom.hex(4).upcase}" }
20
+ end
21
+
22
+ trait :failed do
23
+ status { "failed" }
24
+ end
25
+
26
+ trait :refunded do
27
+ status { "refunded" }
28
+ sequence(:square_payment_id) { |n| "PAY#{n}#{SecureRandom.hex(4).upcase}" }
29
+ end
30
+
31
+ # ── Tender traits ──────────────────────────────────────────
32
+
33
+ trait :cash_tender do
34
+ tender_name { "Cash" }
35
+ payment_type { "cash" }
36
+ end
37
+
38
+ trait :credit_tender do
39
+ tender_name { "Credit Card" }
40
+ payment_type { "credit" }
41
+ end
42
+
43
+ trait :debit_tender do
44
+ tender_name { "Debit Card" }
45
+ payment_type { "debit" }
46
+ end
47
+
48
+ trait :gift_card_tender do
49
+ tender_name { "Gift Card" }
50
+ payment_type { "gift_card" }
51
+ end
52
+
53
+ # ── Split payment trait ────────────────────────────────────
54
+ # Marks this payment as part of a split (amount is a portion of the total)
55
+ trait :split do
56
+ status { "paid" }
57
+ sequence(:square_payment_id) { |n| "PAY#{n}#{SecureRandom.hex(4).upcase}" }
58
+ amount { 750 } # half of a typical order
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EnablePgcrypto < ActiveRecord::Migration[8.0]
4
+ def change
5
+ enable_extension "pgcrypto"
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBusinessTypes < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :business_types, id: :uuid do |t|
6
+ t.string :key, null: false
7
+ t.string :name, null: false
8
+ t.text :description
9
+ t.string :industry
10
+ t.jsonb :order_profile, default: {}
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :business_types, :key, unique: true
16
+ add_index :business_types, :industry
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateCategories < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :categories, id: :uuid do |t|
6
+ t.references :business_type, null: false, foreign_key: { on_delete: :cascade }, type: :uuid
7
+ t.string :name, null: false
8
+ t.integer :sort_order, default: 0
9
+ t.text :description
10
+ t.string :tax_group
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :categories, %i[business_type_id name sort_order], unique: true
16
+ add_index :categories, :sort_order
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateItems < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :items, id: :uuid do |t|
6
+ t.references :category, null: false, foreign_key: { on_delete: :cascade }, type: :uuid
7
+ t.string :name, null: false
8
+ t.integer :price, null: false # cents
9
+ t.string :sku
10
+ t.string :unit
11
+ t.boolean :active, default: true, null: false
12
+ t.jsonb :variants, default: []
13
+ t.jsonb :metadata, default: {}
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ add_index :items, %i[category_id name], unique: true
19
+ add_index :items, :sku
20
+ add_index :items, :active
21
+ add_check_constraint :items, "price >= 0", name: "items_price_non_negative"
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSimulatedOrders < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :simulated_orders, id: :uuid do |t|
6
+ t.string :square_order_id
7
+ t.string :square_location_id, null: false
8
+ # Nullable: orders may be created before a business_type is assigned
9
+ t.references :business_type, foreign_key: true, type: :uuid
10
+ t.string :status, null: false, default: "open"
11
+ t.integer :subtotal, default: 0 # cents
12
+ t.integer :tax_amount, default: 0 # cents
13
+ t.integer :tip_amount, default: 0 # cents
14
+ t.integer :discount_amount, default: 0 # cents
15
+ t.integer :total, default: 0 # cents
16
+ t.string :dining_option
17
+ t.string :meal_period
18
+ t.jsonb :metadata, default: {}
19
+ t.date :business_date, null: false
20
+
21
+ t.timestamps
22
+ end
23
+
24
+ # Unique per location -- prevents duplicate inserts on retry
25
+ add_index :simulated_orders, %i[square_location_id square_order_id],
26
+ unique: true,
27
+ where: "square_order_id IS NOT NULL",
28
+ name: "idx_simulated_orders_location_square_unique"
29
+ add_index :simulated_orders, :square_location_id
30
+ add_index :simulated_orders, :status
31
+ add_index :simulated_orders, :business_date
32
+ add_index :simulated_orders, :meal_period
33
+ add_index :simulated_orders, :dining_option
34
+ add_index :simulated_orders, :created_at
35
+ end
36
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSimulatedPayments < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :simulated_payments, id: :uuid do |t|
6
+ t.references :simulated_order, null: false, foreign_key: { on_delete: :cascade }, type: :uuid
7
+ t.string :square_payment_id
8
+ t.string :tender_name, null: false
9
+ t.integer :amount, default: 0 # cents
10
+ t.integer :tip_amount, default: 0 # cents
11
+ t.integer :tax_amount, default: 0 # cents
12
+ t.string :status, null: false, default: "pending"
13
+ t.string :payment_type
14
+
15
+ t.timestamps
16
+ end
17
+
18
+ # Unique -- prevents duplicate payment inserts on retry
19
+ add_index :simulated_payments, :square_payment_id,
20
+ unique: true,
21
+ where: "square_payment_id IS NOT NULL"
22
+ add_index :simulated_payments, :tender_name
23
+ add_index :simulated_payments, :status
24
+ add_index :simulated_payments, :created_at
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateApiRequests < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :api_requests, id: :uuid do |t|
6
+ t.string :square_location_id
7
+ t.string :http_method, null: false
8
+ t.text :url, null: false
9
+ t.jsonb :request_payload, default: {}
10
+ t.jsonb :response_payload, default: {}
11
+ t.integer :response_status
12
+ t.integer :duration_ms
13
+ t.text :error_message
14
+ t.string :resource_type
15
+ t.string :resource_id
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :api_requests, :square_location_id
21
+ add_index :api_requests, :http_method
22
+ add_index :api_requests, :resource_id
23
+ add_index :api_requests, %i[resource_type resource_id]
24
+ add_index :api_requests, :created_at
25
+ add_index :api_requests, :response_status
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDailySummaries < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :daily_summaries, id: :uuid do |t|
6
+ t.string :merchant_id, null: false
7
+ t.date :business_date, null: false
8
+ t.integer :order_count, default: 0
9
+ t.integer :payment_count, default: 0
10
+ t.integer :refund_count, default: 0
11
+ t.integer :total_revenue, default: 0 # cents
12
+ t.integer :total_tax, default: 0 # cents
13
+ t.integer :total_tips, default: 0 # cents
14
+ t.integer :total_discounts, default: 0 # cents
15
+ t.jsonb :breakdown, default: {}
16
+
17
+ t.timestamps
18
+ end
19
+
20
+ add_index :daily_summaries, %i[merchant_id business_date], unique: true
21
+ add_index :daily_summaries, :business_date
22
+ add_index :daily_summaries, :created_at
23
+ end
24
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SquareSandboxSimulator
4
+ module Generators
5
+ # Loads data from PostgreSQL when connected, falling back to JSON files.
6
+ #
7
+ # The DB path is preferred because it respects any runtime mutations
8
+ # (items deactivated, prices changed, categories added via Seeder).
9
+ # When no DB connection is available the loader reads the static JSON
10
+ # data files shipped with the gem -- the returned hashes use the same
11
+ # keys in both cases so callers are unaware of the source.
12
+ class DataLoader
13
+ attr_reader :business_type
14
+
15
+ def initialize(business_type: :restaurant)
16
+ @business_type = business_type
17
+ end
18
+
19
+ # -- Primary data accessors (DB-first, JSON fallback) --
20
+
21
+ def categories
22
+ @categories ||= if db_connected?
23
+ categories_from_db
24
+ else
25
+ load_json("categories")["categories"]
26
+ end
27
+ end
28
+
29
+ def items
30
+ @items ||= if db_connected?
31
+ items_from_db
32
+ else
33
+ load_json("items")["items"]
34
+ end
35
+ end
36
+
37
+ def tax_rates
38
+ @tax_rates ||= if db_connected?
39
+ tax_rates_from_db
40
+ else
41
+ load_json("tax_rates")["tax_rates"]
42
+ end
43
+ rescue Error
44
+ [] # Return empty array if file doesn't exist
45
+ end
46
+
47
+ def category_tax_mapping
48
+ @category_tax_mapping ||= if db_connected?
49
+ category_tax_mapping_from_db
50
+ else
51
+ load_json("tax_rates")["category_tax_mapping"]
52
+ end
53
+ rescue Error
54
+ {} # Return empty hash if file doesn't exist
55
+ end
56
+
57
+ # -- JSON-only accessors (no DB models for these) --
58
+
59
+ def discounts
60
+ @discounts ||= load_json("discounts")["discounts"]
61
+ end
62
+
63
+ def tenders
64
+ @tenders ||= load_json("tenders")["tenders"]
65
+ end
66
+
67
+ def modifiers
68
+ @modifiers ||= load_json("modifiers")["modifier_groups"]
69
+ rescue Error
70
+ []
71
+ end
72
+
73
+ def coupon_codes
74
+ @coupon_codes ||= load_json("coupon_codes")["coupon_codes"]
75
+ rescue Error
76
+ []
77
+ end
78
+
79
+ # -- Convenience filters --
80
+
81
+ def items_for_category(category_name)
82
+ items.select { |item| item["category"] == category_name }
83
+ end
84
+
85
+ def discounts_by_type(type)
86
+ discounts.select { |d| d["type"] == type }
87
+ end
88
+
89
+ def time_based_discounts
90
+ discounts_by_type("time_based")
91
+ end
92
+
93
+ def line_item_discounts
94
+ discounts.select { |d| d["type"]&.start_with?("line_item") }
95
+ end
96
+
97
+ def loyalty_discounts
98
+ discounts_by_type("loyalty")
99
+ end
100
+
101
+ def threshold_discounts
102
+ discounts_by_type("threshold")
103
+ end
104
+
105
+ def active_coupon_codes
106
+ coupon_codes.select { |c| c["active"] }
107
+ end
108
+
109
+ def find_coupon(code)
110
+ coupon_codes.find { |c| c["code"].upcase == code.upcase }
111
+ end
112
+
113
+ # -- Data source introspection --
114
+
115
+ def data_source
116
+ db_connected? ? :db : :json
117
+ end
118
+
119
+ private
120
+
121
+ def db_connected?
122
+ Database.connected?
123
+ end
124
+
125
+ # -- DB query methods --
126
+ # Each method returns data formatted identically to the JSON
127
+ # structure so callers are unaware of the data source.
128
+
129
+ def categories_from_db
130
+ bt = Models::BusinessType.find_by(key: business_type.to_s)
131
+ return load_json("categories")["categories"] unless bt
132
+
133
+ bt.categories.sorted.map do |cat|
134
+ {
135
+ "name" => cat.name,
136
+ "sort_order" => cat.sort_order,
137
+ "description" => cat.description,
138
+ }
139
+ end
140
+ end
141
+
142
+ def items_from_db
143
+ bt = Models::BusinessType.find_by(key: business_type.to_s)
144
+ return load_json("items")["items"] unless bt
145
+
146
+ Models::Item.active
147
+ .for_business_type(business_type.to_s)
148
+ .includes(:category)
149
+ .order("categories.sort_order", "items.name")
150
+ .map do |item|
151
+ hash = {
152
+ "name" => item.name,
153
+ "price" => item.price,
154
+ "category" => item.category.name,
155
+ }
156
+
157
+ # Include optional fields when present (mirrors factory traits)
158
+ hash["sku"] = item.sku if item.sku.present?
159
+ hash["variants"] = item.variants if item.variants.present?
160
+ hash["unit"] = item.unit if item.unit.present?
161
+ hash["metadata"] = item.metadata if item.metadata.present?
162
+ hash
163
+ end
164
+ end
165
+
166
+ def tax_rates_from_db
167
+ # Tax rates are static configuration without a dedicated DB model.
168
+ # Always loaded from JSON regardless of DB connection status.
169
+ load_json("tax_rates")["tax_rates"]
170
+ end
171
+
172
+ def category_tax_mapping_from_db
173
+ bt = Models::BusinessType.find_by(key: business_type.to_s)
174
+ return load_json("tax_rates")["category_tax_mapping"] unless bt
175
+
176
+ mapping = {}
177
+ bt.categories.where.not(tax_group: nil).find_each do |cat|
178
+ mapping[cat.name] = [cat.tax_group]
179
+ end
180
+
181
+ # Fall back to JSON if DB has no tax_group data
182
+ return load_json("tax_rates")["category_tax_mapping"] if mapping.empty?
183
+
184
+ mapping
185
+ end
186
+
187
+ # -- JSON loading --
188
+
189
+ def load_json(filename)
190
+ path = File.join(data_path, "#{filename}.json")
191
+
192
+ raise Error, "Data file not found: #{path}" unless File.exist?(path)
193
+
194
+ JSON.parse(File.read(path))
195
+ end
196
+
197
+ def data_path
198
+ File.join(SquareSandboxSimulator.root, "lib", "square_sandbox_simulator", "data", business_type.to_s)
199
+ end
200
+ end
201
+ end
202
+ end