spree_cm_commissioner 2.5.0.pre.pre6 → 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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +1 -1
  5. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +7 -1
  6. data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +9 -9
  7. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +10 -10
  8. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +10 -14
  9. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  10. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +4 -4
  11. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -15
  12. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -2
  13. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +2 -14
  14. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +3 -0
  15. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +7 -1
  16. data/app/models/spree_cm_commissioner/guest.rb +0 -13
  17. data/app/models/spree_cm_commissioner/inventory_item.rb +1 -5
  18. data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -13
  19. data/app/models/spree_cm_commissioner/option_type_decorator.rb +0 -8
  20. data/app/models/spree_cm_commissioner/option_value_decorator.rb +0 -34
  21. data/app/models/spree_cm_commissioner/order_decorator.rb +0 -1
  22. data/app/models/spree_cm_commissioner/product_decorator.rb +2 -3
  23. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  24. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  25. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  26. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -13
  27. data/app/models/spree_cm_commissioner/variant_decorator.rb +4 -7
  28. data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
  29. data/app/presenters/spree/variants/{visible_options_presenter.rb → visable_options_presenter.rb} +4 -2
  30. data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +12 -0
  31. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +5 -0
  32. data/app/views/spree/admin/tenants/_form.html.erb +18 -0
  33. data/app/views/spree/admin/tenants/form/_footer.html.erb +31 -0
  34. data/app/views/spree/admin/tenants/form/_social.html.erb +31 -0
  35. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  36. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
  37. data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +13 -6
  38. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +23 -24
  39. data/config/locales/en.yml +0 -8
  40. data/config/locales/km.yml +0 -8
  41. data/config/routes.rb +0 -8
  42. data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +9 -0
  43. data/lib/cm_app_logger.rb +0 -1
  44. data/lib/spree_cm_commissioner/version.rb +1 -1
  45. data/lib/spree_cm_commissioner.rb +7 -8
  46. metadata +7 -57
  47. data/app/controllers/spree/admin/integration_mappings_controller.rb +0 -21
  48. data/app/controllers/spree/admin/integration_sessions_controller.rb +0 -21
  49. data/app/controllers/spree/admin/integrations_controller.rb +0 -83
  50. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +0 -15
  51. data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +0 -10
  52. data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +0 -4
  53. data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -15
  54. data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +0 -58
  55. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +0 -39
  56. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +0 -53
  57. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +0 -30
  58. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +0 -17
  59. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +0 -61
  60. data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +0 -36
  61. data/app/models/concerns/spree_cm_commissioner/order_integration.rb +0 -33
  62. data/app/models/spree_cm_commissioner/integration.rb +0 -29
  63. data/app/models/spree_cm_commissioner/integration_mapping.rb +0 -41
  64. data/app/models/spree_cm_commissioner/integration_sync_session.rb +0 -15
  65. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +0 -37
  66. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +0 -6
  67. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +0 -69
  68. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +0 -183
  69. data/app/services/spree_cm_commissioner/integrations/polling.rb +0 -70
  70. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +0 -79
  71. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +0 -152
  72. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +0 -83
  73. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +0 -113
  74. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +0 -215
  75. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +0 -20
  76. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +0 -19
  77. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +0 -95
  78. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +0 -81
  79. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +0 -19
  80. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +0 -90
  81. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -35
  82. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +0 -38
  83. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +0 -44
  84. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +0 -16
  85. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +0 -49
  86. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +0 -107
  87. data/app/views/spree/admin/integration_mappings/index.html.erb +0 -33
  88. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +0 -116
  89. data/app/views/spree/admin/integration_sessions/index.html.erb +0 -42
  90. data/app/views/spree/admin/integrations/_form.html.erb +0 -104
  91. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +0 -29
  92. data/app/views/spree/admin/integrations/edit.html.erb +0 -45
  93. data/app/views/spree/admin/integrations/index.html.erb +0 -75
  94. data/app/views/spree/admin/integrations/new.html.erb +0 -25
  95. data/db/migrate/20251017094845_create_cm_integrations.rb +0 -22
  96. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +0 -68
  97. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +0 -52
  98. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +0 -25
  99. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +0 -14
  100. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +0 -7
@@ -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
@@ -1,95 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::StadiumXV1
2
- module Resources
3
- class Match < Base
4
- attr_accessor :_id,
5
- :home_name,
6
- :home_logo,
7
- :away_name,
8
- :away_logo,
9
- :home_score,
10
- :away_score,
11
- :date,
12
- :time,
13
- :stadium,
14
- :week,
15
- :league_id,
16
- :club_id,
17
- :discount_ticket,
18
- :on_sale,
19
- :is_ended,
20
- :allow_predition,
21
- :file,
22
- :created_by,
23
- :updated_by,
24
- :zones,
25
- :promotion,
26
- :poster,
27
- :league
28
-
29
- def initialize(attributes = {})
30
- super(attributes)
31
-
32
- @on_sale = attributes['on_sale'] != false
33
- @is_ended = attributes['is_ended'] == true || attributes['is_ended'] == 'true'
34
- @allow_predition = attributes['allow_predition'] == true || attributes['allow_predition'] == 'true'
35
- @league = League.new(attributes['league']) if attributes['league'].is_a?(Hash)
36
- end
37
-
38
- def meta_title
39
- return nil if home_name.blank? || away_name.blank?
40
-
41
- "#{home_name} vs #{away_name} - #{display_match_datetime}"
42
- end
43
-
44
- def meta_description
45
- return nil if home_name.blank? || away_name.blank?
46
-
47
- location = stadium.present? ? " at #{stadium}" : ''
48
- league_info = league&.name.present? ? " - #{league.name}" : ''
49
-
50
- "#{home_name} vs #{away_name}#{league_info} on #{display_match_datetime}#{location}. Book tickets now!"
51
- end
52
-
53
- def display_match_datetime
54
- match_datetime&.strftime('%B %d, %Y at %I:%M %p')
55
- end
56
-
57
- def match_datetime
58
- return nil unless @date && @time
59
-
60
- Time.zone.parse("#{@date} #{@time}")
61
- rescue StandardError
62
- nil
63
- end
64
-
65
- def to_h
66
- {
67
- _id: _id,
68
- home_name: home_name,
69
- home_logo: home_logo,
70
- away_name: away_name,
71
- away_logo: away_logo,
72
- home_score: home_score,
73
- away_score: away_score,
74
- date: date,
75
- time: time,
76
- stadium: stadium,
77
- week: week,
78
- league_id: league_id,
79
- club_id: club_id,
80
- discount_ticket: discount_ticket,
81
- on_sale: on_sale,
82
- is_ended: is_ended,
83
- allow_predition: allow_predition,
84
- file: file,
85
- created_by: created_by,
86
- updated_by: updated_by,
87
- zones: zones&.map(&:to_h),
88
- promotion: promotion,
89
- poster: poster,
90
- league: league&.to_h
91
- }.compact
92
- end
93
- end
94
- end
95
- end
@@ -1,81 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::StadiumXV1
2
- module Resources
3
- class Ticket < Base
4
- attr_accessor :_id,
5
- :match_id,
6
- :uid,
7
- :first_uid,
8
- :generated_by,
9
- :club_id,
10
- :zone_id,
11
- :league_id,
12
- :token_id,
13
- :payment_method,
14
- :ticket_type,
15
- :remark,
16
- :is_used,
17
- :is_online,
18
- :used_at,
19
- :is_expired,
20
- :created_at,
21
- :updated_at,
22
- :cashback,
23
- :__v
24
-
25
- def initialize(attributes = {})
26
- super(attributes)
27
-
28
- @is_used = attributes['is_used'] == true || attributes['is_used'] == 'true'
29
- @is_online = attributes['is_online'] != false
30
- @is_expired = attributes['is_expired'] == true || attributes['is_expired'] == 'true'
31
- @created_at = parse_timestamp(attributes['created_at'])
32
- @updated_at = parse_timestamp(attributes['updated_at'])
33
- @used_at = parse_timestamp(attributes['used_at'])
34
- end
35
-
36
- def to_h
37
- {
38
- _id: _id,
39
- match_id: match_id,
40
- uid: uid,
41
- first_uid: first_uid,
42
- generated_by: generated_by,
43
- club_id: club_id,
44
- zone_id: zone_id,
45
- league_id: league_id,
46
- token_id: token_id,
47
- payment_method: payment_method,
48
- ticket_type: ticket_type,
49
- remark: remark,
50
- is_used: is_used,
51
- is_online: is_online,
52
- used_at: used_at&.iso8601,
53
- is_expired: is_expired,
54
- created_at: created_at&.iso8601,
55
- updated_at: updated_at&.iso8601,
56
- cashback: cashback,
57
- __v: __v
58
- }.compact
59
- end
60
-
61
- private
62
-
63
- # Parses timestamp values from the Stadium X API response.
64
- # The API returns timestamps in two formats:
65
- # 1. Hash format with 'low' and 'high' properties (Long type from backend): {"low"=>239159874, "high"=>411, "unsigned"=>false}
66
- # 2. Numeric format (milliseconds since epoch): 1702353644000
67
- # Converts millisecond timestamps to Time objects in the application's timezone.
68
- # Returns nil if the value is blank or in an unsupported format.
69
- def parse_timestamp(value)
70
- return nil if value.blank?
71
-
72
- if value.is_a?(Hash)
73
- timestamp_ms = (value['high'].to_i << 32) | value['low'].to_i
74
- Time.zone.at(timestamp_ms / 1000)
75
- elsif value.is_a?(Numeric)
76
- Time.zone.at(value / 1000)
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,19 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::StadiumXV1
2
- module Resources
3
- class TicketImage < Base
4
- attr_accessor :zone_id,
5
- :ticket_image
6
-
7
- def initialize(attributes = {})
8
- super(attributes)
9
- end
10
-
11
- def to_h
12
- {
13
- zone_id: zone_id,
14
- ticket_image: ticket_image
15
- }.compact
16
- end
17
- end
18
- end
19
- end
@@ -1,90 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::StadiumXV1
2
- module Resources
3
- class Zone < Base
4
- attr_accessor :_id,
5
- :title,
6
- :color_brand,
7
- :total_seats,
8
- :online_price,
9
- :walkin_price,
10
- :match_id,
11
- :club_id,
12
- :is_active,
13
- :created_by,
14
- :updated_by,
15
- :created_at,
16
- :updated_at,
17
- :end_datetime,
18
- :is_earlybird,
19
- :start_datetime,
20
- :available_seats,
21
- :ticket_image
22
-
23
- def initialize(attributes = {})
24
- super(attributes)
25
-
26
- @total_seats = attributes['total_seats'].to_i
27
- @available_seats = attributes['available_seats'].to_i
28
- @online_price = attributes['online_price'].to_f
29
- @walkin_price = attributes['walkin_price'].to_f
30
- @is_active = attributes['is_active'] != false
31
- @color_brand = valid_hex_color?(attributes['color_brand']) ? attributes['color_brand'].upcase : nil
32
-
33
- @created_at = Time.zone.at(attributes['created_at'] / 1000) if attributes['created_at'].present?
34
- @updated_at = Time.zone.at(attributes['updated_at'] / 1000) if attributes['updated_at'].present?
35
-
36
- @start_datetime = Time.zone.parse(attributes['start_datetime']) if attributes['start_datetime'].present?
37
- @end_datetime = Time.zone.parse(attributes['end_datetime']) if attributes['end_datetime'].present?
38
- @ticket_image = TicketImage.new(attributes['ticket_image']) if attributes['ticket_image'].is_a?(Hash)
39
- end
40
-
41
- # Manually set the match_id on the zone object.
42
- # This is needed because the API does not return match_id but it's required for ticket creation.
43
- # This is a temporary workaround until the API returns the match_id.
44
- def set_match_id(match_id) # rubocop:disable Naming/AccessorMethodName
45
- @match_id = match_id
46
- end
47
-
48
- def to_hash
49
- hash = {
50
- '_id' => _id,
51
- 'title' => title,
52
- 'color_brand' => color_brand,
53
- 'total_seats' => total_seats,
54
- 'online_price' => online_price,
55
- 'walkin_price' => walkin_price,
56
- 'match_id' => match_id,
57
- 'club_id' => club_id,
58
- 'is_active' => is_active,
59
- 'created_by' => created_by,
60
- 'updated_by' => updated_by,
61
- 'created_at' => created_at&.iso8601,
62
- 'updated_at' => updated_at&.iso8601,
63
- 'end_datetime' => end_datetime&.iso8601,
64
- 'is_earlybird' => is_earlybird,
65
- 'start_datetime' => start_datetime&.iso8601,
66
- 'available_seats' => available_seats,
67
- 'ticket_image' => ticket_image&.to_h
68
- }.compact
69
-
70
- hash['ticket_image'] = hash['ticket_image'].transform_keys(&:to_s) if hash['ticket_image']
71
-
72
- hash
73
- end
74
-
75
- alias to_h to_hash
76
-
77
- private
78
-
79
- # Validates if a color value is a valid hex color code.
80
- # Accepts formats: #RRGGBB, #RRGGBBAA, RRGGBB, RRGGBBAA (with or without # prefix)
81
- # Returns true only if the value is a valid hex color, false otherwise.
82
- def valid_hex_color?(value)
83
- return false if value.blank?
84
-
85
- hex_pattern = /\A#?([0-9a-fA-F]{6}|[0-9a-fA-F]{8})\z/
86
- hex_pattern.match?(value.to_s)
87
- end
88
- end
89
- end
90
- end