spree_cm_commissioner 2.4.3 → 2.5.0.pre.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/admin/integration_mappings_controller.rb +21 -0
- data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
- data/app/controllers/spree/admin/integrations_controller.rb +83 -0
- data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
- data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +10 -0
- data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +4 -0
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +15 -0
- data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
- data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +4 -2
- data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
- data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
- data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +5 -4
- data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
- data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +58 -0
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +15 -3
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
- data/app/models/spree_cm_commissioner/integration.rb +21 -0
- data/app/models/spree_cm_commissioner/integration_mapping.rb +37 -0
- data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +21 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +3 -2
- 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 +1 -0
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
- data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
- data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
- data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
- data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
- data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
- data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
- data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
- data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
- data/app/views/spree/admin/integrations/_form.html.erb +104 -0
- data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
- data/app/views/spree/admin/integrations/edit.html.erb +45 -0
- data/app/views/spree/admin/integrations/index.html.erb +75 -0
- data/app/views/spree/admin/integrations/new.html.erb +25 -0
- data/config/locales/en.yml +8 -0
- data/config/locales/km.yml +8 -0
- data/config/routes.rb +8 -0
- data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
- data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
- data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
- data/lib/cm_app_logger.rb +1 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +8 -7
- metadata +56 -5
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class IntegrationMapping < Base
|
|
3
|
+
include StoreMetadata
|
|
4
|
+
|
|
5
|
+
enum status: { active: 0, archived: 1 }
|
|
6
|
+
|
|
7
|
+
# polymorphic (Spree::Taxon, Spree::Product, Spree::Variant, Spree::Vendor)
|
|
8
|
+
belongs_to :internal, polymorphic: true, optional: false
|
|
9
|
+
belongs_to :integration, class_name: 'SpreeCmCommissioner::Integration', inverse_of: :integration_mappings, optional: false
|
|
10
|
+
|
|
11
|
+
validates :external_id, presence: true, uniqueness: { scope: %i[integration_id internal_type internal_id date] }
|
|
12
|
+
validate :validate_internal_exists, if: -> { internal_type.present? && internal_id.present? }
|
|
13
|
+
|
|
14
|
+
def mark_as_archived!
|
|
15
|
+
self.status = :archived
|
|
16
|
+
self.last_synced_at = Time.zone.now
|
|
17
|
+
|
|
18
|
+
save!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mark_as_active!(external_payload:)
|
|
22
|
+
self.external_payload = external_payload
|
|
23
|
+
self.last_synced_at = Time.zone.now
|
|
24
|
+
self.status = :active
|
|
25
|
+
|
|
26
|
+
save!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def validate_internal_exists
|
|
32
|
+
return if internal_type.safe_constantize&.exists?(id: internal_id)
|
|
33
|
+
|
|
34
|
+
errors.add(:internal, "record (#{internal_type}##{internal_id}) does not exist")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
class IntegrationSyncSession < Base
|
|
3
|
+
include StoreMetadata
|
|
4
|
+
|
|
5
|
+
enum status: { pending: 0, in_progress: 1, completed: 2, failed: 3, canceled: 4 }
|
|
6
|
+
enum :sync_type, {
|
|
7
|
+
full: 0, # on initial setup or periodic complete refresh
|
|
8
|
+
incremental: 1, # on scheduled intervals to fetch recent changes
|
|
9
|
+
webhook_triggered: 2 # immediately when an external webhook event is received
|
|
10
|
+
}, prefix: true
|
|
11
|
+
|
|
12
|
+
belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
|
|
13
|
+
belongs_to :integration, class_name: 'SpreeCmCommissioner::Integration', inverse_of: :integration_sync_sessions, optional: false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integration
|
|
2
|
+
store_private_metadata :public_key, :string
|
|
3
|
+
store_private_metadata :private_key, :string
|
|
4
|
+
store_private_metadata :base_url, :string # e.g. 'https://api.stadiumx.com'
|
|
5
|
+
|
|
6
|
+
# override
|
|
7
|
+
def sync_manager
|
|
8
|
+
SpreeCmCommissioner::Integrations::StadiumXV1::SyncManager.new(
|
|
9
|
+
integration: self,
|
|
10
|
+
client: client
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def client
|
|
15
|
+
SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(
|
|
16
|
+
public_key: public_key,
|
|
17
|
+
private_key: private_key,
|
|
18
|
+
base_url: base_url
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -56,8 +56,12 @@ module SpreeCmCommissioner
|
|
|
56
56
|
"inventory:#{id}"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def quantity_in_redis
|
|
60
|
+
SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.get(redis_key).to_i }
|
|
61
|
+
end
|
|
62
|
+
|
|
59
63
|
def adjust_quantity_in_redis(quantity)
|
|
60
|
-
SpreeCmCommissioner.
|
|
64
|
+
SpreeCmCommissioner.inventory_redis_pool.with do |redis|
|
|
61
65
|
# Always update Redis cache, even if it doesn't exist yet.
|
|
62
66
|
# This prevents admin adjustments from being lost when cache is later initialized.
|
|
63
67
|
script = <<~LUA
|
|
@@ -22,6 +22,14 @@ module SpreeCmCommissioner
|
|
|
22
22
|
def base.rules_option_type
|
|
23
23
|
Spree::OptionType.find_by(name: RULES_OPTION_TYPE_NAME)
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# override
|
|
27
|
+
def base.color
|
|
28
|
+
Spree::OptionType.find_or_create_by!(name: 'color') do |ot|
|
|
29
|
+
ot.presentation = 'Color'
|
|
30
|
+
ot.kind = :variant
|
|
31
|
+
end
|
|
32
|
+
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
private
|
|
@@ -3,6 +3,36 @@ module SpreeCmCommissioner
|
|
|
3
3
|
def self.prepended(base)
|
|
4
4
|
base.include SpreeCmCommissioner::OptionValueAttrType
|
|
5
5
|
base.has_many :option_value_vehicles, class_name: 'SpreeCmCommissioner::OptionValueVehicle', foreign_key: :option_value_id, dependent: :destroy
|
|
6
|
+
|
|
7
|
+
base.validates :name, presence: true, uniqueness: { scope: :option_type_id, case_sensitive: -> { name_case_sensitive? } }
|
|
8
|
+
|
|
9
|
+
# Ticket types: case-SENSITIVE matching (preserve external API casing)
|
|
10
|
+
# Other types: case-INSENSITIVE storage (prevent duplicates from varying API casing; presentation keeps original)
|
|
11
|
+
def base.find_or_create_by_name!(option_type, name)
|
|
12
|
+
name = name.strip
|
|
13
|
+
if option_type.ticket_type?
|
|
14
|
+
where(option_type_id: option_type.id, name: name).first_or_create! do |ov|
|
|
15
|
+
ov.option_type = option_type
|
|
16
|
+
ov.name = name
|
|
17
|
+
ov.presentation = name
|
|
18
|
+
end
|
|
19
|
+
else
|
|
20
|
+
where(option_type_id: option_type.id).where('LOWER(name) = ?', name.downcase).first_or_create! do |ov|
|
|
21
|
+
ov.option_type = option_type
|
|
22
|
+
ov.name = name.downcase
|
|
23
|
+
ov.presentation = name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Override Spree's name validation to make it case-sensitive only for ticket-type option values.
|
|
30
|
+
# Spree's default is case-insensitive (false), but we enforce case-sensitivity (true) for ticket-types.
|
|
31
|
+
# Which means ticket type "STANDARD" and "Standard" are different option values.
|
|
32
|
+
def name_case_sensitive?
|
|
33
|
+
return true if ticket_type?
|
|
34
|
+
|
|
35
|
+
false
|
|
6
36
|
end
|
|
7
37
|
|
|
8
38
|
def display_icon
|
|
@@ -14,5 +44,9 @@ module SpreeCmCommissioner
|
|
|
14
44
|
end
|
|
15
45
|
|
|
16
46
|
unless Spree::OptionValue.included_modules.include?(SpreeCmCommissioner::OptionValueDecorator)
|
|
47
|
+
# remove all name validations so we can override them in the decorator.
|
|
48
|
+
Spree::OptionValue._validators.reject! { |key, _| key == :name }
|
|
49
|
+
Spree::OptionValue._validate_callbacks.each { |c| c.filter.attributes.delete(:name) if c.filter.respond_to?(:attributes) }
|
|
50
|
+
|
|
17
51
|
Spree::OptionValue.prepend SpreeCmCommissioner::OptionValueDecorator
|
|
18
52
|
end
|
|
@@ -8,6 +8,7 @@ module SpreeCmCommissioner
|
|
|
8
8
|
base.include SpreeCmCommissioner::TenantUpdatable
|
|
9
9
|
base.include SpreeCmCommissioner::ServiceType
|
|
10
10
|
base.include SpreeCmCommissioner::ServiceRecommendations
|
|
11
|
+
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
11
12
|
|
|
12
13
|
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
|
|
13
14
|
through: :product_option_types, source: :option_type
|
|
@@ -89,7 +90,7 @@ module SpreeCmCommissioner
|
|
|
89
90
|
request_to_book: 8
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
base.
|
|
93
|
+
base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant'
|
|
93
94
|
base.before_save :set_tenant
|
|
94
95
|
end
|
|
95
96
|
|
|
@@ -142,7 +143,7 @@ module SpreeCmCommissioner
|
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
def set_event_id
|
|
145
|
-
self.event_id
|
|
146
|
+
self.event_id ||= taxons.select(&:event?).first&.parent_id
|
|
146
147
|
end
|
|
147
148
|
|
|
148
149
|
def update_variants_vendor_id
|
|
@@ -12,7 +12,7 @@ module SpreeCmCommissioner
|
|
|
12
12
|
keys = inventory_items.map { |item| "inventory:#{item.id}" }
|
|
13
13
|
return [] unless keys.any?
|
|
14
14
|
|
|
15
|
-
counts = SpreeCmCommissioner.
|
|
15
|
+
counts = SpreeCmCommissioner.inventory_redis_pool.with { |redis| redis.mget(*keys) }
|
|
16
16
|
inventory_items.map.with_index do |inventory_item, i|
|
|
17
17
|
::SpreeCmCommissioner::CachedInventoryItem.new(
|
|
18
18
|
inventory_key: keys[i],
|
|
@@ -32,7 +32,7 @@ module SpreeCmCommissioner
|
|
|
32
32
|
|
|
33
33
|
# Use atomic SET NX to prevent race condition where multiple concurrent reads
|
|
34
34
|
# initialize cache with stale values. Only the first thread wins.
|
|
35
|
-
SpreeCmCommissioner.
|
|
35
|
+
SpreeCmCommissioner.inventory_redis_pool.with do |redis|
|
|
36
36
|
redis.eval(set_nx_with_expiry_script, keys: [key], argv: [inventory_item.quantity_available, inventory_item.redis_expired_in])
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -51,13 +51,13 @@ module SpreeCmCommissioner
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def unstock(keys, quantities)
|
|
54
|
-
SpreeCmCommissioner.
|
|
54
|
+
SpreeCmCommissioner.inventory_redis_pool.with do |redis|
|
|
55
55
|
redis.eval(unstock_redis_script, keys: keys, argv: quantities)
|
|
56
56
|
end.positive?
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def restock(keys, quantities)
|
|
60
|
-
SpreeCmCommissioner.
|
|
60
|
+
SpreeCmCommissioner.inventory_redis_pool.with do |redis|
|
|
61
61
|
redis.eval(restock_redis_script, keys: keys, argv: quantities)
|
|
62
62
|
end.positive?
|
|
63
63
|
end
|
|
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
|
|
|
5
5
|
base.include SpreeCmCommissioner::TaxonKind
|
|
6
6
|
base.include SpreeCmCommissioner::ParticipationTypeBitwise
|
|
7
7
|
base.include SpreeCmCommissioner::EventMetadata
|
|
8
|
+
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
8
9
|
|
|
9
10
|
base.has_many :taxon_vendors, class_name: 'SpreeCmCommissioner::TaxonVendor'
|
|
10
11
|
base.has_many :vendors, through: :taxon_vendors
|
|
@@ -3,9 +3,21 @@ module SpreeCmCommissioner
|
|
|
3
3
|
def self.prepended(base)
|
|
4
4
|
base.include SpreeCmCommissioner::TaxonKind
|
|
5
5
|
|
|
6
|
+
def base.events
|
|
7
|
+
ActiveRecord::Base.connected_to(role: :writing) do
|
|
8
|
+
events = Spree::Taxonomy.find_or_create_by(name: 'Events', store: Spree::Store.default)
|
|
9
|
+
events.kind = :event
|
|
10
|
+
events.save if events.changed?
|
|
11
|
+
events
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
def base.businesses
|
|
7
16
|
ActiveRecord::Base.connected_to(role: :writing) do
|
|
8
|
-
Spree::Taxonomy.find_or_create_by(name: 'Businesses',
|
|
17
|
+
businesses = Spree::Taxonomy.find_or_create_by(name: 'Businesses', store: Spree::Store.default)
|
|
18
|
+
businesses.kind = :category
|
|
19
|
+
businesses.save if businesses.changed?
|
|
20
|
+
businesses
|
|
9
21
|
end
|
|
10
22
|
end
|
|
11
23
|
end
|
|
@@ -5,6 +5,7 @@ module SpreeCmCommissioner
|
|
|
5
5
|
base.include SpreeCmCommissioner::ProductDelegation
|
|
6
6
|
base.include SpreeCmCommissioner::VariantOptionsConcern
|
|
7
7
|
base.include SpreeCmCommissioner::KycBitwise
|
|
8
|
+
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
8
9
|
|
|
9
10
|
base.after_commit :update_vendor_price
|
|
10
11
|
base.validate :validate_option_types
|
|
@@ -16,10 +17,6 @@ module SpreeCmCommissioner
|
|
|
16
17
|
base.has_one :vehicle, class_name: 'SpreeCmCommissioner::Vehicle', through: :trip
|
|
17
18
|
|
|
18
19
|
base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
|
|
19
|
-
base.has_many :visible_option_values, lambda {
|
|
20
|
-
joins(:option_type).where(spree_option_types: { hidden: false })
|
|
21
|
-
}, through: :option_value_variants, source: :option_value
|
|
22
|
-
|
|
23
20
|
base.has_many :video_on_demands, class_name: 'SpreeCmCommissioner::VideoOnDemand', dependent: :destroy
|
|
24
21
|
base.has_many :complete_line_items, -> { complete }, class_name: 'Spree::LineItem'
|
|
25
22
|
|
|
@@ -42,6 +39,12 @@ module SpreeCmCommissioner
|
|
|
42
39
|
base.before_validation -> { self.product_type = product.product_type }, if: -> { product_type.nil? }
|
|
43
40
|
end
|
|
44
41
|
|
|
42
|
+
# Override variant.rb to return cached formatted options text, avoiding repeated database queries.
|
|
43
|
+
# Falls back to database queries & computing the format if preload data is unavailable.
|
|
44
|
+
def options_text
|
|
45
|
+
public_metadata[:preload_options_text].presence || Spree::Variants::VisibleOptionsPresenter.new(self).to_sentence
|
|
46
|
+
end
|
|
47
|
+
|
|
45
48
|
def delivery_required?
|
|
46
49
|
delivery_option == 'delivery'
|
|
47
50
|
end
|
|
@@ -9,6 +9,7 @@ module SpreeCmCommissioner
|
|
|
9
9
|
base.include SpreeCmCommissioner::VendorPreference
|
|
10
10
|
base.include SpreeCmCommissioner::TenantUpdatable
|
|
11
11
|
base.include SpreeCmCommissioner::StoreMetadata
|
|
12
|
+
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
12
13
|
|
|
13
14
|
base.attr_accessor :service_availabilities
|
|
14
15
|
|
data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb}
RENAMED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Variants
|
|
3
|
-
class
|
|
4
|
-
delegate :visible_option_values, to: :variant
|
|
5
|
-
|
|
3
|
+
class VisibleOptionsPresenter < OptionsPresenter
|
|
6
4
|
# override
|
|
7
5
|
def to_sentence
|
|
8
|
-
options =
|
|
6
|
+
options = variant.option_values.reject { |ov| ov.option_type.hidden? }
|
|
9
7
|
options = sort_options(options)
|
|
10
8
|
options = present_options(options)
|
|
11
9
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module SpreeCmCommissioner::Integrations::Base
|
|
2
|
+
class SyncManager
|
|
3
|
+
def initialize(integration:, client:)
|
|
4
|
+
@integration = integration
|
|
5
|
+
@client = client
|
|
6
|
+
@sync_session = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Perform a full synchronization (initial setup or complete refresh)
|
|
10
|
+
# Override in subclass and use run_execution to wrap your sync logic
|
|
11
|
+
# @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
|
|
12
|
+
# @raise [SyncError] if sync fails
|
|
13
|
+
def sync_full!
|
|
14
|
+
raise NotImplementedError, "#{self.class.name} must implement #sync_full!"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Perform an incremental synchronization (delta updates)
|
|
18
|
+
# Override in subclass and use run_execution to wrap your sync logic
|
|
19
|
+
# @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
|
|
20
|
+
# @raise [SyncError] if sync fails or not supported
|
|
21
|
+
def sync_incremental!
|
|
22
|
+
raise NotImplementedError, "#{self.class.name} must implement #sync_incremental!"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Perform a webhook-triggered synchronization (real-time event)
|
|
26
|
+
# Override in subclass and use run_execution to wrap your sync logic
|
|
27
|
+
# @param event_type [String] The type of event (e.g., 'match.updated', 'ticket.created')
|
|
28
|
+
# @param event_data [Hash] The event payload
|
|
29
|
+
# @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
|
|
30
|
+
# @raise [SyncError] if sync fails or not supported
|
|
31
|
+
def sync_webhook!(event_type:, event_data:)
|
|
32
|
+
raise NotImplementedError, "#{self.class.name} must implement #sync_webhook!"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
# Wrapper for sync execution that handles session creation and persistence
|
|
38
|
+
# Strategies handle errors internally and always return a SyncResult object
|
|
39
|
+
# This method never throws errors - it always returns a sync session
|
|
40
|
+
#
|
|
41
|
+
# @param sync_type [Symbol] The type of sync (:full, :incremental, :webhook_triggered)
|
|
42
|
+
# @yield Block containing the actual sync logic, must return a SyncResult object
|
|
43
|
+
# @return [SpreeCmCommissioner::IntegrationSyncSession] The sync session record
|
|
44
|
+
def run_execution(sync_type)
|
|
45
|
+
@sync_session = create_sync_session(sync_type)
|
|
46
|
+
sync_result = yield
|
|
47
|
+
|
|
48
|
+
@sync_session.update!(
|
|
49
|
+
status: sync_result.success? ? :completed : :failed,
|
|
50
|
+
ended_at: Time.zone.now,
|
|
51
|
+
sync_result: sync_result.to_h
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@sync_session
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def create_sync_session(sync_type)
|
|
60
|
+
SpreeCmCommissioner::IntegrationSyncSession.create!(
|
|
61
|
+
integration: @integration,
|
|
62
|
+
tenant: @integration.tenant,
|
|
63
|
+
status: :in_progress,
|
|
64
|
+
started_at: Time.zone.now,
|
|
65
|
+
sync_type: sync_type
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|