skytab_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/Gemfile +10 -0
- data/bin/simulate +433 -0
- data/lib/skytab_sandbox_simulator/configuration.rb +205 -0
- data/lib/skytab_sandbox_simulator/data/bar_nightclub/categories.json +9 -0
- data/lib/skytab_sandbox_simulator/data/bar_nightclub/items.json +28 -0
- data/lib/skytab_sandbox_simulator/data/bar_nightclub/tenders.json +19 -0
- data/lib/skytab_sandbox_simulator/data/cafe_bakery/categories.json +9 -0
- data/lib/skytab_sandbox_simulator/data/cafe_bakery/items.json +30 -0
- data/lib/skytab_sandbox_simulator/data/cafe_bakery/tenders.json +17 -0
- data/lib/skytab_sandbox_simulator/data/fine_dining/categories.json +9 -0
- data/lib/skytab_sandbox_simulator/data/fine_dining/items.json +30 -0
- data/lib/skytab_sandbox_simulator/data/fine_dining/tenders.json +18 -0
- data/lib/skytab_sandbox_simulator/data/pizzeria/categories.json +9 -0
- data/lib/skytab_sandbox_simulator/data/pizzeria/items.json +28 -0
- data/lib/skytab_sandbox_simulator/data/pizzeria/tenders.json +18 -0
- data/lib/skytab_sandbox_simulator/data/restaurant/categories.json +44 -0
- data/lib/skytab_sandbox_simulator/data/restaurant/items.json +59 -0
- data/lib/skytab_sandbox_simulator/data/restaurant/tenders.json +22 -0
- data/lib/skytab_sandbox_simulator/database.rb +192 -0
- data/lib/skytab_sandbox_simulator/db/factories/business_types.rb +102 -0
- data/lib/skytab_sandbox_simulator/db/factories/categories.rb +243 -0
- data/lib/skytab_sandbox_simulator/db/factories/items.rb +976 -0
- data/lib/skytab_sandbox_simulator/db/factories/simulated_orders.rb +120 -0
- data/lib/skytab_sandbox_simulator/db/factories/simulated_payments.rb +75 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000000_enable_pgcrypto.rb +7 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000001_create_business_types.rb +18 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000002_create_categories.rb +18 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000003_create_items.rb +23 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000004_create_simulated_orders.rb +35 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000005_create_simulated_payments.rb +26 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000006_create_api_requests.rb +27 -0
- data/lib/skytab_sandbox_simulator/db/migrate/20260316000007_create_daily_summaries.rb +24 -0
- data/lib/skytab_sandbox_simulator/generators/data_loader.rb +125 -0
- data/lib/skytab_sandbox_simulator/generators/entity_generator.rb +107 -0
- data/lib/skytab_sandbox_simulator/generators/order_generator.rb +390 -0
- data/lib/skytab_sandbox_simulator/models/api_request.rb +43 -0
- data/lib/skytab_sandbox_simulator/models/business_type.rb +25 -0
- data/lib/skytab_sandbox_simulator/models/category.rb +17 -0
- data/lib/skytab_sandbox_simulator/models/daily_summary.rb +67 -0
- data/lib/skytab_sandbox_simulator/models/item.rb +32 -0
- data/lib/skytab_sandbox_simulator/models/record.rb +14 -0
- data/lib/skytab_sandbox_simulator/models/simulated_order.rb +40 -0
- data/lib/skytab_sandbox_simulator/models/simulated_payment.rb +28 -0
- data/lib/skytab_sandbox_simulator/seeder.rb +167 -0
- data/lib/skytab_sandbox_simulator/services/base_service.rb +227 -0
- data/lib/skytab_sandbox_simulator/services/skytab/catalog_service.rb +130 -0
- data/lib/skytab_sandbox_simulator/services/skytab/location_service.rb +54 -0
- data/lib/skytab_sandbox_simulator/services/skytab/order_service.rb +139 -0
- data/lib/skytab_sandbox_simulator/services/skytab/payment_service.rb +94 -0
- data/lib/skytab_sandbox_simulator/services/skytab/service_manager.rb +62 -0
- data/lib/skytab_sandbox_simulator/version.rb +5 -0
- data/lib/skytab_sandbox_simulator.rb +45 -0
- metadata +305 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
FactoryBot.define do
|
|
6
|
+
factory :simulated_order, class: "SkytabSandboxSimulator::Models::SimulatedOrder" do
|
|
7
|
+
sequence(:skytab_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 :closed do
|
|
18
|
+
status { "closed" }
|
|
19
|
+
sequence(:skytab_ticket_id) { |n| "TKT#{n}#{SecureRandom.hex(4).upcase}" }
|
|
20
|
+
subtotal { 2500 } # cents
|
|
21
|
+
tax_amount { 206 } # cents (8.25%)
|
|
22
|
+
tip_amount { 450 } # cents
|
|
23
|
+
discount_amount { 0 } # cents
|
|
24
|
+
total { 3156 } # cents
|
|
25
|
+
meal_period { "lunch" }
|
|
26
|
+
revenue_class { "DINE_IN" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
trait :refunded do
|
|
30
|
+
status { "refunded" }
|
|
31
|
+
sequence(:skytab_ticket_id) { |n| "TKT#{n}#{SecureRandom.hex(4).upcase}" }
|
|
32
|
+
subtotal { 1800 }
|
|
33
|
+
tax_amount { 149 }
|
|
34
|
+
tip_amount { 0 }
|
|
35
|
+
discount_amount { 0 }
|
|
36
|
+
total { 1949 }
|
|
37
|
+
meal_period { "dinner" }
|
|
38
|
+
revenue_class { "DINE_IN" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
trait :voided do
|
|
42
|
+
status { "voided" }
|
|
43
|
+
subtotal { 0 }
|
|
44
|
+
tax_amount { 0 }
|
|
45
|
+
tip_amount { 0 }
|
|
46
|
+
total { 0 }
|
|
47
|
+
metadata { { "reason" => "Customer cancelled" } }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
trait :with_business_type do
|
|
51
|
+
association :business_type, factory: [:business_type, :restaurant]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Convenience: a closed order with associated payment
|
|
55
|
+
trait :with_payment do
|
|
56
|
+
closed
|
|
57
|
+
|
|
58
|
+
after(:create) do |order|
|
|
59
|
+
create(:simulated_payment, :completed, simulated_order: order, amount: order.total)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convenience: a closed order with a split payment
|
|
64
|
+
trait :with_split_payment do
|
|
65
|
+
closed
|
|
66
|
+
total { 4000 }
|
|
67
|
+
subtotal { 3400 }
|
|
68
|
+
tax_amount { 281 }
|
|
69
|
+
tip_amount { 319 }
|
|
70
|
+
|
|
71
|
+
after(:create) do |order|
|
|
72
|
+
half = order.total / 2
|
|
73
|
+
create(:simulated_payment, :completed, :cash_tender, simulated_order: order, amount: half)
|
|
74
|
+
create(:simulated_payment, :completed, :visa_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
|
+
revenue_class { "DINE_IN" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
trait :lunch_order do
|
|
85
|
+
meal_period { "lunch" }
|
|
86
|
+
revenue_class { "DINE_IN" }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
trait :dinner_order do
|
|
90
|
+
meal_period { "dinner" }
|
|
91
|
+
revenue_class { "DINE_IN" }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
trait :late_night_order do
|
|
95
|
+
meal_period { "late_night" }
|
|
96
|
+
revenue_class { "BAR" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Revenue class traits
|
|
100
|
+
trait :dine_in do
|
|
101
|
+
revenue_class { "DINE_IN" }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
trait :takeout do
|
|
105
|
+
revenue_class { "TAKEOUT" }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
trait :delivery do
|
|
109
|
+
revenue_class { "DELIVERY" }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
trait :bar do
|
|
113
|
+
revenue_class { "BAR" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
trait :catering do
|
|
117
|
+
revenue_class { "CATERING" }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
FactoryBot.define do
|
|
6
|
+
factory :simulated_payment, class: "SkytabSandboxSimulator::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 :completed do
|
|
18
|
+
status { "completed" }
|
|
19
|
+
sequence(:skytab_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(:skytab_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 :visa_tender do
|
|
39
|
+
tender_name { "VISA" }
|
|
40
|
+
payment_type { "card" }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
trait :mastercard_tender do
|
|
44
|
+
tender_name { "MASTERCARD" }
|
|
45
|
+
payment_type { "card" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
trait :amex_tender do
|
|
49
|
+
tender_name { "AMEX" }
|
|
50
|
+
payment_type { "card" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
trait :discover_tender do
|
|
54
|
+
tender_name { "DISCOVER" }
|
|
55
|
+
payment_type { "card" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
trait :gift_card_tender do
|
|
59
|
+
tender_name { "GIFT_CARD" }
|
|
60
|
+
payment_type { "gift_card" }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
trait :house_account_tender do
|
|
64
|
+
tender_name { "HOUSE_ACCOUNT" }
|
|
65
|
+
payment_type { "house_account" }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ── Split payment trait ────────────────────────────────────
|
|
69
|
+
trait :split do
|
|
70
|
+
status { "completed" }
|
|
71
|
+
sequence(:skytab_payment_id) { |n| "PAY#{n}#{SecureRandom.hex(4).upcase}" }
|
|
72
|
+
amount { 750 }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
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, [:business_type_id, :name], 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, [: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,35 @@
|
|
|
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 :skytab_ticket_id
|
|
7
|
+
t.string :skytab_location_id, null: false
|
|
8
|
+
t.references :business_type, foreign_key: true, type: :uuid
|
|
9
|
+
t.string :status, null: false, default: "open"
|
|
10
|
+
t.integer :subtotal, default: 0 # cents
|
|
11
|
+
t.integer :tax_amount, default: 0 # cents
|
|
12
|
+
t.integer :tip_amount, default: 0 # cents
|
|
13
|
+
t.integer :discount_amount, default: 0 # cents
|
|
14
|
+
t.integer :total, default: 0 # cents
|
|
15
|
+
t.string :revenue_class
|
|
16
|
+
t.string :meal_period
|
|
17
|
+
t.jsonb :metadata, default: {}
|
|
18
|
+
t.date :business_date, null: false
|
|
19
|
+
|
|
20
|
+
t.timestamps
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Unique per location — prevents duplicate inserts on retry
|
|
24
|
+
add_index :simulated_orders, [:skytab_location_id, :skytab_ticket_id],
|
|
25
|
+
unique: true,
|
|
26
|
+
where: "skytab_ticket_id IS NOT NULL",
|
|
27
|
+
name: "idx_simulated_orders_location_ticket_unique"
|
|
28
|
+
add_index :simulated_orders, :skytab_location_id
|
|
29
|
+
add_index :simulated_orders, :status
|
|
30
|
+
add_index :simulated_orders, :business_date
|
|
31
|
+
add_index :simulated_orders, :meal_period
|
|
32
|
+
add_index :simulated_orders, :revenue_class
|
|
33
|
+
add_index :simulated_orders, :created_at
|
|
34
|
+
end
|
|
35
|
+
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 :skytab_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, :skytab_payment_id,
|
|
20
|
+
unique: true,
|
|
21
|
+
where: "skytab_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 :http_method, null: false
|
|
7
|
+
t.text :url, null: false
|
|
8
|
+
t.jsonb :request_payload, default: {}
|
|
9
|
+
t.jsonb :response_payload, default: {}
|
|
10
|
+
t.integer :response_status
|
|
11
|
+
t.integer :duration_ms
|
|
12
|
+
t.text :error_message
|
|
13
|
+
t.string :resource_type
|
|
14
|
+
t.string :resource_id
|
|
15
|
+
t.string :skytab_location_id
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :api_requests, :http_method
|
|
21
|
+
add_index :api_requests, :resource_id
|
|
22
|
+
add_index :api_requests, [:resource_type, :resource_id]
|
|
23
|
+
add_index :api_requests, :created_at
|
|
24
|
+
add_index :api_requests, :response_status
|
|
25
|
+
add_index :api_requests, :skytab_location_id
|
|
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 :location_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, [:location_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,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SkytabSandboxSimulator
|
|
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.
|
|
11
|
+
class DataLoader
|
|
12
|
+
attr_reader :business_type
|
|
13
|
+
|
|
14
|
+
def initialize(business_type: :restaurant)
|
|
15
|
+
@business_type = business_type
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ── Primary data accessors (DB-first, JSON fallback) ───────
|
|
19
|
+
|
|
20
|
+
def categories
|
|
21
|
+
@categories ||= if db_connected?
|
|
22
|
+
categories_from_db
|
|
23
|
+
else
|
|
24
|
+
load_json("categories")["categories"]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def items
|
|
29
|
+
@items ||= if db_connected?
|
|
30
|
+
items_from_db
|
|
31
|
+
else
|
|
32
|
+
load_json("items")["items"]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# ── JSON-only accessors (no DB models for these) ───────────
|
|
37
|
+
|
|
38
|
+
def tenders
|
|
39
|
+
@tenders ||= load_json("tenders")["tenders"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def revenue_classes
|
|
43
|
+
@revenue_classes ||= load_json("tenders")["revenue_classes"]
|
|
44
|
+
rescue Error
|
|
45
|
+
%w[DINE_IN TAKEOUT DELIVERY BAR CATERING]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tax_rates
|
|
49
|
+
@tax_rates ||= load_json("tenders")["tax_rates"]
|
|
50
|
+
rescue Error
|
|
51
|
+
[]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# ── Convenience filters ────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
def items_for_category(category_name)
|
|
57
|
+
items.select { |item| item["category"] == category_name }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ── Data source introspection ──────────────────────────────
|
|
61
|
+
|
|
62
|
+
def data_source
|
|
63
|
+
db_connected? ? :db : :json
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def db_connected?
|
|
69
|
+
Database.connected?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# ── DB query methods ───────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def categories_from_db
|
|
75
|
+
bt = Models::BusinessType.find_by(key: business_type.to_s)
|
|
76
|
+
return load_json("categories")["categories"] unless bt
|
|
77
|
+
|
|
78
|
+
bt.categories.sorted.map do |cat|
|
|
79
|
+
{
|
|
80
|
+
"name" => cat.name,
|
|
81
|
+
"sort_order" => cat.sort_order,
|
|
82
|
+
"description" => cat.description
|
|
83
|
+
}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def items_from_db
|
|
88
|
+
bt = Models::BusinessType.find_by(key: business_type.to_s)
|
|
89
|
+
return load_json("items")["items"] unless bt
|
|
90
|
+
|
|
91
|
+
Models::Item.active
|
|
92
|
+
.for_business_type(business_type.to_s)
|
|
93
|
+
.includes(:category)
|
|
94
|
+
.order("categories.sort_order", "items.name")
|
|
95
|
+
.map do |item|
|
|
96
|
+
hash = {
|
|
97
|
+
"name" => item.name,
|
|
98
|
+
"price" => item.price,
|
|
99
|
+
"category" => item.category.name
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
hash["sku"] = item.sku if item.sku.present?
|
|
103
|
+
hash["unit"] = item.unit if item.unit.present?
|
|
104
|
+
hash
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ── JSON loading ───────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def load_json(filename)
|
|
111
|
+
path = File.join(data_path, "#{filename}.json")
|
|
112
|
+
|
|
113
|
+
unless File.exist?(path)
|
|
114
|
+
raise Error, "Data file not found: #{path}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
JSON.parse(File.read(path))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def data_path
|
|
121
|
+
File.join(SkytabSandboxSimulator.root, "lib", "skytab_sandbox_simulator", "data", business_type.to_s)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SkytabSandboxSimulator
|
|
4
|
+
module Generators
|
|
5
|
+
# Creates restaurant entities in SkyTab (categories, items, tenders, etc.)
|
|
6
|
+
class EntityGenerator
|
|
7
|
+
LOG_SEPARATOR = ("=" * 60).freeze
|
|
8
|
+
|
|
9
|
+
attr_reader :services, :data, :logger
|
|
10
|
+
|
|
11
|
+
def initialize(services: nil, business_type: :restaurant)
|
|
12
|
+
@services = services || Services::Skytab::ServiceManager.new
|
|
13
|
+
@data = DataLoader.new(business_type: business_type)
|
|
14
|
+
@logger = SkytabSandboxSimulator.logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set up all entities (categories, items)
|
|
18
|
+
def setup_all
|
|
19
|
+
logger.info LOG_SEPARATOR
|
|
20
|
+
logger.info "Setting up entities in SkyTab..."
|
|
21
|
+
logger.info LOG_SEPARATOR
|
|
22
|
+
|
|
23
|
+
results = {
|
|
24
|
+
categories: setup_categories,
|
|
25
|
+
items: setup_items
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.info LOG_SEPARATOR
|
|
29
|
+
logger.info "Entity setup complete!"
|
|
30
|
+
logger.info " Categories: #{results[:categories].size}"
|
|
31
|
+
logger.info " Items: #{results[:items].size}"
|
|
32
|
+
logger.info LOG_SEPARATOR
|
|
33
|
+
|
|
34
|
+
results
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create categories from data file
|
|
38
|
+
def setup_categories
|
|
39
|
+
logger.info "Setting up categories..."
|
|
40
|
+
|
|
41
|
+
existing = services.catalog.get_categories
|
|
42
|
+
existing_names = existing.map { |c| c["name"] }
|
|
43
|
+
|
|
44
|
+
created = []
|
|
45
|
+
data.categories.each do |cat|
|
|
46
|
+
if existing_names.include?(cat["name"])
|
|
47
|
+
logger.debug "Category already exists: #{cat['name']}"
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result = services.catalog.create_category(
|
|
52
|
+
name: cat["name"],
|
|
53
|
+
sort_order: cat["sort_order"]
|
|
54
|
+
)
|
|
55
|
+
created << result if result
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
logger.warn "Failed to create category #{cat['name']}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
logger.info "Created #{created.size} categories (#{existing.size} already existed)"
|
|
61
|
+
existing + created
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Create items from data file
|
|
65
|
+
def setup_items
|
|
66
|
+
logger.info "Setting up items..."
|
|
67
|
+
|
|
68
|
+
existing_items = services.catalog.get_items
|
|
69
|
+
existing_names = existing_items.map { |i| i["name"] }
|
|
70
|
+
|
|
71
|
+
# Build category name -> ID mapping
|
|
72
|
+
categories = services.catalog.get_categories
|
|
73
|
+
cat_map = categories.each_with_object({}) { |c, h| h[c["name"]] = c["id"] }
|
|
74
|
+
|
|
75
|
+
created = []
|
|
76
|
+
data.items.each do |item|
|
|
77
|
+
if existing_names.include?(item["name"])
|
|
78
|
+
logger.debug "Item already exists: #{item['name']}"
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
category_id = cat_map[item["category"]]
|
|
83
|
+
|
|
84
|
+
result = services.catalog.create_item(
|
|
85
|
+
name: item["name"],
|
|
86
|
+
price: item["price"],
|
|
87
|
+
category_id: category_id,
|
|
88
|
+
sku: item["sku"]
|
|
89
|
+
)
|
|
90
|
+
created << result if result
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
logger.warn "Failed to create item #{item['name']}: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
logger.info "Created #{created.size} items (#{existing_items.size} already existed)"
|
|
96
|
+
existing_items + created
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Delete all entities
|
|
100
|
+
def delete_all
|
|
101
|
+
logger.warn "Deleting all entities..."
|
|
102
|
+
services.catalog.delete_all
|
|
103
|
+
logger.info "All entities deleted"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|