spree_cm_commissioner 2.5.0.pre.pre8 → 2.5.0.pre.pre10

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 (128) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +3 -2
  3. data/.github/workflows/test_and_build_gem.yml +123 -57
  4. data/.tool-versions +1 -1
  5. data/Gemfile.lock +1 -1
  6. data/Rakefile +55 -29
  7. data/app/controllers/spree/admin/base_controller_decorator.rb +3 -3
  8. data/app/controllers/spree/admin/base_import_orders_controller.rb +6 -1
  9. data/app/controllers/spree/admin/classifications_controller.rb +1 -1
  10. data/app/controllers/spree/admin/integration_mappings_controller.rb +21 -0
  11. data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
  12. data/app/controllers/spree/admin/integrations_controller.rb +83 -0
  13. data/app/controllers/spree/admin/notification_sender_controller.rb +1 -1
  14. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
  15. data/app/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +6 -6
  16. data/app/controllers/spree/api/v2/storefront/trips_controller.rb +11 -0
  17. data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +10 -0
  18. data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +4 -0
  19. data/app/finders/spree_cm_commissioner/events/find_matches.rb +15 -0
  20. data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
  21. data/app/interactors/spree_cm_commissioner/create_event.rb +1 -1
  22. data/app/interactors/spree_cm_commissioner/customer_notification_cron_executor.rb +1 -1
  23. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  24. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +3 -2
  25. data/app/jobs/spree_cm_commissioner/conversion_pre_calculator_job.rb +2 -2
  26. data/app/jobs/spree_cm_commissioner/customer_notification_sender_job.rb +3 -3
  27. data/app/jobs/spree_cm_commissioner/enqueue_cart/add_item_job.rb +7 -7
  28. data/app/jobs/spree_cm_commissioner/ensure_event_for_product_line_item_guests_job.rb +1 -1
  29. data/app/jobs/spree_cm_commissioner/event_line_items_date_syncer_job.rb +2 -2
  30. data/app/jobs/spree_cm_commissioner/export_csv_job.rb +2 -2
  31. data/app/jobs/spree_cm_commissioner/import_order_job.rb +5 -5
  32. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
  33. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
  34. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
  35. data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +2 -2
  36. data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +8 -2
  37. data/app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb +7 -3
  38. data/app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb +6 -2
  39. data/app/jobs/spree_cm_commissioner/order_complete_telegram_sender_job.rb +2 -2
  40. data/app/jobs/spree_cm_commissioner/product_event_id_to_children_syncer_job.rb +2 -2
  41. data/app/jobs/spree_cm_commissioner/reports_assigner_job.rb +2 -2
  42. data/app/jobs/spree_cm_commissioner/sms_pin_code_job.rb +1 -1
  43. data/app/jobs/spree_cm_commissioner/state_job.rb +2 -2
  44. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +6 -3
  45. data/app/jobs/spree_cm_commissioner/stock/inventory_items_generator_job.rb +2 -2
  46. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
  47. data/app/jobs/spree_cm_commissioner/transit/route_fulfilled_order_count_incrementer_job.rb +2 -2
  48. data/app/jobs/spree_cm_commissioner/transit/route_order_count_incrementer_job.rb +2 -2
  49. data/app/jobs/spree_cm_commissioner/transit/route_previous_trip_count_decrementer_job.rb +2 -2
  50. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_decrementer_job.rb +2 -2
  51. data/app/jobs/spree_cm_commissioner/transit/route_trip_count_incrementer_job.rb +2 -2
  52. data/app/jobs/spree_cm_commissioner/vendor_creation_telegram_alert_sender_job.rb +2 -2
  53. data/app/jobs/spree_cm_commissioner/vendor_job.rb +2 -2
  54. data/app/jobs/spree_cm_commissioner/waiting_room_session_firebase_logger_job.rb +1 -1
  55. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +61 -0
  56. data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +36 -0
  57. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +16 -4
  58. data/app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb +1 -1
  59. data/app/models/concerns/spree_cm_commissioner/order_integration.rb +33 -0
  60. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +4 -2
  61. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
  62. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
  63. data/app/models/spree_cm_commissioner/export.rb +1 -1
  64. data/app/models/spree_cm_commissioner/guest.rb +13 -0
  65. data/app/models/spree_cm_commissioner/integration.rb +29 -0
  66. data/app/models/spree_cm_commissioner/integration_mapping.rb +41 -0
  67. data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
  68. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +37 -0
  69. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
  70. data/app/models/spree_cm_commissioner/line_item_decorator.rb +13 -1
  71. data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
  72. data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
  73. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  74. data/app/models/spree_cm_commissioner/product_decorator.rb +4 -3
  75. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  76. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +6 -3
  77. data/app/models/spree_cm_commissioner/stock_item_decorator.rb +4 -4
  78. data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -1
  79. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
  80. data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
  81. data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -2
  82. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
  83. data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
  84. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +1 -1
  85. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
  86. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
  87. data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
  88. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
  89. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
  90. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +83 -0
  91. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
  92. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
  93. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
  94. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
  95. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
  96. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
  97. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
  98. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
  99. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
  100. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
  101. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
  102. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
  103. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
  104. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
  105. data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
  106. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
  107. data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
  108. data/app/views/spree/admin/integrations/_form.html.erb +104 -0
  109. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
  110. data/app/views/spree/admin/integrations/edit.html.erb +45 -0
  111. data/app/views/spree/admin/integrations/index.html.erb +75 -0
  112. data/app/views/spree/admin/integrations/new.html.erb +25 -0
  113. data/bin/run_spec_group +101 -0
  114. data/config/locales/en.yml +8 -0
  115. data/config/locales/km.yml +8 -0
  116. data/config/routes.rb +8 -0
  117. data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
  118. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
  119. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
  120. data/lib/cm_app_logger.rb +36 -4
  121. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
  122. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
  123. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
  124. data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -0
  125. data/lib/spree_cm_commissioner/test_helper/factories/vendor_place_factory.rb +3 -2
  126. data/lib/spree_cm_commissioner/version.rb +1 -1
  127. data/lib/spree_cm_commissioner.rb +8 -7
  128. metadata +58 -3
@@ -0,0 +1,58 @@
1
+ module SpreeCmCommissioner
2
+ module ExternalIntegrationsHelper
3
+ def integration_status_badge(status)
4
+ badge_class = case status.to_s
5
+ when 'active' then 'badge-success'
6
+ when 'paused' then 'badge-warning'
7
+ else 'badge-secondary'
8
+ end
9
+
10
+ content_tag(:span, status.to_s.humanize, class: "badge badge-pill #{badge_class}")
11
+ end
12
+
13
+ def integration_conflict_strategy_badge(strategy)
14
+ content_tag(:span, strategy.to_s.humanize, class: 'badge badge-pill badge-secondary')
15
+ end
16
+
17
+ def integration_interval_badge(value, unit)
18
+ content_tag(:span, "#{value}#{unit}", class: 'badge badge-pill badge-info')
19
+ end
20
+
21
+ def integration_base_url_link(url)
22
+ if url.present?
23
+ link_to url, url, target: '_blank', rel: 'noopener', class: 'text-decoration-underline'
24
+ else
25
+ content_tag(:span, '—', class: 'text-muted')
26
+ end
27
+ end
28
+
29
+ def integration_mapping_internal_link(mapping)
30
+ return mapping.internal_id if mapping.internal.blank?
31
+
32
+ begin
33
+ case mapping.internal_type
34
+ when 'Spree::Taxon'
35
+ taxon = mapping.internal
36
+ taxonomy_id = taxon.taxonomy_id || taxon.taxonomy&.id
37
+ if taxonomy_id
38
+ link_to mapping.internal_id,
39
+ edit_admin_taxonomy_taxon_path(taxonomy_id, taxon.id),
40
+ class: 'text-primary'
41
+ end
42
+ when 'Spree::Product'
43
+ link_to mapping.internal_id, edit_admin_product_path(mapping.internal), class: 'text-primary'
44
+ when 'Spree::Variant'
45
+ link_to mapping.internal_id, edit_admin_product_variant_path(mapping.internal.product_id, mapping.internal), class: 'text-primary'
46
+ when 'Spree::Vendor'
47
+ link_to mapping.internal_id, edit_admin_vendor_path(mapping.internal), class: 'text-primary'
48
+ else
49
+ # Try polymorphic route as fallback
50
+ link_to mapping.internal_id, [:edit, :admin, mapping.internal], class: 'text-primary'
51
+ end
52
+ rescue StandardError => e
53
+ Rails.logger.warn("Failed to generate link for mapping #{mapping.id}: #{e.message}")
54
+ mapping.internal_id
55
+ end
56
+ end
57
+ end
58
+ end
@@ -94,7 +94,7 @@ module SpreeCmCommissioner
94
94
  end
95
95
 
96
96
  def generate_reports
97
- SpreeCmCommissioner::ReportsAssignerJob.perform_later(@parent_taxon.id, @parent_taxon.class.to_s)
97
+ SpreeCmCommissioner::ReportsAssignerJob.perform_later(queryable_id: @parent_taxon.id, queryable_type: @parent_taxon.class.to_s)
98
98
  end
99
99
  end
100
100
  end
@@ -11,7 +11,7 @@ module SpreeCmCommissioner
11
11
  end
12
12
 
13
13
  def enqueue_customer_notification_alert(customer_notification)
14
- SpreeCmCommissioner::CustomerNotificationSenderJob.perform_later(customer_notification.id)
14
+ SpreeCmCommissioner::CustomerNotificationSenderJob.perform_later(customer_notification_id: customer_notification.id)
15
15
  end
16
16
  end
17
17
  end
@@ -35,7 +35,7 @@ module SpreeCmCommissioner
35
35
  end
36
36
 
37
37
  def clear_inventory_cache
38
- SpreeCmCommissioner.redis_pool.with do |redis|
38
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
39
39
  redis.del(inventory_item.redis_key)
40
40
  end
41
41
  end
@@ -23,8 +23,9 @@ module SpreeCmCommissioner
23
23
  private
24
24
 
25
25
  def adjust_inventory_items_async(variant_id, quantity)
26
- CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: { variant_id: variant_id, quantity: quantity }) do
27
- SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant_id, quantity: quantity)
26
+ args = { variant_id: variant_id, quantity: quantity }
27
+ CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: args) do
28
+ SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(**args)
28
29
  end
29
30
  end
30
31
  end
@@ -2,8 +2,8 @@ module SpreeCmCommissioner
2
2
  class ConversionPreCalculatorJob < ApplicationUniqueJob
3
3
  queue_as :default
4
4
 
5
- def perform(product_id)
6
- product = Spree::Product.find(product_id)
5
+ def perform(options = {})
6
+ product = Spree::Product.find(options[:product_id])
7
7
  product.classification_ids.each do |classification_id|
8
8
  SpreeCmCommissioner::ConversionPreCalculator.call(product_taxon: Spree::Classification.find(classification_id))
9
9
  end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  class CustomerNotificationSenderJob < ApplicationUniqueJob
3
- def perform(customer_notification_id, user_ids = nil)
4
- customer_notification = SpreeCmCommissioner::CustomerNotification.find(customer_notification_id)
5
- SpreeCmCommissioner::CustomerNotificationSender.call(customer_notification: customer_notification, user_ids: user_ids)
3
+ def perform(options = {})
4
+ customer_notification = SpreeCmCommissioner::CustomerNotification.find(options[:customer_notification_id])
5
+ SpreeCmCommissioner::CustomerNotificationSender.call(customer_notification: customer_notification, user_ids: options[:user_ids])
6
6
  end
7
7
  end
8
8
  end
@@ -1,14 +1,14 @@
1
1
  module SpreeCmCommissioner
2
2
  module EnqueueCart
3
3
  class AddItemJob < ApplicationUniqueJob
4
- def perform(order_id, variant_id, quantity, public_metadata, private_metadata, options) # rubocop:disable Metrics/ParameterLists
4
+ def perform(options = {})
5
5
  SpreeCmCommissioner::EnqueueCart::AddItem.call(
6
- order_id: order_id,
7
- variant_id: variant_id,
8
- quantity: quantity,
9
- public_metadata: public_metadata,
10
- private_metadata: private_metadata,
11
- options: options,
6
+ order_id: options[:order_id],
7
+ variant_id: options[:variant_id],
8
+ quantity: options[:quantity],
9
+ public_metadata: options[:public_metadata],
10
+ private_metadata: options[:private_metadata],
11
+ options: options[:options],
12
12
  job_id: job_id
13
13
  )
14
14
  end
@@ -5,7 +5,7 @@ module SpreeCmCommissioner
5
5
  Spree::Taxon.event.includes(:children_products).find_each do |event|
6
6
  event.children_products.where('event_id IS NULL OR event_id != ?', event.id).find_each do |product|
7
7
  product.update_columns(event_id: event.id) # rubocop:disable Rails/SkipsModelValidations
8
- ::SpreeCmCommissioner::ProductEventIdToChildrenSyncerJob.perform_later(product.id)
8
+ ::SpreeCmCommissioner::ProductEventIdToChildrenSyncerJob.perform_later(product_id: product.id)
9
9
  end
10
10
  end
11
11
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class EventLineItemsDateSyncerJob < ApplicationJob
3
- def perform(event_id)
4
- event = Spree::Taxon.event.find(event_id)
3
+ def perform(options = {})
4
+ event = Spree::Taxon.event.find(options[:event_id])
5
5
  SpreeCmCommissioner::EventLineItemsDateSyncer.call(event: event)
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class ExportCsvJob < ApplicationUniqueJob
3
- def perform(id)
4
- Exports::ExportGuestCsvService.new(id).call
3
+ def perform(options = {})
4
+ Exports::ExportGuestCsvService.new(options[:export_id]).call
5
5
  end
6
6
  end
7
7
  end
@@ -1,14 +1,14 @@
1
1
  module SpreeCmCommissioner
2
2
  class ImportOrderJob < ApplicationUniqueJob
3
- def perform(import_order_id, import_by_user_id, import_type)
4
- if import_type == 'new_order'
3
+ def perform(options = {})
4
+ if options[:import_type] == 'new_order'
5
5
  SpreeCmCommissioner::Imports::CreateOrderService.new(
6
- import_order_id: import_order_id,
7
- import_by_user_id: import_by_user_id
6
+ import_order_id: options[:import_order_id],
7
+ import_by_user_id: options[:import_by_user_id]
8
8
  ).call
9
9
  else
10
10
  SpreeCmCommissioner::Imports::UpdateOrderService.new(
11
- import_order_id: import_order_id
11
+ import_order_id: options[:import_order_id]
12
12
  ).call
13
13
  end
14
14
  end
@@ -0,0 +1,39 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ # Base class for integration jobs
4
+ #
5
+ # Provides common error handling and alerting functionality for all integration jobs.
6
+ # Subclasses should implement their own perform logic and call the appropriate
7
+ # error handler methods.
8
+ class BaseJob < ApplicationJob
9
+ queue_as :integration
10
+
11
+ protected
12
+
13
+ # Handle error by logging and sending alert
14
+ # @param label [String] The error label for logging
15
+ # @param error [StandardError] The error that occurred
16
+ # @param data [Hash] Additional context data (e.g., sync_type, match_id, event_type)
17
+ def handle_error(label:, error:, data: {})
18
+ error_message = "#{error.class.name}: #{error.message}"
19
+
20
+ # Log the error with structured data
21
+ CmAppLogger.error(
22
+ label: label,
23
+ data: data.merge(
24
+ message: error_message,
25
+ backtrace: error.backtrace
26
+ )
27
+ )
28
+
29
+ # Send alert via Telegram
30
+ TelegramAlerts::IntegrationSyncFailureJob.perform_later(
31
+ error_message: error_message,
32
+ data: data
33
+ )
34
+
35
+ error_message
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ # Job that performs the actual polling (syncing) for a specific integration
4
+ #
5
+ # This job is enqueued by PollingSchedulerJob and handles:
6
+ # - Full syncs: Complete refresh of all data
7
+ # - Incremental syncs: Fetch only recent changes
8
+ # - Webhook syncs: Real-time event processing (triggered externally)
9
+ #
10
+ # Each integration type (StadiumXV1, LarrytaV1, etc.) must implement
11
+ # a sync_manager that responds to sync_full!, sync_incremental!, and sync_webhook!
12
+ class PollingJob < BaseJob
13
+ # Perform the integration pull
14
+ # @param integration_id [Integer] The integration ID
15
+ # @param sync_type [String] The sync type: 'full', 'incremental', or 'webhook_triggered'
16
+ # @param event_type [String] Optional event type for webhook syncs
17
+ # @param event_data [Hash] Optional event data for webhook syncs
18
+ def perform(options)
19
+ integration_id = options[:integration_id]
20
+ sync_type = options[:sync_type]
21
+ event_type = options[:event_type]
22
+ event_data = options[:event_data]
23
+ integration = SpreeCmCommissioner::Integration.find(integration_id)
24
+
25
+ # integration is no longer active, skip processing.
26
+ return unless integration.active?
27
+
28
+ SpreeCmCommissioner::Integrations::Polling.new.call(
29
+ integration: integration,
30
+ sync_type: sync_type,
31
+ event_type: event_type,
32
+ event_data: event_data
33
+ )
34
+ rescue StandardError => e
35
+ handle_sync_error(e, options)
36
+ raise
37
+ end
38
+
39
+ private
40
+
41
+ # Handle sync errors and alert
42
+ # @param error [StandardError] The error that occurred
43
+ # @param options [Hash] The options passed to perform
44
+ def handle_sync_error(error, options)
45
+ handle_error(
46
+ label: 'Integrations::IntegrationPullJob#perform Sync failed',
47
+ error: error,
48
+ data: options
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,30 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ # Scheduler job that runs periodically to enqueue integration polling jobs
4
+ #
5
+ # This job is responsible for:
6
+ # - Running full syncs every 24 hours
7
+ # - Running incremental syncs every 10 seconds
8
+ # - Webhook syncs are triggered externally (not scheduled here)
9
+ #
10
+ # The job queries all active integrations and enqueues appropriate pull jobs
11
+ # based on their last sync time and sync type configuration.
12
+ class PollingSchedulerJob < BaseJob
13
+ def perform
14
+ SpreeCmCommissioner::Integrations::PollingScheduler.new.call
15
+ rescue StandardError => e
16
+ handle_scheduler_error(e)
17
+ raise
18
+ end
19
+
20
+ private
21
+
22
+ def handle_scheduler_error(error)
23
+ handle_error(
24
+ label: 'Integrations::PollingSchedulerJob#perform Error scheduling integration pulls',
25
+ error: error
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class InvalidateCacheRequestJob < ApplicationUniqueJob
3
- def perform(pattern)
4
- SpreeCmCommissioner::InvalidateCacheRequest.call(pattern: pattern)
3
+ def perform(options = {})
4
+ SpreeCmCommissioner::InvalidateCacheRequest.call(pattern: options[:pattern])
5
5
  end
6
6
  end
7
7
  end
@@ -1,7 +1,13 @@
1
1
  module SpreeCmCommissioner
2
2
  class InventoryItemSyncerJob < ApplicationUniqueJob
3
- def perform(inventory_id_and_quantities:)
4
- InventoryItemSyncer.call(inventory_id_and_quantities:)
3
+ # :line_item_ids, :inventory_id_and_quantities
4
+ #
5
+ # :line_item_ids is included for unique job key generation to prevent duplicate jobs,
6
+ # though it's not used in the perform method implementation.
7
+ def perform(options = {})
8
+ InventoryItemSyncer.call(
9
+ inventory_id_and_quantities: options[:inventory_id_and_quantities]
10
+ )
5
11
  end
6
12
  end
7
13
  end
@@ -1,8 +1,12 @@
1
1
  module SpreeCmCommissioner
2
2
  class OptionTypeVariantsPublicMetadataUpdaterJob < ApplicationUniqueJob
3
- def perform(option_type_id)
4
- optino_type = ::Spree::OptionType.find(option_type_id)
5
- optino_type.variants.find_each(&:set_options_to_public_metadata!)
3
+ def perform(options = {})
4
+ option_type = ::Spree::OptionType.find_by(id: options[:option_type_id])
5
+
6
+ # job can be queued with invalid id or record is created but rolled back or deleted.
7
+ return if option_type.blank?
8
+
9
+ option_type.variants.find_each(&:set_options_to_public_metadata!)
6
10
  end
7
11
  end
8
12
  end
@@ -1,7 +1,11 @@
1
1
  module SpreeCmCommissioner
2
2
  class OptionValueVariantsPublicMetadataUpdaterJob < ApplicationUniqueJob
3
- def perform(option_value_id)
4
- option_value = ::Spree::OptionValue.find(option_value_id)
3
+ def perform(options = {})
4
+ option_value = ::Spree::OptionValue.find_by(id: options[:option_value_id])
5
+
6
+ # job can be queued with invalid id or record is created but rolled back or deleted.
7
+ return if option_value.blank?
8
+
5
9
  option_value.variants.find_each(&:set_options_to_public_metadata!)
6
10
  end
7
11
  end
@@ -4,8 +4,8 @@ module SpreeCmCommissioner
4
4
  class OrderCompleteTelegramSenderJob < ApplicationUniqueJob
5
5
  queue_as :telegram_bot
6
6
 
7
- def perform(order_id)
8
- order = Spree::Order.find(order_id)
7
+ def perform(options = {})
8
+ order = Spree::Order.find(options[:order_id])
9
9
  SpreeCmCommissioner::OrderCompleteTelegramSender.call(order: order)
10
10
  end
11
11
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class ProductEventIdToChildrenSyncerJob < ApplicationUniqueJob
3
- def perform(product_id)
4
- product = Spree::Product.find(product_id)
3
+ def perform(options = {})
4
+ product = Spree::Product.find(options[:product_id])
5
5
  SpreeCmCommissioner::ProductEventIdToChildrenSyncer.call(product: product)
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class ReportsAssignerJob < ApplicationUniqueJob
3
- def perform(queryable_id, queryable_type)
4
- SpreeCmCommissioner::ReportsAssigner.call(queryable_id: queryable_id, queryable_type: queryable_type)
3
+ def perform(options = {})
4
+ SpreeCmCommissioner::ReportsAssigner.call(queryable_id: options[:queryable_id], queryable_type: options[:queryable_type])
5
5
  end
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class SmsPinCodeJob < SpreeCmCommissioner::SmsJob
3
3
  # options = { from: xxxx, to: xxxx, body: xxxx }
4
- def perform(options)
4
+ def perform(options = {})
5
5
  SpreeCmCommissioner::Sms.call(options)
6
6
  end
7
7
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class StateJob < ApplicationUniqueJob
3
- def perform(state_id)
4
- state = ::Spree::State.find_by(id: state_id)
3
+ def perform(options = {})
4
+ state = ::Spree::State.find_by(id: options[:state_id])
5
5
  StateUpdater.call(state: state)
6
6
  end
7
7
  end
@@ -1,10 +1,13 @@
1
1
  module SpreeCmCommissioner
2
2
  module Stock
3
3
  class InventoryItemsAdjusterJob < ApplicationUniqueJob
4
- def perform(variant_id:, quantity:)
5
- variant = Spree::Variant.find(variant_id)
4
+ def perform(options = {})
5
+ variant = Spree::Variant.find_by(id: options[:variant_id])
6
6
 
7
- SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: quantity)
7
+ # potentially the variant was deleted during the job wait time, we just skip in that case.
8
+ return if variant.blank?
9
+
10
+ SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: options[:quantity])
8
11
  end
9
12
  end
10
13
  end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Stock
3
3
  class InventoryItemsGeneratorJob < ApplicationUniqueJob
4
- def perform(variant_id:)
5
- variant = Spree::Variant.find(variant_id)
4
+ def perform(options = {})
5
+ variant = Spree::Variant.find(options[:variant_id])
6
6
 
7
7
  SpreeCmCommissioner::Stock::InventoryItemsGenerator.call(variant: variant)
8
8
  end
@@ -0,0 +1,17 @@
1
+ module SpreeCmCommissioner
2
+ module TelegramAlerts
3
+ class IntegrationSyncFailureJob < ApplicationJob
4
+ queue_as :telegram_bot
5
+
6
+ # Handle error by logging and sending alert
7
+ # @param error_message [String] The error message
8
+ # @param data [Hash] Additional context data (e.g., sync_type, match_id, event_type)
9
+ def perform(options)
10
+ SpreeCmCommissioner::TelegramAlerts::IntegrationSyncFailure.call(
11
+ error_message: options[:error_message],
12
+ data: options[:data] || {}
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Transit
3
3
  class RouteFulfilledOrderCountIncrementerJob < ApplicationUniqueJob
4
- def perform(order_id:)
5
- order = Spree::Order.find(order_id)
4
+ def perform(options = {})
5
+ order = Spree::Order.find(options[:order_id])
6
6
  SpreeCmCommissioner::Routes::IncrementFulfilledOrderCount.call(order: order)
7
7
  end
8
8
  end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Transit
3
3
  class RouteOrderCountIncrementerJob < ApplicationUniqueJob
4
- def perform(order_id:)
5
- order = Spree::Order.find(order_id)
4
+ def perform(options = {})
5
+ order = Spree::Order.find(options[:order_id])
6
6
  SpreeCmCommissioner::Routes::IncrementOrderCount.call(order: order)
7
7
  end
8
8
  end
@@ -5,8 +5,8 @@ module SpreeCmCommissioner
5
5
  class RoutePreviousTripCountDecrementerJob < ApplicationUniqueJob
6
6
  queue_as :default
7
7
 
8
- def perform(previous_route_id:)
9
- SpreeCmCommissioner::Routes::DecrementPreviousTripCount.call(previous_route_id: previous_route_id)
8
+ def perform(options = {})
9
+ SpreeCmCommissioner::Routes::DecrementPreviousTripCount.call(previous_route_id: options[:previous_route_id])
10
10
  end
11
11
  end
12
12
  end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Transit
3
3
  class RouteTripCountDecrementerJob < ApplicationUniqueJob
4
- def perform(trip_id:)
5
- trip = SpreeCmCommissioner::Trip.find(trip_id)
4
+ def perform(options = {})
5
+ trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
6
  SpreeCmCommissioner::Routes::DecrementTripCount.call(trip: trip)
7
7
  end
8
8
  end
@@ -1,8 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module Transit
3
3
  class RouteTripCountIncrementerJob < ApplicationUniqueJob
4
- def perform(trip_id:)
5
- trip = SpreeCmCommissioner::Trip.find(trip_id)
4
+ def perform(options = {})
5
+ trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
6
  SpreeCmCommissioner::Routes::IncrementTripCount.call(trip: trip)
7
7
  end
8
8
  end
@@ -2,8 +2,8 @@ module SpreeCmCommissioner
2
2
  class VendorCreationTelegramAlertSenderJob < ApplicationUniqueJob
3
3
  queue_as :telegram_bot
4
4
 
5
- def perform(vendor_id)
6
- vendor = Spree::Vendor.find(vendor_id)
5
+ def perform(options = {})
6
+ vendor = Spree::Vendor.find(options[:vendor_id])
7
7
  SpreeCmCommissioner::VendorCreationTelegramAlertSender.call(vendor: vendor)
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  class VendorJob < ApplicationUniqueJob
3
- def perform(vendor_id)
4
- vendor = ::Spree::Vendor.find(vendor_id)
3
+ def perform(options = {})
4
+ vendor = ::Spree::Vendor.find(options[:vendor_id])
5
5
  VendorUpdater.call(vendor: vendor)
6
6
  end
7
7
  end
@@ -23,7 +23,7 @@ module SpreeCmCommissioner
23
23
  class WaitingRoomSessionFirebaseLoggerJob < ApplicationUniqueJob
24
24
  retry_on StandardError, wait: :exponentially_longer, attempts: 4
25
25
 
26
- def perform(options)
26
+ def perform(options = {})
27
27
  room_session = SpreeCmCommissioner::WaitingRoomSession.find(options[:room_session_id])
28
28
  waiting_guest_firebase_doc_id = options[:waiting_guest_firebase_doc_id]
29
29
 
@@ -0,0 +1,61 @@
1
+ module SpreeCmCommissioner
2
+ module Integrations
3
+ # IntegrationMappable provides a polymorphic mapping layer between internal records and external integration IDs.
4
+ #
5
+ # Design Rationale: Why use a separate mapping table instead of adding external_id columns directly?
6
+ #
7
+ # 1. **No Schema Changes Required**: Adding a new integration doesn't require migrations to add columns to every table.
8
+ # The mapping table is reusable across all models that include this concern.
9
+ #
10
+ # 2. **Polymorphic Scalability**: A single mapping table handles multiple record types (Spree::Product, Spree::Taxon,
11
+ # SeatLayout, etc.) without duplicating external_id columns across the schema.
12
+ #
13
+ # 3. **Multiple Source Support**: Records can have mappings to multiple integrations simultaneously. A single product
14
+ # might be synced from StadiumXV1, LarrytaV1, and other sources. Direct columns would require one per integration.
15
+ #
16
+ # 4. **Acceptable Performance Trade-off**: While joins are slightly slower than direct column lookups, the performance
17
+ # impact is negligible in practice because:
18
+ # - Lookups occur primarily during background jobs (async, not blocking user requests)
19
+ # - Integration sync operations (incremental, full, webhook)
20
+ # - Occasional user-triggered operations (not on every page load)
21
+ # The flexibility and maintainability gains far outweigh the minimal performance cost.
22
+ module IntegrationMappable
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ has_many :integration_mappings, as: :internal, class_name: 'SpreeCmCommissioner::IntegrationMapping', dependent: :destroy
27
+ has_many :external_wins_integration_mappings, lambda {
28
+ joins(:integration).where(integration: { conflict_strategy: :external_wins })
29
+ }, as: :internal, class_name: 'SpreeCmCommissioner::IntegrationMapping'
30
+ end
31
+
32
+ class_methods do
33
+ def find_or_initialize_integration_mapping(integration_id:, external_id:)
34
+ IntegrationMapping.includes(:internal).find_or_initialize_by(
35
+ integration_id: integration_id,
36
+ internal_type: name,
37
+ external_id: external_id
38
+ ) do |mapping|
39
+ mapping.internal = new
40
+ mapping.external_id = external_id
41
+ end
42
+ end
43
+
44
+ def find_integration_mapping(integration_id:, external_id:)
45
+ IntegrationMapping.includes(:internal).find_by(
46
+ integration_id: integration_id,
47
+ internal_type: name,
48
+ external_id: external_id
49
+ )
50
+ end
51
+
52
+ def find_oldest_active_mapping(integration_id:)
53
+ IntegrationMapping.where(
54
+ integration_id: integration_id,
55
+ internal_type: name
56
+ ).active.order(:last_synced_at).first
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end