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