spree_cm_commissioner 2.5.0.pre.pre7 → 2.5.0.pre.pre8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test_and_build_gem.yml +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +7 -1
- data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +9 -9
- data/app/finders/spree_cm_commissioner/places/find_with_route.rb +10 -10
- data/app/finders/spree_cm_commissioner/routes/find_popular.rb +10 -14
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +3 -3
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -15
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +0 -2
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +2 -14
- data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +3 -0
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +7 -1
- data/app/models/spree_cm_commissioner/guest.rb +0 -13
- data/app/models/spree_cm_commissioner/inventory_item.rb +1 -5
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -13
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +0 -8
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +0 -34
- data/app/models/spree_cm_commissioner/order_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/product_decorator.rb +2 -3
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +0 -1
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +1 -13
- data/app/models/spree_cm_commissioner/variant_decorator.rb +4 -7
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +0 -4
- data/app/presenters/spree/variants/{visible_options_presenter.rb → visable_options_presenter.rb} +4 -2
- data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +12 -0
- data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +5 -0
- data/app/views/spree/admin/tenants/_form.html.erb +18 -0
- data/app/views/spree/admin/tenants/form/_footer.html.erb +31 -0
- data/app/views/spree/admin/tenants/form/_social.html.erb +31 -0
- data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
- data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +13 -6
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +23 -24
- data/config/locales/en.yml +0 -8
- data/config/locales/km.yml +0 -8
- data/config/routes.rb +0 -8
- data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +9 -0
- data/lib/cm_app_logger.rb +3 -21
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +7 -8
- metadata +7 -57
- data/app/controllers/spree/admin/integration_mappings_controller.rb +0 -21
- data/app/controllers/spree/admin/integration_sessions_controller.rb +0 -21
- data/app/controllers/spree/admin/integrations_controller.rb +0 -83
- data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +0 -15
- data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +0 -10
- data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +0 -4
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +0 -15
- data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +0 -58
- data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +0 -39
- data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +0 -53
- data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +0 -30
- data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +0 -17
- data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +0 -61
- data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +0 -36
- data/app/models/concerns/spree_cm_commissioner/order_integration.rb +0 -33
- data/app/models/spree_cm_commissioner/integration.rb +0 -29
- data/app/models/spree_cm_commissioner/integration_mapping.rb +0 -41
- data/app/models/spree_cm_commissioner/integration_sync_session.rb +0 -15
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +0 -37
- data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +0 -6
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +0 -69
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +0 -183
- data/app/services/spree_cm_commissioner/integrations/polling.rb +0 -70
- data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +0 -79
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +0 -152
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +0 -83
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +0 -113
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +0 -215
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +0 -20
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +0 -19
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +0 -95
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +0 -81
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +0 -19
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +0 -90
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +0 -35
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +0 -38
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +0 -44
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +0 -16
- data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +0 -49
- data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +0 -107
- data/app/views/spree/admin/integration_mappings/index.html.erb +0 -33
- data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +0 -116
- data/app/views/spree/admin/integration_sessions/index.html.erb +0 -42
- data/app/views/spree/admin/integrations/_form.html.erb +0 -104
- data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +0 -29
- data/app/views/spree/admin/integrations/edit.html.erb +0 -45
- data/app/views/spree/admin/integrations/index.html.erb +0 -75
- data/app/views/spree/admin/integrations/new.html.erb +0 -25
- data/db/migrate/20251017094845_create_cm_integrations.rb +0 -22
- data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +0 -68
- data/db/migrate/20251017101605_create_cm_integration_mappings.rb +0 -52
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +0 -25
- data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +0 -14
- data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +0 -7
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Caches the integration status of a line item's variant in private_metadata.
|
|
2
|
-
#
|
|
3
|
-
# Why cache?
|
|
4
|
-
# Most products don't have integrations, so checking `variant.vendor.integration.present?`
|
|
5
|
-
# on every read would cause N+1 queries. Caching avoids this performance hit.
|
|
6
|
-
#
|
|
7
|
-
# Behavior:
|
|
8
|
-
# The flag is set once at line item creation and remains immutable. This preserves
|
|
9
|
-
# the historical integration state of the order, even if the vendor's integration
|
|
10
|
-
# status changes later. We don't have use cases requiring dynamic updates of this yet,
|
|
11
|
-
# so we keep it constant for simplicity and consistency.
|
|
12
|
-
module SpreeCmCommissioner
|
|
13
|
-
module LineItemIntegration
|
|
14
|
-
extend ActiveSupport::Concern
|
|
15
|
-
|
|
16
|
-
included do
|
|
17
|
-
before_create :set_integration_flag
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def integration?
|
|
21
|
-
return false if private_metadata['integration'].nil?
|
|
22
|
-
|
|
23
|
-
ActiveModel::Type::Boolean.new.cast(private_metadata['integration'])
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def set_integration_flag
|
|
29
|
-
return if private_metadata.key?('integration')
|
|
30
|
-
return if variant.nil? || variant.vendor.nil?
|
|
31
|
-
|
|
32
|
-
# We use variant.vendor instead of .vendor directly as on create, the association may not be set to line item yet.
|
|
33
|
-
private_metadata['integration'] ||= variant.vendor.integration.present?
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module OrderIntegration
|
|
3
|
-
# Checks if any line item in this order has an integration (i.e., is from a vendor with an integration).
|
|
4
|
-
# This is used to determine if inventory management operations should be delegated to an external system.
|
|
5
|
-
def integration?
|
|
6
|
-
line_items.any?(&:integration?)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
# Attempts to unstock inventory for all integration line items in this order.
|
|
10
|
-
# Groups line items by vendor and delegates to the vendor's integration service.
|
|
11
|
-
def unstock_inventory_from_external_system!
|
|
12
|
-
integration_line_items = line_items.select(&:integration?)
|
|
13
|
-
return if integration_line_items.empty?
|
|
14
|
-
|
|
15
|
-
integration_line_items.group_by(&:vendor_id).each do |_, line_items|
|
|
16
|
-
integration = line_items.first&.vendor&.integration
|
|
17
|
-
integration.unstock_external_inventory!(self, line_items) if integration.present?
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Attempts to restock inventory for all integration line items in this order.
|
|
22
|
-
# Groups line items by vendor and delegates to the vendor's integration service.
|
|
23
|
-
def restock_inventory_from_external_system!
|
|
24
|
-
integration_line_items = line_items.select(&:integration?)
|
|
25
|
-
return if integration_line_items.empty?
|
|
26
|
-
|
|
27
|
-
integration_line_items.group_by(&:vendor_id).each do |_, line_items|
|
|
28
|
-
integration = line_items.first&.vendor&.integration
|
|
29
|
-
integration.restock_external_inventory!(self, line_items) if integration.present?
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,29 +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, inverse_of: :integration
|
|
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
|
-
|
|
21
|
-
def restock_external_inventory!(_order, _line_items)
|
|
22
|
-
raise NotImplementedError, 'Subclasses must implement the restock_external_inventory! method'
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def unstock_external_inventory!(_order, _line_items)
|
|
26
|
-
raise NotImplementedError, 'Subclasses must implement the unstock_external_inventory! method'
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
@@ -1,41 +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
|
-
# QR data for check-in. If external_wins? is true, use their QR data (they have their own check-in system).
|
|
15
|
-
# Otherwise, use our system's QR data. Only applicable to map with internal that has QR support (e.g., line_item, guest).
|
|
16
|
-
store_public_metadata :external_qr_data, :string, default: nil
|
|
17
|
-
|
|
18
|
-
def mark_as_archived!
|
|
19
|
-
self.status = :archived
|
|
20
|
-
self.last_synced_at = Time.zone.now
|
|
21
|
-
|
|
22
|
-
save!
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def mark_as_active!(external_payload:)
|
|
26
|
-
self.external_payload = external_payload
|
|
27
|
-
self.last_synced_at = Time.zone.now
|
|
28
|
-
self.status = :active
|
|
29
|
-
|
|
30
|
-
save!
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def validate_internal_exists
|
|
36
|
-
return if internal_type.safe_constantize&.exists?(id: internal_id)
|
|
37
|
-
|
|
38
|
-
errors.add(:internal, "record (#{internal_type}##{internal_id}) does not exist")
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
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,37 +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
|
-
# override
|
|
15
|
-
def unstock_external_inventory!(order, line_items)
|
|
16
|
-
result = SpreeCmCommissioner::Integrations::StadiumXV1::Inventory::UnstockInventory.new.call(
|
|
17
|
-
integration: self,
|
|
18
|
-
order: order,
|
|
19
|
-
line_items: line_items
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# override
|
|
26
|
-
def restock_external_inventory!(_order, _line_items)
|
|
27
|
-
raise SpreeCmCommissioner::Integrations::SyncError, 'Ticket cannot be cancelled'
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def client
|
|
31
|
-
SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(
|
|
32
|
-
public_key: public_key,
|
|
33
|
-
private_key: private_key,
|
|
34
|
-
base_url: base_url
|
|
35
|
-
)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -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
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module Integrations
|
|
3
|
-
class Polling
|
|
4
|
-
def call(integration:, sync_type:, event_type: nil, event_data: nil)
|
|
5
|
-
sync_manager = integration.sync_manager
|
|
6
|
-
|
|
7
|
-
case sync_type.to_sym
|
|
8
|
-
when :full
|
|
9
|
-
perform_full_sync!(sync_manager, integration)
|
|
10
|
-
when :incremental
|
|
11
|
-
perform_incremental_sync!(sync_manager, integration)
|
|
12
|
-
when :webhook_triggered
|
|
13
|
-
perform_webhook_sync!(sync_manager, integration, event_type, event_data)
|
|
14
|
-
else
|
|
15
|
-
raise ArgumentError, "Unknown sync type: #{sync_type}"
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
# Perform a full sync
|
|
22
|
-
# @param sync_manager [Object] The integration's sync manager
|
|
23
|
-
# @param integration [SpreeCmCommissioner::Integration]
|
|
24
|
-
def perform_full_sync!(sync_manager, integration)
|
|
25
|
-
CmAppLogger.log(
|
|
26
|
-
label: 'Integrations::IntegrationPullJob#perform_full_sync! Starting full sync',
|
|
27
|
-
data: {
|
|
28
|
-
integration_id: integration.id,
|
|
29
|
-
integration_type: integration.class.name
|
|
30
|
-
}
|
|
31
|
-
) do
|
|
32
|
-
sync_manager.sync_full!
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Perform an incremental sync
|
|
37
|
-
# @param sync_manager [Object] The integration's sync manager
|
|
38
|
-
# @param integration [SpreeCmCommissioner::Integration]
|
|
39
|
-
def perform_incremental_sync!(sync_manager, integration)
|
|
40
|
-
CmAppLogger.log(
|
|
41
|
-
label: 'Integrations::IntegrationPullJob#perform_incremental_sync! Starting incremental sync',
|
|
42
|
-
data: {
|
|
43
|
-
integration_id: integration.id,
|
|
44
|
-
integration_type: integration.class.name
|
|
45
|
-
}
|
|
46
|
-
) do
|
|
47
|
-
sync_manager.sync_incremental!
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# Perform a webhook-triggered sync
|
|
52
|
-
# @param sync_manager [Object] The integration's sync manager
|
|
53
|
-
# @param integration [SpreeCmCommissioner::Integration]
|
|
54
|
-
# @param event_type [String] The event type
|
|
55
|
-
# @param event_data [Hash] The event data
|
|
56
|
-
def perform_webhook_sync!(sync_manager, integration, event_type, event_data)
|
|
57
|
-
CmAppLogger.log(
|
|
58
|
-
label: 'Integrations::IntegrationPullJob#perform_webhook_sync! Starting webhook sync',
|
|
59
|
-
data: {
|
|
60
|
-
integration_id: integration.id,
|
|
61
|
-
integration_type: integration.class.name,
|
|
62
|
-
event_type: event_type
|
|
63
|
-
}
|
|
64
|
-
) do
|
|
65
|
-
sync_manager.sync_webhook!(event_type: event_type, event_data: event_data)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
module SpreeCmCommissioner
|
|
2
|
-
module Integrations
|
|
3
|
-
class PollingScheduler
|
|
4
|
-
BATCH_SIZE = 10 # Process integrations in batches to avoid overwhelming the server
|
|
5
|
-
|
|
6
|
-
def call
|
|
7
|
-
SpreeCmCommissioner::Integration.active.find_each(batch_size: BATCH_SIZE) do |integration|
|
|
8
|
-
if should_run_full_sync?(integration)
|
|
9
|
-
schedule_full_sync(integration)
|
|
10
|
-
elsif should_run_incremental_sync?(integration)
|
|
11
|
-
schedule_incremental_sync(integration)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
# Check if full sync should run
|
|
19
|
-
# Run full sync if:
|
|
20
|
-
# 1. No full sync is running or pending (excluding sessions older than 1 hour)
|
|
21
|
-
# 2. No previous full sync exists, OR
|
|
22
|
-
# 3. Last full sync was more than the configured interval ago (eg. 24 hours ago)
|
|
23
|
-
def should_run_full_sync?(integration)
|
|
24
|
-
return false if integration.integration_sync_sessions
|
|
25
|
-
.where(sync_type: %i[full], status: %i[in_progress pending])
|
|
26
|
-
.exists?(['created_at > ?', 1.hour.ago])
|
|
27
|
-
|
|
28
|
-
last_full_sync = integration.integration_sync_sessions
|
|
29
|
-
.where(sync_type: :full, status: :completed)
|
|
30
|
-
.order(created_at: :desc)
|
|
31
|
-
.first
|
|
32
|
-
|
|
33
|
-
return true if last_full_sync.blank?
|
|
34
|
-
|
|
35
|
-
full_sync_interval = integration.full_sync_interval_hours.hours
|
|
36
|
-
(Time.current - last_full_sync.created_at) > full_sync_interval
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Check if incremental sync should run
|
|
40
|
-
# Ignore sessions older than 1 hour to allow retry of stalled syncs
|
|
41
|
-
def should_run_incremental_sync?(integration)
|
|
42
|
-
return false if integration.integration_sync_sessions
|
|
43
|
-
.where(sync_type: %i[full incremental], status: %i[in_progress pending])
|
|
44
|
-
.exists?(['created_at > ?', 1.hour.ago])
|
|
45
|
-
|
|
46
|
-
last_incremental_sync = integration.integration_sync_sessions
|
|
47
|
-
.where(sync_type: :incremental, status: :completed)
|
|
48
|
-
.order(created_at: :desc)
|
|
49
|
-
.first
|
|
50
|
-
|
|
51
|
-
# Run incremental sync if:
|
|
52
|
-
# 1. No previous incremental sync exists, OR
|
|
53
|
-
# 2. Last incremental sync was more than the configured interval ago (eg. 10 seconds ago)
|
|
54
|
-
return true if last_incremental_sync.blank?
|
|
55
|
-
|
|
56
|
-
incremental_sync_interval = integration.incremental_sync_interval_seconds.seconds
|
|
57
|
-
(Time.current - last_incremental_sync.created_at) > incremental_sync_interval
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def schedule_full_sync(integration)
|
|
61
|
-
CmAppLogger.log(
|
|
62
|
-
label: 'Integrations::PollingSchedulerJob#schedule_full_sync Enqueued full sync',
|
|
63
|
-
data: { integration_id: integration.id, integration_type: integration.class.name }
|
|
64
|
-
) do
|
|
65
|
-
PollingJob.perform_later(integration_id: integration.id, sync_type: :full)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def schedule_incremental_sync(integration)
|
|
70
|
-
CmAppLogger.log(
|
|
71
|
-
label: 'Integrations::PollingSchedulerJob#schedule_incremental_sync Enqueued incremental sync',
|
|
72
|
-
data: { integration_id: integration.id, integration_type: integration.class.name }
|
|
73
|
-
) do
|
|
74
|
-
PollingJob.perform_later(integration_id: integration.id, sync_type: :incremental)
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|