spree_cm_commissioner 2.5.0.pre.pre7 → 2.5.0.pre.pre8
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 +4 -4
- data/.github/workflows/test_and_build_gem.yml +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +7 -1
- data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +9 -9
- data/app/finders/spree_cm_commissioner/places/find_with_route.rb +10 -10
- data/app/finders/spree_cm_commissioner/routes/find_popular.rb +10 -14
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +3 -3
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -15
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +2 -14
- data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +3 -0
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +7 -1
- data/app/models/spree_cm_commissioner/guest.rb +0 -13
- data/app/models/spree_cm_commissioner/inventory_item.rb +1 -5
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -13
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +0 -8
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +0 -34
- data/app/models/spree_cm_commissioner/order_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/product_decorator.rb +2 -3
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -13
- data/app/models/spree_cm_commissioner/variant_decorator.rb +4 -7
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
- data/app/presenters/spree/variants/{visible_options_presenter.rb → visable_options_presenter.rb} +4 -2
- data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +12 -0
- data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +5 -0
- data/app/views/spree/admin/tenants/_form.html.erb +18 -0
- data/app/views/spree/admin/tenants/form/_footer.html.erb +31 -0
- data/app/views/spree/admin/tenants/form/_social.html.erb +31 -0
- data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
- data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +13 -6
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +23 -24
- data/config/locales/en.yml +0 -8
- data/config/locales/km.yml +0 -8
- data/config/routes.rb +0 -8
- data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +9 -0
- data/lib/cm_app_logger.rb +3 -21
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +7 -8
- metadata +7 -57
- data/app/controllers/spree/admin/integration_mappings_controller.rb +0 -21
- data/app/controllers/spree/admin/integration_sessions_controller.rb +0 -21
- data/app/controllers/spree/admin/integrations_controller.rb +0 -83
- data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +0 -15
- data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +0 -10
- data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +0 -4
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -15
- data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +0 -58
- data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +0 -39
- data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +0 -53
- data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +0 -30
- data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +0 -17
- data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +0 -61
- data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +0 -36
- data/app/models/concerns/spree_cm_commissioner/order_integration.rb +0 -33
- data/app/models/spree_cm_commissioner/integration.rb +0 -29
- data/app/models/spree_cm_commissioner/integration_mapping.rb +0 -41
- data/app/models/spree_cm_commissioner/integration_sync_session.rb +0 -15
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +0 -37
- data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +0 -6
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +0 -69
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +0 -183
- data/app/services/spree_cm_commissioner/integrations/polling.rb +0 -70
- data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +0 -79
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +0 -152
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +0 -83
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +0 -113
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +0 -215
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +0 -20
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +0 -19
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +0 -95
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +0 -81
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +0 -19
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +0 -90
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -35
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +0 -38
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +0 -44
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +0 -16
- data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +0 -49
- data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +0 -107
- data/app/views/spree/admin/integration_mappings/index.html.erb +0 -33
- data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +0 -116
- data/app/views/spree/admin/integration_sessions/index.html.erb +0 -42
- data/app/views/spree/admin/integrations/_form.html.erb +0 -104
- data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +0 -29
- data/app/views/spree/admin/integrations/edit.html.erb +0 -45
- data/app/views/spree/admin/integrations/index.html.erb +0 -75
- data/app/views/spree/admin/integrations/new.html.erb +0 -25
- data/db/migrate/20251017094845_create_cm_integrations.rb +0 -22
- data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +0 -68
- data/db/migrate/20251017101605_create_cm_integration_mappings.rb +0 -52
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +0 -25
- data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +0 -14
- data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +0 -7
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module ExternalClient
|
|
3
|
-
class Client
|
|
4
|
-
BASE_PATH = '/api/v1'.freeze
|
|
5
|
-
RATE_LIMIT = 100
|
|
6
|
-
|
|
7
|
-
def initialize(public_key:, private_key:, base_url:)
|
|
8
|
-
@public_key = public_key
|
|
9
|
-
@private_key = private_key
|
|
10
|
-
@base_url = base_url
|
|
11
|
-
@connection = build_connection
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
# Ticket Operations
|
|
15
|
-
|
|
16
|
-
# Get a specific ticket by ID
|
|
17
|
-
# @param id [String] The ticket ID
|
|
18
|
-
# @return [Resources::Ticket] The ticket object
|
|
19
|
-
def get_ticket!(id:)
|
|
20
|
-
response = @connection.get("#{BASE_PATH}/tickets/#{id}") do |req|
|
|
21
|
-
req.body = auth_params.to_json
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
handle_response(response, Resources::Ticket)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Get all tickets for a specific user
|
|
28
|
-
# @param user_id [String] The user ID
|
|
29
|
-
# @return [Array<Resources::Ticket>] Array of ticket objects
|
|
30
|
-
def get_tickets_by_user!(user_id:)
|
|
31
|
-
response = @connection.get("#{BASE_PATH}/tickets/user/#{user_id}") do |req|
|
|
32
|
-
req.body = auth_params.merge(user_id: user_id).to_json
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
handle_response(response, Resources::Ticket, collection: true)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Create a new ticket
|
|
39
|
-
# @param match_id [String] The match ID
|
|
40
|
-
# @param user_id [String] The user ID
|
|
41
|
-
# @param club_id [String] The club ID
|
|
42
|
-
# @param zone_id [String] The zone ID
|
|
43
|
-
# @param quantity [Integer] The quantity of tickets to create
|
|
44
|
-
# @return [Array<Resources::Ticket>] Array of created ticket objects
|
|
45
|
-
def create_tickets!(match_id:, user_id:, club_id:, zone_id:, quantity:)
|
|
46
|
-
params = {
|
|
47
|
-
match_id: match_id,
|
|
48
|
-
user_id: user_id,
|
|
49
|
-
club_id: club_id,
|
|
50
|
-
zone_id: zone_id,
|
|
51
|
-
quantity: quantity
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
response = @connection.post("#{BASE_PATH}/tickets") do |req|
|
|
55
|
-
req.body = auth_params.merge(params).to_json
|
|
56
|
-
req.headers['Content-Type'] = 'application/json'
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
handle_response(response, Resources::Ticket, collection: true)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Match Operations
|
|
63
|
-
|
|
64
|
-
# Get all available matches
|
|
65
|
-
# @return [Array<Resources::Match>] Array of match objects
|
|
66
|
-
def get_matches! # rubocop:disable Naming/AccessorMethodName
|
|
67
|
-
response = @connection.get("#{BASE_PATH}/matches") do |req|
|
|
68
|
-
req.params = auth_params
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
handle_response(response, Resources::Match, collection: true)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Get a specific match by ID
|
|
75
|
-
# @param id [String] The match ID
|
|
76
|
-
# @return [Resources::Match] The match object
|
|
77
|
-
def get_match!(id:)
|
|
78
|
-
response = @connection.get("#{BASE_PATH}/matches/#{id}") do |req|
|
|
79
|
-
req.params = auth_params
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
handle_response(response, Resources::Match)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
# Zone Operations
|
|
86
|
-
|
|
87
|
-
# Get available zones for a club and match
|
|
88
|
-
# @param club_id [String] The club ID
|
|
89
|
-
# @param match_id [String] The match ID
|
|
90
|
-
# @return [Array<Resources::Zone>] Array of zone objects
|
|
91
|
-
def get_zones!(club_id:, match_id:)
|
|
92
|
-
response = @connection.get("#{BASE_PATH}/zones/club") do |req|
|
|
93
|
-
req.params = auth_params.merge(
|
|
94
|
-
club_id: club_id,
|
|
95
|
-
match_id: match_id
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
handle_response(response, Resources::Zone, collection: true)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
private
|
|
103
|
-
|
|
104
|
-
def build_connection
|
|
105
|
-
Faraday.new(url: @base_url) do |faraday|
|
|
106
|
-
faraday.request :json
|
|
107
|
-
faraday.response :json, content_type: /\bjson$/
|
|
108
|
-
faraday.adapter Faraday.default_adapter
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def auth_params
|
|
113
|
-
{
|
|
114
|
-
public_key: @public_key,
|
|
115
|
-
private_key: @private_key
|
|
116
|
-
}
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def handle_response(response, model_class, collection: false)
|
|
120
|
-
if response.success?
|
|
121
|
-
data = response.body
|
|
122
|
-
unless data['success']
|
|
123
|
-
raise SpreeCmCommissioner::Integrations::ExternalClientError.new(
|
|
124
|
-
"API request failed: #{data['error']}",
|
|
125
|
-
response.status
|
|
126
|
-
)
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
collection ? model_class.from_collection(data) : model_class.new(data['data'])
|
|
130
|
-
else
|
|
131
|
-
handle_error_response(response)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def handle_error_response(response)
|
|
136
|
-
error_message = "API request failed with status #{response.status}"
|
|
137
|
-
|
|
138
|
-
# Response body is already parsed by Faraday's JSON middleware
|
|
139
|
-
error_body = response.body
|
|
140
|
-
|
|
141
|
-
if error_body.is_a?(Hash)
|
|
142
|
-
error = error_body['error'] || error_body['message'] || error_body
|
|
143
|
-
error_message = error.is_a?(Hash) ? error.to_json : error.to_s
|
|
144
|
-
else
|
|
145
|
-
error_message = error_body.to_s.presence || error_message
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
raise SpreeCmCommissioner::Integrations::ExternalClientError.new(error_message, response.status)
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
end
|
data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module Inventory
|
|
3
|
-
class UnstockInventory
|
|
4
|
-
prepend ::Spree::ServiceModule::Base
|
|
5
|
-
|
|
6
|
-
def call(integration:, order:, line_items:)
|
|
7
|
-
ApplicationRecord.transaction do
|
|
8
|
-
line_items.each do |line_item|
|
|
9
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'Invalid guests' if line_item.guests.size != line_item.quantity
|
|
10
|
-
|
|
11
|
-
sync_line_item!(integration, order, line_item)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
success(order: order, line_items: line_items)
|
|
15
|
-
end
|
|
16
|
-
rescue SpreeCmCommissioner::Integrations::SyncError,
|
|
17
|
-
SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
18
|
-
failure(nil, e.message)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def sync_line_item!(integration, order, line_item)
|
|
22
|
-
zone_mapping = line_item.product.integration_mappings.find_by(integration_id: integration.id)
|
|
23
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'Integration mapping not found for product' if zone_mapping.nil?
|
|
24
|
-
|
|
25
|
-
external_zone_id = zone_mapping.external_id
|
|
26
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'zone_id is required' if external_zone_id.blank?
|
|
27
|
-
|
|
28
|
-
external_match_id = zone_mapping.external_payload&.dig('match_id')
|
|
29
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'match_id is required' if external_match_id.blank?
|
|
30
|
-
|
|
31
|
-
external_club_id = zone_mapping.external_payload&.dig('club_id')
|
|
32
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'club_id is required' if external_club_id.blank?
|
|
33
|
-
|
|
34
|
-
tickets = integration.client.create_tickets!(
|
|
35
|
-
match_id: external_match_id,
|
|
36
|
-
user_id: order.number,
|
|
37
|
-
club_id: external_club_id,
|
|
38
|
-
zone_id: external_zone_id,
|
|
39
|
-
quantity: line_item.quantity
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# External system did not create the correct number of tickets, we cannot proceed as it would lead to data inconsistency between systems.
|
|
43
|
-
# We raise an error to rollback the transaction & we can manually investigate the issue.
|
|
44
|
-
if tickets.size != line_item.quantity
|
|
45
|
-
raise SpreeCmCommissioner::Integrations::SyncError,
|
|
46
|
-
"Created tickets count (#{tickets.size}) does not match line item quantity (#{line_item.quantity})"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
sync_guests!(line_item.guests, tickets, integration)
|
|
50
|
-
|
|
51
|
-
line_item_mapping = line_item.integration_mappings.find_or_initialize_by(
|
|
52
|
-
integration: integration,
|
|
53
|
-
external_id: tickets.map(&:_id).sort.join(',')
|
|
54
|
-
)
|
|
55
|
-
line_item_mapping.external_qr_data = group_ticket_qr_data(tickets)
|
|
56
|
-
line_item_mapping.mark_as_active!(external_payload: {})
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def sync_guests!(guests, tickets, integration)
|
|
60
|
-
guests.each_with_index do |guest, index|
|
|
61
|
-
ticket = tickets[index]
|
|
62
|
-
next if ticket.blank?
|
|
63
|
-
|
|
64
|
-
guest_mapping = guest.integration_mappings.find_or_initialize_by(
|
|
65
|
-
external_id: ticket._id,
|
|
66
|
-
integration: integration
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
guest_mapping.external_qr_data = ticket._id
|
|
70
|
-
guest_mapping.mark_as_active!(external_payload: ticket.to_h)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def group_ticket_qr_data(tickets)
|
|
75
|
-
{
|
|
76
|
-
match_id: tickets.first.match_id,
|
|
77
|
-
ticket_ids: tickets.map(&:_id).sort,
|
|
78
|
-
is_online: true
|
|
79
|
-
}.to_json
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module Polling
|
|
3
|
-
class SyncMatches
|
|
4
|
-
def initialize(client:, integration:, sync_result: nil)
|
|
5
|
-
@client = client
|
|
6
|
-
@integration = integration
|
|
7
|
-
@sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call
|
|
11
|
-
synced_match_mappings = sync_matches!
|
|
12
|
-
cleanup_stale_mappings!(synced_match_mappings)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Sync all available matches from the API and create them as events
|
|
16
|
-
def sync_matches!
|
|
17
|
-
external_matches = begin
|
|
18
|
-
@sync_result.track_api_call('get_matches') { @client.get_matches! }
|
|
19
|
-
rescue SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
20
|
-
raise SpreeCmCommissioner::Integrations::SyncError, "Failed to sync matches: #{e.message}"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
external_matches.map do |external_match|
|
|
24
|
-
sync_match!(external_match)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Sync a single match and create it as an event taxon under events/
|
|
29
|
-
# Match data might be incomplete in the matches list, so we fetch full details
|
|
30
|
-
def sync_match!(external_match)
|
|
31
|
-
external_match = begin
|
|
32
|
-
@sync_result.track_api_call('get_match_details') { @client.get_match!(id: external_match._id) }
|
|
33
|
-
rescue SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
34
|
-
raise SpreeCmCommissioner::Integrations::SyncError, "Failed to sync matches: #{e.message}"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
match_mapping = Spree::Taxon.find_or_initialize_integration_mapping(integration_id: @integration.id, external_id: external_match._id)
|
|
38
|
-
match_taxon = match_mapping.internal
|
|
39
|
-
|
|
40
|
-
# Track match sync and create event taxon directly under events/
|
|
41
|
-
@sync_result.track(:match, match_taxon) do |tracker|
|
|
42
|
-
match_taxon.assign_attributes(
|
|
43
|
-
vendor: @integration.vendor,
|
|
44
|
-
parent: events_root_taxon,
|
|
45
|
-
taxonomy: events_taxonomy,
|
|
46
|
-
name: "#{external_match.home_name} vs #{external_match.away_name}",
|
|
47
|
-
kind: :event,
|
|
48
|
-
from_date: external_match.match_datetime,
|
|
49
|
-
to_date: external_match.match_datetime + 120.minutes,
|
|
50
|
-
|
|
51
|
-
# seo
|
|
52
|
-
meta_title: external_match.meta_title,
|
|
53
|
-
meta_description: external_match.meta_description,
|
|
54
|
-
|
|
55
|
-
# Sync common match fields for cross-integration compatibility
|
|
56
|
-
# Only include fields that other integrations typically provide to avoid data gaps.
|
|
57
|
-
public_metadata: {
|
|
58
|
-
home_name: external_match.home_name,
|
|
59
|
-
home_score: external_match.home_score,
|
|
60
|
-
home_logo_url: external_match.home_logo,
|
|
61
|
-
away_name: external_match.away_name,
|
|
62
|
-
away_score: external_match.away_score,
|
|
63
|
-
away_logo_url: external_match.away_logo,
|
|
64
|
-
stadium: external_match.stadium,
|
|
65
|
-
league_name: external_match.league&.name,
|
|
66
|
-
league_logo_url: external_match.league&.logo
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
tracker.save_if_changed!(match_taxon)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
match_mapping.mark_as_active!(external_payload: external_match.to_h)
|
|
73
|
-
match_mapping
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Archive stale match mappings that are no longer in the external API
|
|
77
|
-
# Finds all active event taxon mappings for this integration and archives any
|
|
78
|
-
# that weren't included in the latest API response
|
|
79
|
-
def cleanup_stale_mappings!(synced_match_mappings)
|
|
80
|
-
synced_external_match_ids = synced_match_mappings.map(&:external_id)
|
|
81
|
-
|
|
82
|
-
# Find all active match mappings (event taxons) for this integration
|
|
83
|
-
active_match_mappings = SpreeCmCommissioner::IntegrationMapping.where(
|
|
84
|
-
integration_id: @integration.id,
|
|
85
|
-
internal_type: 'Spree::Taxon',
|
|
86
|
-
status: :active
|
|
87
|
-
).where(internal_id: Spree::Taxon.where(kind: :event).select(:id))
|
|
88
|
-
|
|
89
|
-
# Archive matches that are no longer in the API response
|
|
90
|
-
active_match_mappings.find_each do |mapping|
|
|
91
|
-
next if synced_external_match_ids.include?(mapping.external_id)
|
|
92
|
-
|
|
93
|
-
# Discontinue all products associated with this match
|
|
94
|
-
mapping.internal.event_products.find_each do |product|
|
|
95
|
-
product.discontinue!
|
|
96
|
-
@sync_result.increment_metric(:zone, :discontinued)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
mapping.mark_as_archived!
|
|
100
|
-
@sync_result.increment_metric(:match, :archived)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def events_root_taxon
|
|
105
|
-
events_taxonomy.root
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def events_taxonomy
|
|
109
|
-
@events_taxonomy ||= Spree::Taxonomy.events
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module Polling
|
|
3
|
-
class SyncZones
|
|
4
|
-
def initialize(client:, integration:, sync_result:)
|
|
5
|
-
@client = client
|
|
6
|
-
@integration = integration
|
|
7
|
-
@sync_result = sync_result
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def call(external_match_id:)
|
|
11
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'external_match_id is required' if external_match_id.nil?
|
|
12
|
-
|
|
13
|
-
match_mapping = Spree::Taxon.find_integration_mapping(integration_id: @integration.id, external_id: external_match_id)
|
|
14
|
-
raise SpreeCmCommissioner::Integrations::SyncError, "Match not found: #{external_match_id}" if match_mapping.blank?
|
|
15
|
-
|
|
16
|
-
synced_zone_mappings = sync_zones!(match_mapping)
|
|
17
|
-
cleanup_stale_mappings!(match_mapping, synced_zone_mappings)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Sync zones for a specific club and match
|
|
21
|
-
def sync_zones!(match_mapping)
|
|
22
|
-
external_club_id = match_mapping.external_payload&.dig('club_id')
|
|
23
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'club_id is required' if external_club_id.blank?
|
|
24
|
-
|
|
25
|
-
external_zones = begin
|
|
26
|
-
@sync_result.track_api_call('get_zones') { @client.get_zones!(club_id: external_club_id, match_id: match_mapping.external_id) }
|
|
27
|
-
rescue SpreeCmCommissioner::Integrations::ExternalClientError => e
|
|
28
|
-
raise SpreeCmCommissioner::Integrations::SyncError, "Failed to sync zones match #{match_mapping.external_id}: #{e.message}"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# match ID is not returned by API, but it is important info when creating tickets in the external system
|
|
32
|
-
# We need to set it manually on each zone object so that it can be used when creating tickets.
|
|
33
|
-
# This is a temporary workaround until the API returns the match ID
|
|
34
|
-
external_zones.each do |zone|
|
|
35
|
-
zone.set_match_id(match_mapping.external_id)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
taxon_match_section = match_mapping.internal.children.first_or_create! { |s| s.name = 'Zones' }
|
|
39
|
-
synced_zone_mappings = external_zones.map do |external_zone|
|
|
40
|
-
sync_zone!(external_zone, match_mapping.internal, taxon_match_section)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
CmAppLogger.log(
|
|
44
|
-
label: 'SyncZones#sync_zones! Zones synced for match',
|
|
45
|
-
data: { external_match_id: match_mapping.external_id }
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
synced_zone_mappings
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def sync_zone!(external_zone, internal_match, taxon_match_section)
|
|
54
|
-
Spree::Product.transaction do
|
|
55
|
-
product_mapping = Spree::Product.find_or_initialize_integration_mapping(integration_id: @integration.id, external_id: external_zone._id)
|
|
56
|
-
product = product_mapping.internal
|
|
57
|
-
|
|
58
|
-
zone_option_type = Spree::OptionType.ticket_type
|
|
59
|
-
color_option_type = external_zone.color_brand.present? ? Spree::OptionType.color : nil
|
|
60
|
-
|
|
61
|
-
# Track zone sync
|
|
62
|
-
@sync_result.track(:zone, product) do |tracker|
|
|
63
|
-
product.assign_attributes(
|
|
64
|
-
status: :active,
|
|
65
|
-
tenant: @integration.tenant,
|
|
66
|
-
vendor: @integration.vendor,
|
|
67
|
-
product_type: :ecommerce,
|
|
68
|
-
event: internal_match,
|
|
69
|
-
taxons: [taxon_match_section],
|
|
70
|
-
name: external_zone.title.strip,
|
|
71
|
-
price: external_zone.online_price,
|
|
72
|
-
available_on: external_zone.start_datetime,
|
|
73
|
-
discontinue_on: external_zone.end_datetime,
|
|
74
|
-
option_types: [zone_option_type, color_option_type].compact,
|
|
75
|
-
shipping_category: Spree::ShippingCategory.first,
|
|
76
|
-
stores: [Spree::Store.default]
|
|
77
|
-
)
|
|
78
|
-
tracker.save_if_changed!(product)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
product_mapping.mark_as_active!(external_payload: external_zone.to_h)
|
|
82
|
-
|
|
83
|
-
ensure_variant_with_zone_option_value!(product, external_zone, zone_option_type, color_option_type)
|
|
84
|
-
ensure_inventory!(product.variants[0], external_zone)
|
|
85
|
-
|
|
86
|
-
product_mapping
|
|
87
|
-
end
|
|
88
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
89
|
-
CmAppLogger.error(
|
|
90
|
-
label: 'SyncZones#sync_zone Failed to save zone',
|
|
91
|
-
data: { zone_id: external_zone._id, error: e.message, backtrace: e.backtrace }
|
|
92
|
-
)
|
|
93
|
-
raise SpreeCmCommissioner::Integrations::SyncError, "Failed to save zone #{external_zone.title}: #{e.message}"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def ensure_variant_with_zone_option_value!(product, external_zone, zone_option_type, color_option_type)
|
|
97
|
-
option_values = [Spree::OptionValue.find_or_create_by_name!(zone_option_type, external_zone.title)]
|
|
98
|
-
|
|
99
|
-
if color_option_type.present? && external_zone.color_brand.present?
|
|
100
|
-
option_values << Spree::OptionValue.find_or_create_by_name!(color_option_type, external_zone.color_brand)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
variant = product.variants.first_or_initialize
|
|
104
|
-
|
|
105
|
-
# Track variant sync
|
|
106
|
-
@sync_result.track(:variant, variant) do |tracker|
|
|
107
|
-
variant.assign_attributes(
|
|
108
|
-
vendor: @integration.vendor,
|
|
109
|
-
track_inventory: true,
|
|
110
|
-
product_type: :ecommerce,
|
|
111
|
-
price: external_zone.online_price,
|
|
112
|
-
discontinue_on: external_zone.end_datetime,
|
|
113
|
-
option_values: option_values
|
|
114
|
-
)
|
|
115
|
-
tracker.save_if_changed!(variant)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Synchronize variant inventory with external StadiumX API
|
|
120
|
-
# Calculates the difference between external available seats and system inventory,
|
|
121
|
-
# then creates a stock movement to align system inventory with the external source.
|
|
122
|
-
#
|
|
123
|
-
# EXECUTION FLOW:
|
|
124
|
-
# 1. Calculate quantity difference between external and system inventory
|
|
125
|
-
# 2. Create StockMovement via StockMovementCreator (if difference != 0)
|
|
126
|
-
# 3. StockMovement.save triggers after_create callback → update_stock_item_quantity
|
|
127
|
-
# 4. StockItem.adjust_count_on_hand(quantity) updates count_on_hand
|
|
128
|
-
# 5. StockItem.save triggers after_commit callback → create_inventory_items (on first create)
|
|
129
|
-
# 6. InventoryItemsGeneratorJob → InventoryItemsGenerator → create_default_non_permanent_inventory_item!
|
|
130
|
-
# 7. Creates InventoryItem with quantity_available = stock_item.count_on_hand
|
|
131
|
-
# 8. StockMovementCreator enqueues InventoryItemsAdjusterJob (async)
|
|
132
|
-
# 9. InventoryItemsAdjuster.adjust_quantity!(quantity) updates existing inventory_items
|
|
133
|
-
# 10. Updates both DB (quantity_available, max_capacity) and Redis cache
|
|
134
|
-
#
|
|
135
|
-
# IMPORTANT: Inventory items are created AFTER stock item is saved (via callback).
|
|
136
|
-
# On first sync, the flow is: StockMovement → StockItem → InventoryItem creation → InventoryItem adjustment.
|
|
137
|
-
# On subsequent syncs: StockMovement → StockItem → InventoryItem adjustment only.
|
|
138
|
-
#
|
|
139
|
-
# Examples:
|
|
140
|
-
#
|
|
141
|
-
# 1. First sync (no inventory items yet):
|
|
142
|
-
# - External available seats: 50
|
|
143
|
-
# - System inventory: 0 (no inventory_items created)
|
|
144
|
-
# - Stock movement created: +50 (initialize system to 50)
|
|
145
|
-
# - Flow: StockMovement(+50) → StockItem.count_on_hand=50 → InventoryItem created with quantity=50 → Adjuster adjusts by +50
|
|
146
|
-
#
|
|
147
|
-
# 2. System has more inventory than external:
|
|
148
|
-
# - External available seats: 20
|
|
149
|
-
# - System inventory: 30
|
|
150
|
-
# - Stock movement created: -10 (adjust system down to 20)
|
|
151
|
-
# - Flow: StockMovement(-10) → StockItem.count_on_hand=20 → Adjuster adjusts inventory_item by -10
|
|
152
|
-
#
|
|
153
|
-
# 3. System has less inventory than external:
|
|
154
|
-
# - External available seats: 50
|
|
155
|
-
# - System inventory: 30
|
|
156
|
-
# - Stock movement created: +20 (adjust system up to 50)
|
|
157
|
-
# - Flow: StockMovement(+20) → StockItem.count_on_hand=50 → Adjuster adjusts inventory_item by +20
|
|
158
|
-
#
|
|
159
|
-
# 4. Already in sync:
|
|
160
|
-
# - External available seats: 25
|
|
161
|
-
# - System inventory: 25
|
|
162
|
-
# - Stock movement created: 0 (no change needed)
|
|
163
|
-
# - Flow: Early return, no operations performed
|
|
164
|
-
def ensure_inventory!(variant, external_zone)
|
|
165
|
-
raise ArgumentError, 'variant cannot be nil' if variant.nil? || variant.id.blank?
|
|
166
|
-
raise ArgumentError, 'external_zone cannot be nil' if external_zone.nil?
|
|
167
|
-
|
|
168
|
-
# NOTE: StadiumX zones have no permanent stock or date-based inventory, so we expect
|
|
169
|
-
# only one inventory item (index 0) per variant. Uses safe navigation to handle cases
|
|
170
|
-
# where inventory items haven't been created yet (first sync).
|
|
171
|
-
stock_location = variant.vendor.stock_locations.first || variant.vendor.send(:create_stock_location)
|
|
172
|
-
system_quantity = variant.inventory_items.active[0]&.quantity_in_redis || 0
|
|
173
|
-
external_quantity = external_zone.available_seats || 0
|
|
174
|
-
|
|
175
|
-
quantity_difference = external_quantity - system_quantity
|
|
176
|
-
return if quantity_difference.zero?
|
|
177
|
-
|
|
178
|
-
context = SpreeCmCommissioner::Stock::StockMovementCreator.call(
|
|
179
|
-
variant_id: variant.id,
|
|
180
|
-
stock_location_id: stock_location.id,
|
|
181
|
-
current_store: Spree::Store.default,
|
|
182
|
-
stock_movement_params: { quantity: quantity_difference }
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
return unless context.success?
|
|
186
|
-
|
|
187
|
-
@sync_result.increment_metric(:inventory, :adjusted, quantity_difference.abs)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Archive stale zone mappings that are no longer in the external API
|
|
191
|
-
# Finds all active zone mappings for this integration and archives any
|
|
192
|
-
# that weren't included in the latest API response.
|
|
193
|
-
def cleanup_stale_mappings!(match_mapping, synced_zone_mappings)
|
|
194
|
-
synced_external_zone_ids = synced_zone_mappings.map(&:external_id)
|
|
195
|
-
|
|
196
|
-
# Find all active zone mappings for this integration and match
|
|
197
|
-
active_zone_mappings = SpreeCmCommissioner::IntegrationMapping.where(
|
|
198
|
-
integration_id: @integration.id,
|
|
199
|
-
internal_type: 'Spree::Product',
|
|
200
|
-
status: :active
|
|
201
|
-
).where(internal_id: Spree::Product.where(event_id: match_mapping.internal_id).select(:id))
|
|
202
|
-
|
|
203
|
-
# Archive zones that are no longer in the API response
|
|
204
|
-
active_zone_mappings.find_each do |mapping|
|
|
205
|
-
next if synced_external_zone_ids.include?(mapping.external_id)
|
|
206
|
-
|
|
207
|
-
mapping.internal.discontinue!
|
|
208
|
-
|
|
209
|
-
mapping.mark_as_archived!
|
|
210
|
-
@sync_result.increment_metric(:zone, :discontinued)
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module Resources
|
|
3
|
-
class Base
|
|
4
|
-
class << self
|
|
5
|
-
def from_collection(data)
|
|
6
|
-
return [] unless data['data'].is_a?(Array)
|
|
7
|
-
|
|
8
|
-
data['data'].map { |item| new(item) }
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def initialize(attributes = {})
|
|
13
|
-
attributes.each do |key, value|
|
|
14
|
-
setter = "#{key}="
|
|
15
|
-
send(setter, value) if respond_to?(setter)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
class SpreeCmCommissioner::Integrations::StadiumXV1
|
|
2
|
-
module Resources
|
|
3
|
-
class League < Base
|
|
4
|
-
attr_accessor :_id, :name, :logo
|
|
5
|
-
|
|
6
|
-
def initialize(attributes = {})
|
|
7
|
-
super(attributes)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def to_h
|
|
11
|
-
{
|
|
12
|
-
_id: _id,
|
|
13
|
-
name: name,
|
|
14
|
-
logo: logo
|
|
15
|
-
}.compact
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|