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.
- 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/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +2 -4
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +4 -5
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +3 -15
- 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/inventory_item.rb +1 -5
- 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/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 -1
- 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 +0 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +7 -8
- metadata +7 -54
- 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 -58
- data/app/models/spree_cm_commissioner/integration.rb +0 -21
- data/app/models/spree_cm_commissioner/integration_mapping.rb +0 -37
- data/app/models/spree_cm_commissioner/integration_sync_session.rb +0 -15
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +0 -21
- 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/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,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,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
|