spree_cm_commissioner 2.4.2 → 2.5.0.pre.pre1

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 (84) 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/README.md +32 -0
  6. data/app/controllers/spree/admin/integration_mappings_controller.rb +21 -0
  7. data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
  8. data/app/controllers/spree/admin/integrations_controller.rb +83 -0
  9. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
  10. data/app/controllers/spree/api/v2/storefront/tenants_controller.rb +30 -0
  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/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
  15. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  16. data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +4 -2
  17. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
  18. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
  19. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
  20. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +5 -4
  21. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
  22. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +58 -0
  23. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +15 -3
  24. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
  25. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +2 -0
  26. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
  27. data/app/models/spree_cm_commissioner/integration.rb +21 -0
  28. data/app/models/spree_cm_commissioner/integration_mapping.rb +37 -0
  29. data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
  30. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +21 -0
  31. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
  32. data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
  33. data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
  34. data/app/models/spree_cm_commissioner/product_decorator.rb +3 -2
  35. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  36. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  37. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  38. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
  39. data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
  40. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  41. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
  42. data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
  43. data/app/serializers/spree/v2/storefront/tenant_serializer.rb +14 -0
  44. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
  45. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
  46. data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
  47. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
  48. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
  49. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
  50. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
  51. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
  52. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
  53. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
  54. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
  55. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
  56. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
  57. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
  58. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
  59. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
  60. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
  61. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
  62. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
  63. data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
  64. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
  65. data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
  66. data/app/views/spree/admin/integrations/_form.html.erb +104 -0
  67. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
  68. data/app/views/spree/admin/integrations/edit.html.erb +45 -0
  69. data/app/views/spree/admin/integrations/index.html.erb +75 -0
  70. data/app/views/spree/admin/integrations/new.html.erb +25 -0
  71. data/app/views/spree/admin/tenants/_form.html.erb +79 -36
  72. data/config/locales/en.yml +8 -0
  73. data/config/locales/km.yml +8 -0
  74. data/config/routes.rb +9 -1
  75. data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
  76. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
  77. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
  78. data/lib/cm_app_logger.rb +1 -0
  79. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
  80. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
  81. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
  82. data/lib/spree_cm_commissioner/version.rb +1 -1
  83. data/lib/spree_cm_commissioner.rb +8 -7
  84. metadata +58 -5
@@ -13,6 +13,7 @@
13
13
  # - integer
14
14
  # - array
15
15
  # - datetime
16
+ # - hash
16
17
  #
17
18
  # Example usage:
18
19
  # ```
@@ -22,6 +23,7 @@
22
23
  # store_public_metadata :completed, :boolean, default: true
23
24
  # store_public_metadata :count, :integer, default: 5
24
25
  # store_public_metadata :tags, :array, default: []
26
+ # store_public_metadata :tags, :hash
25
27
  # store_private_metadata :app_token, :string, default: "XYZ"
26
28
  # store_public_metadata :scheduled_at, :datetime, default: Time.now
27
29
  # end
@@ -92,7 +94,7 @@ module SpreeCmCommissioner
92
94
  # - Return default if nil
93
95
  # - Cast to correct type
94
96
  # - Add `?` predicate for booleans
95
- def define_metadata_reader(column_name, key, type, default)
97
+ def define_metadata_reader(column_name, key, type, default) # rubocop:disable Metrics/CyclomaticComplexity
96
98
  define_method(key) do
97
99
  metadata = send(column_name) || {}
98
100
  raw_value = metadata[key.to_s]
@@ -109,6 +111,8 @@ module SpreeCmCommissioner
109
111
  Array(raw_value)
110
112
  when :datetime
111
113
  Time.zone.parse(raw_value.to_s)
114
+ when :hash
115
+ raw_value.is_a?(Hash) ? raw_value : {}
112
116
  else
113
117
  raw_value
114
118
  end
@@ -118,7 +122,7 @@ module SpreeCmCommissioner
118
122
  end
119
123
 
120
124
  # Validates only new assignments (raw JSON values) for type safety
121
- def define_metadata_validation(column_name, key, type) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
125
+ def define_metadata_validation(column_name, key, type) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/AbcSize
122
126
  case type
123
127
  when :boolean
124
128
  validates key, inclusion: { in: [true, false] }, allow_nil: true
@@ -160,6 +164,14 @@ module SpreeCmCommissioner
160
164
  errors.add(key, 'is not a valid datetime')
161
165
  end
162
166
  end
167
+ when :hash
168
+ validate do
169
+ metadata = send(column_name) || {}
170
+ raw_value = metadata[key.to_s]
171
+ next if raw_value.nil?
172
+
173
+ errors.add(key, 'must be a hash') unless raw_value.is_a?(Hash)
174
+ end
163
175
  end
164
176
  end
165
177
  end
@@ -10,6 +10,8 @@ module SpreeCmCommissioner
10
10
  preference :payment_success_image, :string, default: ''
11
11
  preference :payment_loader, :string, default: ''
12
12
  preference :brand_primary_color, :string, default: ''
13
+ preference :redirect_target_host, :string, default: ''
14
+ preference :redirect_excluded_paths, :string, default: ''
13
15
  end
14
16
  end
15
17
  end
@@ -35,12 +35,6 @@ module SpreeCmCommissioner
35
35
  to: :options
36
36
  end
37
37
 
38
- # Override variant.rb to return cached formatted options text, avoiding repeated database queries.
39
- # Falls back to database queries & computing the format if preload data is unavailable.
40
- def options_text
41
- @options_text ||= public_metadata[:preload_options_text].presence || Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
42
- end
43
-
44
38
  def options
45
39
  @options ||= VariantOptions.new(self)
46
40
  end
@@ -111,7 +105,7 @@ module SpreeCmCommissioner
111
105
  # Precomputes the human-readable format (e.g., "Red, 256GB") to avoid
112
106
  # repeated formatting and association queries.
113
107
  # Example: {"color" => "red", "storage" => "256GB"} - option_type_name => option_value_name pairs
114
- self.public_metadata[:preload_options_text] = Spree::Variants::VisableOptionsPresenter.new(self).to_sentence
108
+ self.public_metadata[:preload_options_text] = Spree::Variants::VisibleOptionsPresenter.new(self).to_sentence
115
109
  end
116
110
 
117
111
  def set_options_to_public_metadata!
@@ -0,0 +1,21 @@
1
+ module SpreeCmCommissioner
2
+ class Integration < Base
3
+ include StoreMetadata
4
+
5
+ enum status: { inactive: 0, active: 1, paused: 2 }
6
+ enum conflict_strategy: { newest_wins: 0, internal_wins: 1, external_wins: 2, manual_resolution: 3 }
7
+
8
+ belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
9
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
10
+
11
+ has_many :integration_mappings, class_name: 'SpreeCmCommissioner::IntegrationMapping', dependent: :destroy, inverse_of: :integration
12
+ has_many :integration_sync_sessions, class_name: 'SpreeCmCommissioner::IntegrationSyncSession', dependent: :destroy, inverse_of: :integration
13
+
14
+ validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 3600 }
15
+ validates :full_sync_interval_hours, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 168 }
16
+
17
+ def sync_manager
18
+ raise NotImplementedError, 'Subclasses must implement the sync_manager method'
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,37 @@
1
+ module SpreeCmCommissioner
2
+ class IntegrationMapping < Base
3
+ include StoreMetadata
4
+
5
+ enum status: { active: 0, archived: 1 }
6
+
7
+ # polymorphic (Spree::Taxon, Spree::Product, Spree::Variant, Spree::Vendor)
8
+ belongs_to :internal, polymorphic: true, optional: false
9
+ belongs_to :integration, class_name: 'SpreeCmCommissioner::Integration', inverse_of: :integration_mappings, optional: false
10
+
11
+ validates :external_id, presence: true, uniqueness: { scope: %i[integration_id internal_type internal_id date] }
12
+ validate :validate_internal_exists, if: -> { internal_type.present? && internal_id.present? }
13
+
14
+ def mark_as_archived!
15
+ self.status = :archived
16
+ self.last_synced_at = Time.zone.now
17
+
18
+ save!
19
+ end
20
+
21
+ def mark_as_active!(external_payload:)
22
+ self.external_payload = external_payload
23
+ self.last_synced_at = Time.zone.now
24
+ self.status = :active
25
+
26
+ save!
27
+ end
28
+
29
+ private
30
+
31
+ def validate_internal_exists
32
+ return if internal_type.safe_constantize&.exists?(id: internal_id)
33
+
34
+ errors.add(:internal, "record (#{internal_type}##{internal_id}) does not exist")
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,15 @@
1
+ module SpreeCmCommissioner
2
+ class IntegrationSyncSession < Base
3
+ include StoreMetadata
4
+
5
+ enum status: { pending: 0, in_progress: 1, completed: 2, failed: 3, canceled: 4 }
6
+ enum :sync_type, {
7
+ full: 0, # on initial setup or periodic complete refresh
8
+ incremental: 1, # on scheduled intervals to fetch recent changes
9
+ webhook_triggered: 2 # immediately when an external webhook event is received
10
+ }, prefix: true
11
+
12
+ belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
13
+ belongs_to :integration, class_name: 'SpreeCmCommissioner::Integration', inverse_of: :integration_sync_sessions, optional: false
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integration
2
+ store_private_metadata :public_key, :string
3
+ store_private_metadata :private_key, :string
4
+ store_private_metadata :base_url, :string # e.g. 'https://api.stadiumx.com'
5
+
6
+ # override
7
+ def sync_manager
8
+ SpreeCmCommissioner::Integrations::StadiumXV1::SyncManager.new(
9
+ integration: self,
10
+ client: client
11
+ )
12
+ end
13
+
14
+ def client
15
+ SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(
16
+ public_key: public_key,
17
+ private_key: private_key,
18
+ base_url: base_url
19
+ )
20
+ end
21
+ end
@@ -56,8 +56,12 @@ module SpreeCmCommissioner
56
56
  "inventory:#{id}"
57
57
  end
58
58
 
59
+ def quantity_in_redis
60
+ SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.get(redis_key).to_i }
61
+ end
62
+
59
63
  def adjust_quantity_in_redis(quantity)
60
- SpreeCmCommissioner.redis_pool.with do |redis|
64
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
61
65
  # Always update Redis cache, even if it doesn't exist yet.
62
66
  # This prevents admin adjustments from being lost when cache is later initialized.
63
67
  script = <<~LUA
@@ -22,6 +22,14 @@ module SpreeCmCommissioner
22
22
  def base.rules_option_type
23
23
  Spree::OptionType.find_by(name: RULES_OPTION_TYPE_NAME)
24
24
  end
25
+
26
+ # override
27
+ def base.color
28
+ Spree::OptionType.find_or_create_by!(name: 'color') do |ot|
29
+ ot.presentation = 'Color'
30
+ ot.kind = :variant
31
+ end
32
+ end
25
33
  end
26
34
 
27
35
  private
@@ -3,6 +3,36 @@ module SpreeCmCommissioner
3
3
  def self.prepended(base)
4
4
  base.include SpreeCmCommissioner::OptionValueAttrType
5
5
  base.has_many :option_value_vehicles, class_name: 'SpreeCmCommissioner::OptionValueVehicle', foreign_key: :option_value_id, dependent: :destroy
6
+
7
+ base.validates :name, presence: true, uniqueness: { scope: :option_type_id, case_sensitive: -> { name_case_sensitive? } }
8
+
9
+ # Ticket types: case-SENSITIVE matching (preserve external API casing)
10
+ # Other types: case-INSENSITIVE storage (prevent duplicates from varying API casing; presentation keeps original)
11
+ def base.find_or_create_by_name!(option_type, name)
12
+ name = name.strip
13
+ if option_type.ticket_type?
14
+ where(option_type_id: option_type.id, name: name).first_or_create! do |ov|
15
+ ov.option_type = option_type
16
+ ov.name = name
17
+ ov.presentation = name
18
+ end
19
+ else
20
+ where(option_type_id: option_type.id).where('LOWER(name) = ?', name.downcase).first_or_create! do |ov|
21
+ ov.option_type = option_type
22
+ ov.name = name.downcase
23
+ ov.presentation = name
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ # Override Spree's name validation to make it case-sensitive only for ticket-type option values.
30
+ # Spree's default is case-insensitive (false), but we enforce case-sensitivity (true) for ticket-types.
31
+ # Which means ticket type "STANDARD" and "Standard" are different option values.
32
+ def name_case_sensitive?
33
+ return true if ticket_type?
34
+
35
+ false
6
36
  end
7
37
 
8
38
  def display_icon
@@ -14,5 +44,9 @@ module SpreeCmCommissioner
14
44
  end
15
45
 
16
46
  unless Spree::OptionValue.included_modules.include?(SpreeCmCommissioner::OptionValueDecorator)
47
+ # remove all name validations so we can override them in the decorator.
48
+ Spree::OptionValue._validators.reject! { |key, _| key == :name }
49
+ Spree::OptionValue._validate_callbacks.each { |c| c.filter.attributes.delete(:name) if c.filter.respond_to?(:attributes) }
50
+
17
51
  Spree::OptionValue.prepend SpreeCmCommissioner::OptionValueDecorator
18
52
  end
@@ -8,6 +8,7 @@ module SpreeCmCommissioner
8
8
  base.include SpreeCmCommissioner::TenantUpdatable
9
9
  base.include SpreeCmCommissioner::ServiceType
10
10
  base.include SpreeCmCommissioner::ServiceRecommendations
11
+ base.include SpreeCmCommissioner::Integrations::IntegrationMappable
11
12
 
12
13
  base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
13
14
  through: :product_option_types, source: :option_type
@@ -89,7 +90,7 @@ module SpreeCmCommissioner
89
90
  request_to_book: 8
90
91
  }
91
92
 
92
- base.multi_tenant :tenant, class_name: 'SpreeCmCommissioner::Tenant'
93
+ base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant'
93
94
  base.before_save :set_tenant
94
95
  end
95
96
 
@@ -142,7 +143,7 @@ module SpreeCmCommissioner
142
143
  end
143
144
 
144
145
  def set_event_id
145
- self.event_id = taxons.select(&:event?).first&.parent_id
146
+ self.event_id ||= taxons.select(&:event?).first&.parent_id
146
147
  end
147
148
 
148
149
  def update_variants_vendor_id
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
12
12
  keys = inventory_items.map { |item| "inventory:#{item.id}" }
13
13
  return [] unless keys.any?
14
14
 
15
- counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
15
+ counts = SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.mget(*keys) }
16
16
  inventory_items.map.with_index do |inventory_item, i|
17
17
  ::SpreeCmCommissioner::CachedInventoryItem.new(
18
18
  inventory_key: keys[i],
@@ -32,7 +32,7 @@ module SpreeCmCommissioner
32
32
 
33
33
  # Use atomic SET NX to prevent race condition where multiple concurrent reads
34
34
  # initialize cache with stale values. Only the first thread wins.
35
- SpreeCmCommissioner.redis_pool.with do |redis|
35
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
36
36
  redis.eval(set_nx_with_expiry_script, keys: [key], argv: [inventory_item.quantity_available, inventory_item.redis_expired_in])
37
37
  end
38
38
 
@@ -51,13 +51,13 @@ module SpreeCmCommissioner
51
51
  end
52
52
 
53
53
  def unstock(keys, quantities)
54
- SpreeCmCommissioner.redis_pool.with do |redis|
54
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
55
55
  redis.eval(unstock_redis_script, keys: keys, argv: quantities)
56
56
  end.positive?
57
57
  end
58
58
 
59
59
  def restock(keys, quantities)
60
- SpreeCmCommissioner.redis_pool.with do |redis|
60
+ SpreeCmCommissioner.inventory_redis_pool.with do |redis|
61
61
  redis.eval(restock_redis_script, keys: keys, argv: quantities)
62
62
  end.positive?
63
63
  end
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  base.include SpreeCmCommissioner::TaxonKind
6
6
  base.include SpreeCmCommissioner::ParticipationTypeBitwise
7
7
  base.include SpreeCmCommissioner::EventMetadata
8
+ base.include SpreeCmCommissioner::Integrations::IntegrationMappable
8
9
 
9
10
  base.has_many :taxon_vendors, class_name: 'SpreeCmCommissioner::TaxonVendor'
10
11
  base.has_many :vendors, through: :taxon_vendors
@@ -3,9 +3,21 @@ module SpreeCmCommissioner
3
3
  def self.prepended(base)
4
4
  base.include SpreeCmCommissioner::TaxonKind
5
5
 
6
+ def base.events
7
+ ActiveRecord::Base.connected_to(role: :writing) do
8
+ events = Spree::Taxonomy.find_or_create_by(name: 'Events', store: Spree::Store.default)
9
+ events.kind = :event
10
+ events.save if events.changed?
11
+ events
12
+ end
13
+ end
14
+
6
15
  def base.businesses
7
16
  ActiveRecord::Base.connected_to(role: :writing) do
8
- Spree::Taxonomy.find_or_create_by(name: 'Businesses', kind: 'category', store: Spree::Store.default)
17
+ businesses = Spree::Taxonomy.find_or_create_by(name: 'Businesses', store: Spree::Store.default)
18
+ businesses.kind = :category
19
+ businesses.save if businesses.changed?
20
+ businesses
9
21
  end
10
22
  end
11
23
  end
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
5
5
  base.include SpreeCmCommissioner::ProductDelegation
6
6
  base.include SpreeCmCommissioner::VariantOptionsConcern
7
7
  base.include SpreeCmCommissioner::KycBitwise
8
+ base.include SpreeCmCommissioner::Integrations::IntegrationMappable
8
9
 
9
10
  base.after_commit :update_vendor_price
10
11
  base.validate :validate_option_types
@@ -16,10 +17,6 @@ module SpreeCmCommissioner
16
17
  base.has_one :vehicle, class_name: 'SpreeCmCommissioner::Vehicle', through: :trip
17
18
 
18
19
  base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
19
- base.has_many :visible_option_values, lambda {
20
- joins(:option_type).where(spree_option_types: { hidden: false })
21
- }, through: :option_value_variants, source: :option_value
22
-
23
20
  base.has_many :video_on_demands, class_name: 'SpreeCmCommissioner::VideoOnDemand', dependent: :destroy
24
21
  base.has_many :complete_line_items, -> { complete }, class_name: 'Spree::LineItem'
25
22
 
@@ -42,6 +39,12 @@ module SpreeCmCommissioner
42
39
  base.before_validation -> { self.product_type = product.product_type }, if: -> { product_type.nil? }
43
40
  end
44
41
 
42
+ # Override variant.rb to return cached formatted options text, avoiding repeated database queries.
43
+ # Falls back to database queries & computing the format if preload data is unavailable.
44
+ def options_text
45
+ public_metadata[:preload_options_text].presence || Spree::Variants::VisibleOptionsPresenter.new(self).to_sentence
46
+ end
47
+
45
48
  def delivery_required?
46
49
  delivery_option == 'delivery'
47
50
  end
@@ -9,6 +9,7 @@ module SpreeCmCommissioner
9
9
  base.include SpreeCmCommissioner::VendorPreference
10
10
  base.include SpreeCmCommissioner::TenantUpdatable
11
11
  base.include SpreeCmCommissioner::StoreMetadata
12
+ base.include SpreeCmCommissioner::Integrations::IntegrationMappable
12
13
 
13
14
  base.attr_accessor :service_availabilities
14
15
 
@@ -0,0 +1,6 @@
1
+ <!-- insert_bottom "[data-hook='admin_integrations']" -->
2
+
3
+ <%= tab :integrations,
4
+ match_path: '/integrations',
5
+ label: Spree.t('admin.external_integrations_title'),
6
+ if: can?(:manage, SpreeCmCommissioner::Integration) %>
@@ -1,11 +1,9 @@
1
1
  module Spree
2
2
  module Variants
3
- class VisableOptionsPresenter < OptionsPresenter
4
- delegate :visible_option_values, to: :variant
5
-
3
+ class VisibleOptionsPresenter < OptionsPresenter
6
4
  # override
7
5
  def to_sentence
8
- options = visible_option_values
6
+ options = variant.option_values.reject { |ov| ov.option_type.hidden? }
9
7
  options = sort_options(options)
10
8
  options = present_options(options)
11
9
 
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module V2
3
+ module Storefront
4
+ class TenantSerializer < BaseSerializer
5
+ set_type :tenant
6
+
7
+ attributes :name, :slug, :host, :state
8
+
9
+ attribute :redirect_target_host, &:preferred_redirect_target_host
10
+ attribute :redirect_excluded_paths, &:preferred_redirect_excluded_paths
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,69 @@
1
+ module SpreeCmCommissioner::Integrations::Base
2
+ class SyncManager
3
+ def initialize(integration:, client:)
4
+ @integration = integration
5
+ @client = client
6
+ @sync_session = nil
7
+ end
8
+
9
+ # Perform a full synchronization (initial setup or complete refresh)
10
+ # Override in subclass and use run_execution to wrap your sync logic
11
+ # @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
12
+ # @raise [SyncError] if sync fails
13
+ def sync_full!
14
+ raise NotImplementedError, "#{self.class.name} must implement #sync_full!"
15
+ end
16
+
17
+ # Perform an incremental synchronization (delta updates)
18
+ # Override in subclass and use run_execution to wrap your sync logic
19
+ # @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
20
+ # @raise [SyncError] if sync fails or not supported
21
+ def sync_incremental!
22
+ raise NotImplementedError, "#{self.class.name} must implement #sync_incremental!"
23
+ end
24
+
25
+ # Perform a webhook-triggered synchronization (real-time event)
26
+ # Override in subclass and use run_execution to wrap your sync logic
27
+ # @param event_type [String] The type of event (e.g., 'match.updated', 'ticket.created')
28
+ # @param event_data [Hash] The event payload
29
+ # @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
30
+ # @raise [SyncError] if sync fails or not supported
31
+ def sync_webhook!(event_type:, event_data:)
32
+ raise NotImplementedError, "#{self.class.name} must implement #sync_webhook!"
33
+ end
34
+
35
+ protected
36
+
37
+ # Wrapper for sync execution that handles session creation and persistence
38
+ # Strategies handle errors internally and always return a SyncResult object
39
+ # This method never throws errors - it always returns a sync session
40
+ #
41
+ # @param sync_type [Symbol] The type of sync (:full, :incremental, :webhook_triggered)
42
+ # @yield Block containing the actual sync logic, must return a SyncResult object
43
+ # @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
44
+ def run_execution(sync_type)
45
+ @sync_session = create_sync_session(sync_type)
46
+ sync_result = yield
47
+
48
+ @sync_session.update!(
49
+ status: sync_result.success? ? :completed : :failed,
50
+ ended_at: Time.zone.now,
51
+ sync_result: sync_result.to_h
52
+ )
53
+
54
+ @sync_session
55
+ end
56
+
57
+ private
58
+
59
+ def create_sync_session(sync_type)
60
+ SpreeCmCommissioner::IntegrationSyncSession.create!(
61
+ integration: @integration,
62
+ tenant: @integration.tenant,
63
+ status: :in_progress,
64
+ started_at: Time.zone.now,
65
+ sync_type: sync_type
66
+ )
67
+ end
68
+ end
69
+ end