spree_cm_commissioner 2.5.0.pre.pre1 → 2.5.0.pre.pre2

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 (94) 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/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +2 -4
  11. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +4 -5
  12. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -15
  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/inventory_item.rb +1 -5
  17. data/app/models/spree_cm_commissioner/option_type_decorator.rb +0 -8
  18. data/app/models/spree_cm_commissioner/option_value_decorator.rb +0 -34
  19. data/app/models/spree_cm_commissioner/product_decorator.rb +2 -3
  20. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  21. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  22. data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
  23. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -13
  24. data/app/models/spree_cm_commissioner/variant_decorator.rb +4 -7
  25. data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -1
  26. data/app/presenters/spree/variants/{visible_options_presenter.rb → visable_options_presenter.rb} +4 -2
  27. data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +12 -0
  28. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +5 -0
  29. data/app/views/spree/admin/tenants/_form.html.erb +18 -0
  30. data/app/views/spree/admin/tenants/form/_footer.html.erb +31 -0
  31. data/app/views/spree/admin/tenants/form/_social.html.erb +31 -0
  32. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  33. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
  34. data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +13 -6
  35. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +23 -24
  36. data/config/locales/en.yml +0 -8
  37. data/config/locales/km.yml +0 -8
  38. data/config/routes.rb +0 -8
  39. data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +9 -0
  40. data/lib/cm_app_logger.rb +0 -1
  41. data/lib/spree_cm_commissioner/version.rb +1 -1
  42. data/lib/spree_cm_commissioner.rb +7 -8
  43. metadata +7 -54
  44. data/app/controllers/spree/admin/integration_mappings_controller.rb +0 -21
  45. data/app/controllers/spree/admin/integration_sessions_controller.rb +0 -21
  46. data/app/controllers/spree/admin/integrations_controller.rb +0 -83
  47. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +0 -15
  48. data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +0 -10
  49. data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +0 -4
  50. data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -15
  51. data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +0 -58
  52. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +0 -39
  53. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +0 -53
  54. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +0 -30
  55. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +0 -17
  56. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +0 -58
  57. data/app/models/spree_cm_commissioner/integration.rb +0 -21
  58. data/app/models/spree_cm_commissioner/integration_mapping.rb +0 -37
  59. data/app/models/spree_cm_commissioner/integration_sync_session.rb +0 -15
  60. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +0 -21
  61. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +0 -6
  62. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +0 -69
  63. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +0 -183
  64. data/app/services/spree_cm_commissioner/integrations/polling.rb +0 -70
  65. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +0 -79
  66. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +0 -152
  67. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +0 -113
  68. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +0 -215
  69. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +0 -20
  70. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +0 -19
  71. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +0 -95
  72. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +0 -81
  73. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +0 -19
  74. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +0 -90
  75. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -35
  76. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +0 -38
  77. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +0 -44
  78. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +0 -16
  79. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +0 -49
  80. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +0 -107
  81. data/app/views/spree/admin/integration_mappings/index.html.erb +0 -33
  82. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +0 -116
  83. data/app/views/spree/admin/integration_sessions/index.html.erb +0 -42
  84. data/app/views/spree/admin/integrations/_form.html.erb +0 -104
  85. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +0 -29
  86. data/app/views/spree/admin/integrations/edit.html.erb +0 -45
  87. data/app/views/spree/admin/integrations/index.html.erb +0 -75
  88. data/app/views/spree/admin/integrations/new.html.erb +0 -25
  89. data/db/migrate/20251017094845_create_cm_integrations.rb +0 -22
  90. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +0 -68
  91. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +0 -52
  92. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +0 -25
  93. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +0 -14
  94. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +0 -7
@@ -1,15 +0,0 @@
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
@@ -1,10 +0,0 @@
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
@@ -1,4 +0,0 @@
1
- module SpreeCmCommissioner::Integrations
2
- class SyncError < StandardError
3
- end
4
- end
@@ -1,15 +0,0 @@
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
@@ -1,58 +0,0 @@
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
@@ -1,39 +0,0 @@
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
@@ -1,53 +0,0 @@
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
@@ -1,30 +0,0 @@
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,17 +0,0 @@
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,58 +0,0 @@
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
- end
28
-
29
- class_methods do
30
- def find_or_initialize_integration_mapping(integration_id:, external_id:)
31
- IntegrationMapping.includes(:internal).find_or_initialize_by(
32
- integration_id: integration_id,
33
- internal_type: name,
34
- external_id: external_id
35
- ) do |mapping|
36
- mapping.internal = new
37
- mapping.external_id = external_id
38
- end
39
- end
40
-
41
- def find_integration_mapping(integration_id:, external_id:)
42
- IntegrationMapping.includes(:internal).find_by(
43
- integration_id: integration_id,
44
- internal_type: name,
45
- external_id: external_id
46
- )
47
- end
48
-
49
- def find_oldest_active_mapping(integration_id:)
50
- IntegrationMapping.where(
51
- integration_id: integration_id,
52
- internal_type: name
53
- ).active.order(:last_synced_at).first
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,21 +0,0 @@
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
@@ -1,37 +0,0 @@
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
@@ -1,15 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,6 +0,0 @@
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,69 +0,0 @@
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
@@ -1,183 +0,0 @@
1
- module SpreeCmCommissioner::Integrations::Base
2
- # Base class for sync results
3
- # Provides standardized tracking methods for API calls and record changes
4
- #
5
- # Persists to database as JSONB in CmIntegrationSyncSession#sync_result column
6
- # Enables comprehensive sync monitoring and debugging:
7
- # - Track success/failure status
8
- # - Monitor API call counts and patterns
9
- # - Audit record creation/update counts
10
- # - Capture error details with automatic error codes
11
- # - Generate sync performance reports
12
- # - Identify patterns in failed syncs
13
- #
14
- # Example persisted results:
15
- #
16
- # Success:
17
- # {
18
- # "metrics": { "match": { "created": 2, "updated": 1, "total": 3 } },
19
- # "api_calls": { "get_matches": 1, "get_match_details": 3 }
20
- # }
21
- #
22
- # Failure:
23
- # {
24
- # "metrics": { "match": { "created": 1, "total": 1 } },
25
- # "api_calls": { "get_matches": 1 },
26
- # "error": { "message": "Failed to sync matches: Connection timeout", "code": "ExternalClientError" }
27
- # }
28
- class SyncResult
29
- def initialize
30
- @metrics = {}
31
- @api_calls = {}
32
- @error = nil
33
- end
34
-
35
- # Track API call with block
36
- # Automatically increments the call count and executes the block
37
- # Failed calls (exceptions) are not counted
38
- #
39
- # @param call_name [String] Name of the API call (e.g., 'get_matches', 'get_zones')
40
- # @yield Block containing the API call
41
- # @return Result of the block
42
- #
43
- # Example:
44
- # external_matches = track_api_call('get_matches') { @client.get_matches! }
45
- def track_api_call(call_name)
46
- @api_calls[call_name] = (@api_calls[call_name] || 0) + 1
47
- yield
48
- end
49
-
50
- # Track synced record with block
51
- # Automatically detects if record was created or updated based on new_record? and saved_changes
52
- #
53
- # @param type [Symbol, String] Type of record (e.g., :match, :zone)
54
- # @param record [Object] The record being synced (must respond to new_record? and saved_changes)
55
- # @yield Block containing the save/update logic
56
- # @return Result of the block
57
- #
58
- # Example:
59
- # track_synced_record(:match, match_taxon) do
60
- # match_taxon.assign_attributes(name: 'ISI Dangkorsenchey FC vs ANGKOR TIGER FC')
61
- # match_taxon.save!
62
- # end
63
- def track_synced_record(type, record)
64
- was_new = record.new_record?
65
- result = yield
66
-
67
- if was_new
68
- increment_metric(type, :created)
69
- elsif record.saved_changes.any?
70
- increment_metric(type, :updated)
71
- end
72
-
73
- result
74
- end
75
-
76
- # Track and sync record with explicit tracker object
77
- # Provides a tracker object with helper methods for explicit save/tracking operations
78
- #
79
- # @param type [Symbol, String] Type of record (e.g., :league, :match, :zone, :variant)
80
- # @param record [Object] The record being synced (must respond to new_record? and saved_changes)
81
- # @yield Block receiving tracker object for explicit operations
82
- # @return Result of the block
83
- #
84
- # Example:
85
- # track(:zone, product) do |tracker|
86
- # product.assign_attributes(name: 'Zone A', price: 100)
87
- # tracker.save_if_changed!(product)
88
- # end
89
- def track(type, record)
90
- tracker = RecordTracker.new(self, type, record)
91
- yield tracker
92
- tracker
93
- end
94
-
95
- # Helper class for explicit record tracking and saving
96
- class RecordTracker
97
- def initialize(sync_result, type, record)
98
- @sync_result = sync_result
99
- @type = type
100
- @record = record
101
- @was_new = record.new_record?
102
- end
103
-
104
- # Save record if it's new or has changes, and track the operation
105
- # Automatically detects create vs update based on initial state
106
- #
107
- # @param record [Object] The record to save (defaults to the tracked record)
108
- def save_if_changed!(record = @record)
109
- return unless record.new_record? || record.changed?
110
-
111
- record.save!
112
-
113
- # Track based on initial state
114
- if @was_new
115
- @sync_result.increment_metric(@type, :created)
116
- else
117
- @sync_result.increment_metric(@type, :updated)
118
- end
119
- end
120
- end
121
-
122
- # Increment a metric counter
123
- # Useful for tracking actions that don't fit the created/updated pattern
124
- #
125
- # @param type [Symbol, String] Type of metric (e.g., :match, :inventory)
126
- # @param action [Symbol, String] Action performed (e.g., :archived, :adjustments)
127
- # @param amount [Integer] Amount to increment by (default: 1)
128
- #
129
- # Example:
130
- # increment_metric(:match, :archived) # increment by 1
131
- # increment_metric(:inventory, :total_quantity_adjusted, 50) # increment by 50
132
- def increment_metric(type, action, amount = 1)
133
- @metrics[type.to_sym] ||= {}
134
- @metrics[type.to_sym][action.to_sym] ||= 0
135
- @metrics[type.to_sym][action.to_sym] += amount
136
-
137
- update_metric_total(type)
138
- end
139
-
140
- # Record error
141
- # @param error [StandardError] The error that occurred
142
- def record_error(error)
143
- @error = {
144
- message: error.message,
145
- code: error.class.name.split('::').last
146
- }
147
- end
148
-
149
- # Check if sync was successful
150
- # @return [Boolean] True if no error, false otherwise
151
- def success?
152
- @error.nil?
153
- end
154
-
155
- # Serialize to hash for JSONB persistence
156
- # @return [Hash] Serialized representation
157
- def to_h
158
- {
159
- metrics: @metrics,
160
- api_calls: @api_calls,
161
- error: @error
162
- }.tap do |hash|
163
- hash.delete(:error) if @error.nil?
164
- hash.delete(:metrics) if @metrics.empty?
165
- hash.delete(:api_calls) if @api_calls.empty?
166
- end
167
- end
168
-
169
- private
170
-
171
- # Update total for a metric type
172
- # Sums all numeric values in the metric hash
173
- def update_metric_total(type)
174
- metric = @metrics[type.to_sym]
175
- return unless metric.is_a?(Hash)
176
-
177
- # Calculate total from numeric values only
178
- numeric_values = metric.select { |k, v| v.is_a?(Integer) && k != :total }
179
- total = numeric_values.values.sum
180
- metric[:total] = total if total.positive?
181
- end
182
- end
183
- end