spree_cm_commissioner 2.5.0.pre.pre2 → 2.5.0.pre.pre4

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 (101) 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/admin/integration_mappings_controller.rb +21 -0
  6. data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
  7. data/app/controllers/spree/admin/integrations_controller.rb +83 -0
  8. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
  9. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +1 -7
  10. data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +9 -9
  11. data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +10 -0
  12. data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +4 -0
  13. data/app/finders/spree_cm_commissioner/events/find_matches.rb +15 -0
  14. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +10 -10
  15. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +14 -10
  16. data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
  17. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  18. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +3 -2
  19. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
  20. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
  21. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
  22. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +6 -3
  23. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
  24. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +61 -0
  25. data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +36 -0
  26. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +15 -3
  27. data/app/models/concerns/spree_cm_commissioner/order_integration.rb +33 -0
  28. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +2 -0
  29. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
  30. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +0 -3
  31. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
  32. data/app/models/spree_cm_commissioner/guest.rb +13 -0
  33. data/app/models/spree_cm_commissioner/integration.rb +29 -0
  34. data/app/models/spree_cm_commissioner/integration_mapping.rb +41 -0
  35. data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
  36. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +37 -0
  37. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
  38. data/app/models/spree_cm_commissioner/line_item_decorator.rb +13 -1
  39. data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
  40. data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
  41. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  42. data/app/models/spree_cm_commissioner/product_decorator.rb +3 -2
  43. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  44. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  45. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  46. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
  47. data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
  48. data/app/models/spree_cm_commissioner/vendor_decorator.rb +4 -0
  49. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
  50. data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
  51. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +0 -5
  52. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
  53. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
  54. data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
  55. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
  56. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
  57. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +83 -0
  58. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
  59. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
  60. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
  61. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
  62. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
  63. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
  64. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
  65. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
  66. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
  67. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
  68. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
  69. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
  70. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
  71. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
  72. data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
  73. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
  74. data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
  75. data/app/views/spree/admin/integrations/_form.html.erb +104 -0
  76. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
  77. data/app/views/spree/admin/integrations/edit.html.erb +45 -0
  78. data/app/views/spree/admin/integrations/index.html.erb +75 -0
  79. data/app/views/spree/admin/integrations/new.html.erb +25 -0
  80. data/app/views/spree/admin/tenants/_form.html.erb +0 -18
  81. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  82. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
  83. data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +6 -13
  84. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +24 -23
  85. data/config/locales/en.yml +8 -0
  86. data/config/locales/km.yml +8 -0
  87. data/config/routes.rb +8 -0
  88. data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
  89. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
  90. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
  91. data/lib/cm_app_logger.rb +1 -0
  92. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
  93. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
  94. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
  95. data/lib/spree_cm_commissioner/version.rb +1 -1
  96. data/lib/spree_cm_commissioner.rb +8 -7
  97. metadata +56 -6
  98. data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +0 -12
  99. data/app/views/spree/admin/tenants/form/_footer.html.erb +0 -31
  100. data/app/views/spree/admin/tenants/form/_social.html.erb +0 -31
  101. data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f64a6d2661d4c1deb862d020b44eca3748d001e0376731881b47de3c1e4965c
4
- data.tar.gz: 4a11c037dd432001d56d379c73e1cad574971f2a1cd8cc5ef3cbb1585b297c3b
3
+ metadata.gz: ba91dacd894816a0b4105c032a0550371aeab884b3f344ebe554758c3556a0a0
4
+ data.tar.gz: 077d13272adb0ced0f246dc9d48d5d3b9ae56a95beaa74a6d2cd6fc7f134f7e1
5
5
  SHA512:
6
- metadata.gz: 1e3de7556ebafefc89a00bbf454f2cb70700ef9dc56b8131b103f9f26f7e3af8341fcf0bc4c0b757a372128fbf56d6ecf967682ba46f97f0b117f0b0319ee979
7
- data.tar.gz: ea662f3962d01be3d39e11ba4d86611fcc3713c03061ab8e0131c2063acfdb70ade45b77f821a616ad3320637d2ba547389a91a5f14bbe05530d26ac40c692fd
6
+ metadata.gz: 8a990d42d6cf09c9304971a41efb74525761de29ee6cefe04e1a7b0dbe2831e9ad40f52f229ca463ee905ed1e039b1562daa2eace5307c4dd3eb6f725488e671
7
+ data.tar.gz: 309280aeb9987456b2788be11cdad59095da58b2b258e294477cfb5edb63323b4ee6d3fd8ae03f845fd79db0d0186ff54d64f6cb406567c2a284dc26454446f8
@@ -10,7 +10,7 @@ on:
10
10
  - reopened
11
11
  branches:
12
12
  - develop
13
- - milestone-77-scalable-design
13
+ - 3114-external-integration-milestone
14
14
  push:
15
15
  tags:
16
16
  - "*"
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 3.3.5
1
+ ruby 3.2.0
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.0.pre.pre2)
37
+ spree_cm_commissioner (2.5.0.pre.pre4)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Admin
3
+ class IntegrationMappingsController < Spree::Admin::BaseController
4
+ helper SpreeCmCommissioner::ExternalIntegrationsHelper
5
+ before_action :load_integration
6
+
7
+ def index
8
+ @mappings = @integration.integration_mappings
9
+ .order(updated_at: :desc)
10
+ .page(params[:page])
11
+ .per(params[:per_page] || 25)
12
+ end
13
+
14
+ private
15
+
16
+ def load_integration
17
+ @integration ||= SpreeCmCommissioner::Integration.find(params[:integration_id])
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Admin
3
+ class IntegrationSessionsController < Spree::Admin::BaseController
4
+ helper SpreeCmCommissioner::ExternalIntegrationsHelper
5
+ before_action :load_integration
6
+
7
+ def index
8
+ @sync_sessions = @integration.integration_sync_sessions
9
+ .order(created_at: :desc)
10
+ .page(params[:page])
11
+ .per(params[:per_page] || 25)
12
+ end
13
+
14
+ private
15
+
16
+ def load_integration
17
+ @integration ||= SpreeCmCommissioner::Integration.find(params[:integration_id])
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,83 @@
1
+ module Spree
2
+ module Admin
3
+ class IntegrationsController < Spree::Admin::ResourceController
4
+ helper SpreeCmCommissioner::ExternalIntegrationsHelper
5
+ before_action :load_form_collections, only: %i[new create edit update]
6
+ before_action :load_integration, only: %i[enqueue_polling]
7
+
8
+ def index
9
+ @integrations = model_class.all
10
+ end
11
+
12
+ def enqueue_polling
13
+ sync_type = params[:sync_type].presence || :full
14
+
15
+ if @integration.active?
16
+ SpreeCmCommissioner::Integrations::PollingJob.perform_later(
17
+ integration_id: @integration.id,
18
+ sync_type: sync_type
19
+ )
20
+ flash[:success] = Spree.t('admin.integration_sync_enqueued')
21
+ else
22
+ flash[:error] = Spree.t('admin.integration_inactive')
23
+ end
24
+
25
+ redirect_to admin_integration_sessions_path(@integration)
26
+ end
27
+
28
+ # override
29
+ def model_class
30
+ SpreeCmCommissioner::Integration
31
+ end
32
+
33
+ # override
34
+ def collection_url(options = {})
35
+ admin_integrations_url(options)
36
+ end
37
+
38
+ private
39
+
40
+ def permitted_resource_params
41
+ params.require(:spree_cm_commissioner_integration).permit(
42
+ :name,
43
+ :type,
44
+ :status,
45
+ :conflict_strategy,
46
+ :incremental_sync_interval_seconds,
47
+ :full_sync_interval_hours,
48
+ :tenant_id,
49
+ :vendor_id,
50
+ public_metadata: {},
51
+ private_metadata: {}
52
+ )
53
+ end
54
+
55
+ def load_integration
56
+ @integration ||= SpreeCmCommissioner::Integration.find(params[:id])
57
+ end
58
+
59
+ def load_form_collections
60
+ @integration_type_options ||= integration_type_options
61
+ @status_options ||= enum_to_options(SpreeCmCommissioner::Integration.statuses)
62
+ @conflict_strategy_options ||= enum_to_options(SpreeCmCommissioner::Integration.conflict_strategies)
63
+ @tenants ||= SpreeCmCommissioner::Tenant.select(:id, :name).order(:name)
64
+ @vendors ||= Spree::Vendor.select(:id, :name).order(:name)
65
+ end
66
+
67
+ def enum_to_options(enum_hash)
68
+ enum_hash.keys.map { |key| [key.humanize, key] }
69
+ end
70
+
71
+ def integration_type_options
72
+ files = Dir[SpreeCmCommissioner::Engine.root.join('app/models/spree_cm_commissioner/integrations/*.rb')]
73
+
74
+ return [[Spree.t(:type), 'SpreeCmCommissioner::Integration']] if files.blank?
75
+
76
+ files.map do |path|
77
+ class_name = "SpreeCmCommissioner::Integrations::#{File.basename(path, '.rb').camelize}"
78
+ [class_name, class_name]
79
+ end.sort_by(&:first)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,15 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class EventMatchesController < TaxonsController
6
+ private
7
+
8
+ def collection_finder
9
+ SpreeCmCommissioner::Events::FindMatches
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -19,8 +19,7 @@ module Spree
19
19
  # override
20
20
  def collection
21
21
  @collection ||= collection_finder.new.execute(
22
- route_type: params[:route_type],
23
- query: params[:query]
22
+ route_type: params[:route_type]
24
23
  )
25
24
  end
26
25
 
@@ -52,11 +51,6 @@ module Spree
52
51
  )
53
52
  end
54
53
 
55
- # override
56
- def required_schema
57
- SpreeCmCommissioner::PopularRoutePlacesRequestSchema
58
- end
59
-
60
54
  def include_vendors?
61
55
  resource_includes.include?(:vendors) || false
62
56
  end
@@ -3,22 +3,22 @@
3
3
  # GET /api/v2/storefront/route_places
4
4
  #
5
5
  # Finds places (origins or destinations) that are connected to a given place through existing routes.
6
- # Optionally filters results by query or route_type. Supports two modes:
6
+ # Optionally filters results by keyword or route_type. Supports two modes:
7
7
  #
8
8
  # Usage 1: Connected places (requires place_id)
9
9
  # - Finds places connected to a specific place via routes
10
10
  # - Returns origins/destinations that have routes with the specified place
11
11
  #
12
- # Usage 2: query search (requires query, place_id optional)
13
- # - Searches all route places by query
14
- # - Returns all origins/destinations matching the query
12
+ # Usage 2: Keyword search (requires query, place_id optional)
13
+ # - Searches all route places by keyword
14
+ # - Returns all origins/destinations matching the keyword
15
15
  #
16
16
  # Query Parameters:
17
17
  # - place_type: [String] Required. Type of route place: 'origin' or 'destination'
18
18
  # * 'origin': returns origins that have routes TO the specified place (if place_id provided)
19
19
  # * 'destination': returns destinations that have routes FROM the specified place (if place_id provided)
20
20
  # - place_id: [Integer] Optional. The place ID to find connected places for
21
- # - query: [String] Optional. query to filter place names (case-insensitive)
21
+ # - query: [String] Optional. Keyword to filter place names (case-insensitive)
22
22
  # - include: Optional comma-separated list (e.g., 'vendors,nearby_places')
23
23
  #
24
24
  # Response: Collection of places serialized with PlaceSerializer
@@ -27,15 +27,15 @@
27
27
  # - Returns empty collection if place_type is invalid
28
28
  # - If place_id provided: returns places connected to that place
29
29
  # - If place_id blank: returns all origins/destinations
30
- # - Filters results by query or route_type if provided
30
+ # - Filters results by keyword or route_type if provided
31
31
  #
32
32
  # @example Mode 1: Find destinations connected to origin place ID 123
33
33
  # GET /api/v2/storefront/route_places?place_id=123&place_type=destination
34
34
  #
35
- # @example Mode 2: Search all destination places by query
35
+ # @example Mode 2: Search all destination places by keyword
36
36
  # GET /api/v2/storefront/route_places?place_type=destination&query=Phnom
37
37
  #
38
- # @example Combined: Find origins connected to place 456, filtered by query
38
+ # @example Combined: Find origins connected to place 456, filtered by keyword
39
39
  # GET /api/v2/storefront/route_places?place_id=456&place_type=origin&query=Siem
40
40
  module Spree
41
41
  module Api
@@ -49,7 +49,7 @@ module Spree
49
49
  @collection ||= collection_finder.new(
50
50
  place_type: params[:place_type],
51
51
  place_id: params[:place_id],
52
- query: params[:query],
52
+ keyword: params[:query],
53
53
  route_type: params[:route_type]
54
54
  ).execute
55
55
  end
@@ -0,0 +1,10 @@
1
+ module SpreeCmCommissioner::Integrations
2
+ class ExternalClientError < StandardError
3
+ attr_reader :status_code
4
+
5
+ def initialize(message, status_code = nil)
6
+ super(message)
7
+ @status_code = status_code
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module SpreeCmCommissioner::Integrations
2
+ class SyncError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ module Events
3
+ class FindMatches < ::Spree::BaseFinder
4
+ # currently, there is no way to determine if an event is a match yet.
5
+ # this finder returns all events that have active integration mappings (synced from external systems like StadiumX)
6
+ # as a temporary workaround to filter out non-match events.
7
+ def execute
8
+ scope.joins(:integration_mappings).where(
9
+ kind: :event,
10
+ integration_mappings: { status: :active }
11
+ ).distinct
12
+ end
13
+ end
14
+ end
15
+ end
@@ -2,14 +2,14 @@
2
2
  #
3
3
  # @param place_type [String] Required. 'origin' or 'destination'
4
4
  # @param place_id [Integer] Optional. Filter by connected place ID
5
- # @param query [String] Optional. Filter by place name (case-insensitive)
5
+ # @param keyword [String] Optional. Filter by place name (case-insensitive)
6
6
  # @param route_type [Symbol, String] Optional. Filter by route type from associated trips (e.g., :ferry, :bus)
7
7
  #
8
8
  # @return [ActiveRecord::Relation<SpreeCmCommissioner::Place>]
9
9
  #
10
10
  # Modes:
11
11
  # - place_id only: returns connected places (origins TO or destinations FROM given place)
12
- # - query only: filters by name
12
+ # - keyword only: filters by name
13
13
  # - route_type only: filters by trip route types on those routes
14
14
  # - combinations: connected places filtered by name and/or route type
15
15
  # - neither: all origins/destinations in routes
@@ -17,17 +17,17 @@
17
17
  # @example Origins with ferry trips to place 123
18
18
  # FindWithRoute.new(place_type: 'origin', place_id: 123, route_type: :ferry).execute
19
19
  #
20
- # @example Destinations filtered by query and route type
21
- # FindWithRoute.new(place_type: 'destination', query: 'Phnom', route_type: :bus).execute
20
+ # @example Destinations filtered by keyword and route type
21
+ # FindWithRoute.new(place_type: 'destination', keyword: 'Phnom', route_type: :bus).execute
22
22
  module SpreeCmCommissioner
23
23
  module Places
24
24
  class FindWithRoute
25
- attr_reader :place_type, :place_id, :query, :route_type
25
+ attr_reader :place_type, :place_id, :keyword, :route_type
26
26
 
27
- def initialize(place_type:, place_id: nil, query: nil, route_type: nil)
27
+ def initialize(place_type:, place_id: nil, keyword: nil, route_type: nil)
28
28
  @place_type = place_type
29
29
  @place_id = place_id
30
- @query = query
30
+ @keyword = keyword
31
31
  @route_type = route_type
32
32
  end
33
33
 
@@ -36,7 +36,7 @@ module SpreeCmCommissioner
36
36
  return SpreeCmCommissioner::Place.none if place_id.present? && !SpreeCmCommissioner::Place.exists?(place_id)
37
37
 
38
38
  result = scope
39
- result = apply_query_filter(result) if query.present?
39
+ result = apply_keyword_filter(result) if keyword.present?
40
40
  result = apply_route_type_filter(result) if route_type.present?
41
41
  result
42
42
  end
@@ -57,8 +57,8 @@ module SpreeCmCommissioner
57
57
  end
58
58
  end
59
59
 
60
- def apply_query_filter(result)
61
- result.where('cm_places.name ILIKE ?', "%#{query}%")
60
+ def apply_keyword_filter(result)
61
+ result.where('cm_places.name ILIKE ?', "%#{keyword}%")
62
62
  end
63
63
 
64
64
  def apply_route_type_filter(result)
@@ -24,22 +24,26 @@
24
24
  module SpreeCmCommissioner
25
25
  module Routes
26
26
  class FindPopular
27
- def execute(route_type: nil, query: nil)
28
- scope(route_type: route_type, query: query)
27
+ def execute(route_type: nil)
28
+ scope(route_type: route_type)
29
29
  end
30
30
 
31
31
  private
32
32
 
33
- def scope(route_type:, query:)
34
- includes_associations = %i[origin_place destination_place]
35
- includes_associations << :trips if route_type.present?
36
- result = SpreeCmCommissioner::Route.includes(*includes_associations)
33
+ def scope(route_type: nil)
34
+ result = SpreeCmCommissioner::Route
35
+ .includes(:origin_place, :destination_place)
37
36
 
38
- result = result.joins(:trips).where(cm_trips: { route_type: route_type.to_sym }) if route_type.present?
37
+ if route_type.present?
38
+ # Filter routes that have trips with the specified route_type
39
+ route_scope = SpreeCmCommissioner::Route
40
+ .joins(:trips)
41
+ .where(cm_trips: { route_type: route_type.to_sym })
42
+ result = result.where(id: route_scope.select(:id))
43
+ end
39
44
 
40
- result = result.where('cm_routes.route_name ILIKE ?', "%#{query}%") if query.present?
41
-
42
- result.distinct.order(fulfilled_order_count: :desc, order_count: :desc)
45
+ result.order(Arel.sql('COALESCE(fulfilled_order_count, 0) DESC'))
46
+ .order(Arel.sql('COALESCE(order_count, 0) DESC'))
43
47
  end
44
48
  end
45
49
  end
@@ -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
@@ -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
@@ -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,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
@@ -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