spree_cm_commissioner 2.13.3.pre.pre1 → 2.13.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6debbf57a54a0a2fcbc3ba4ee4c3e07045c520377afbd0ad13d6f6fde2120654
4
- data.tar.gz: 3c66791879a8187925ac7ecba67437a4e049bce327f7d19ff9f435d0acddd54e
3
+ metadata.gz: 1c04efc07b0defa6eedb7b800bf8c5504401b7de9042751e32a6eb40c1ec6897
4
+ data.tar.gz: 414a5fb2a306e1a1e948c9e08cbb14e412752b112c2509332ba6c15132870369
5
5
  SHA512:
6
- metadata.gz: f5a7c7f1e396e46ae559484c92900a1180ede361e562b192a0b13abc922779f521fbc093a86d695b760abcc8609e0563785e5fadd285fb919670c0c8816bf5d9
7
- data.tar.gz: 124912777c8bacddd0a1f067ab1e6faaa245af600da5343150f4862fd5c9cabdea60e54efddf64e636903a99cb2e0a7143c91647ff34e05a5c0d9d705ff8a5da
6
+ metadata.gz: d658bc4bfad3c59f0fb602afefd148e1ba7edc05a95925e4f7da564780f434178b16a910f747a58a21d9fe3644089ebd77e38a9b5751ec77804a6e91fb427ad5
7
+ data.tar.gz: fc0f5fb8a6c7ceed9874f78d2428f700afb508f1589d9ac1477c3e5768ce19885819719b74f7151309a671ec5eb51a0020a981f27da5fc977818bf4a5b067bb6
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.13.3.pre.pre1)
37
+ spree_cm_commissioner (2.13.3.pre.pre2)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -12,9 +12,7 @@ module Spree
12
12
  def enqueue_polling
13
13
  sync_type = params[:sync_type].presence || :full
14
14
 
15
- if !@integration.supports_polling?
16
- flash[:error] = Spree.t('admin.integration_polling_not_supported')
17
- elsif @integration.active?
15
+ if @integration.active?
18
16
  SpreeCmCommissioner::Integrations::PollingJob.perform_later(
19
17
  integration_id: @integration.id,
20
18
  sync_type: sync_type
@@ -22,22 +22,10 @@ module SpreeCmCommissioner
22
22
  .sort
23
23
  end
24
24
 
25
- # Integrations the partner calls into (rather than us pulling from them) have no sync_manager
26
- # and override this to false. Class-level so the pollable scope can filter on `type`.
27
- def self.supports_polling?
28
- true
29
- end
30
-
31
- scope :pollable, -> { where(type: types.select { |type| type.constantize.supports_polling? }) }
32
-
33
25
  def sync_manager
34
26
  raise NotImplementedError, 'Subclasses must implement the sync_manager method'
35
27
  end
36
28
 
37
- def supports_polling?
38
- self.class.supports_polling?
39
- end
40
-
41
29
  # Whether the partner, not BookMe+, sets this variant's per-night stock, prices and closings.
42
30
  # When true, stock resets and dashboard edits leave its inventory items alone, and new nights
43
31
  # start closed until the partner opens them (InventoryItem). Off by default.
@@ -1,6 +1,7 @@
1
1
  # A hotel connected to switch.cm, which calls our Booking.com-compatible channel-manager API
2
2
  # (Spree::Api::SwitchCm) instead of us calling it: it pushes availability, prices and
3
- # restrictions, and pulls reservations. So there's nothing to poll, hold or unstock externally.
3
+ # restrictions, and pulls reservations. So there's nothing to pull, hold or unstock externally;
4
+ # polling only runs our own checks (SyncManager).
4
5
  #
5
6
  # Every switch.cm row shares one Doorkeeper app; the hotels switch.cm may manage are the rows
6
7
  # pointing at it. `paused` is the setup state: switch.cm can read the hotel (room list,
@@ -18,11 +19,16 @@ class SpreeCmCommissioner::Integrations::SwitchCm < SpreeCmCommissioner::Integra
18
19
  before_create :set_connected_at
19
20
  after_commit :sync_unpushed_nights, on: %i[create update]
20
21
 
22
+ # Polling runs the hotel's periodic checks (SyncManager), and the incremental one alerts on
23
+ # every run while reservations sit unpulled, so the column's 10-second default would flood
24
+ # Telegram. Full runs daily upkeep.
25
+ attribute :incremental_sync_interval_seconds, :integer, default: -> { SyncStrategies::IncrementalSyncStrategy::UNSEEN_AFTER.to_i }
26
+
21
27
  scope :readable, -> { where(status: READABLE_STATUSES) }
22
28
 
23
29
  # override
24
- def self.supports_polling?
25
- false
30
+ def sync_manager
31
+ SyncManager.new(integration: self)
26
32
  end
27
33
 
28
34
  # Only for rooms switch.cm can see (RoomCatalog): a hotel's other products, and rooms that need
@@ -75,6 +81,6 @@ class SpreeCmCommissioner::Integrations::SwitchCm < SpreeCmCommissioner::Integra
75
81
  owned_before = status_before_last_save.to_s == 'active' && conflict_strategy_before_last_save.to_s == 'external_wins'
76
82
  return if owned_before == owns_inventory?
77
83
 
78
- owns_inventory? ? PushedNights.new(self).close_unpushed! : PushedNights.new(self).reopen_unpushed!
84
+ PushedNights.new(self).reconcile_unpushed!
79
85
  end
80
86
  end
@@ -4,7 +4,7 @@ module SpreeCmCommissioner
4
4
  BATCH_SIZE = 10 # Process integrations in batches to avoid overwhelming the server
5
5
 
6
6
  def call
7
- SpreeCmCommissioner::Integration.active.pollable.find_each(batch_size: BATCH_SIZE) do |integration|
7
+ SpreeCmCommissioner::Integration.active.find_each(batch_size: BATCH_SIZE) do |integration|
8
8
  if should_run_full_sync?(integration)
9
9
  schedule_full_sync(integration)
10
10
  elsif should_run_incremental_sync?(integration)
@@ -45,6 +45,16 @@ class SpreeCmCommissioner::Integrations::SwitchCm
45
45
  unpushed_nights.where(closed: true).update_all(closed_update(false)) # rubocop:disable Rails/SkipsModelValidations
46
46
  end
47
47
 
48
+ # Puts unpushed nights back in the state ownership calls for; returns how many changed.
49
+ def reconcile_unpushed!
50
+ @integration.owns_inventory? ? close_unpushed! : reopen_unpushed!
51
+ end
52
+
53
+ # Returns how many were deleted.
54
+ def prune_past!
55
+ mappings.where('date < ?', Time.zone.today).delete_all
56
+ end
57
+
48
58
  # Upcoming room-nights pushed, out of all we hold, for the admin integration page.
49
59
  def progress
50
60
  { pushed: mappings.where(internal_id: upcoming_nights.select(:id)).count, total: upcoming_nights.count }
@@ -0,0 +1,16 @@
1
+ class SpreeCmCommissioner::Integrations::SwitchCm
2
+ # switch.cm calls us, so polling has nothing to pull. It runs our own periodic checks for the
3
+ # hotel instead, logged as sync sessions like any other integration's: incremental alerts ops
4
+ # about reservations switch.cm hasn't picked up, full keeps the pushed-night bookkeeping tidy.
5
+ class SyncManager < SpreeCmCommissioner::Integrations::Base::SyncManager
6
+ # override
7
+ def sync_full!
8
+ run_execution(:full) { SyncStrategies::FullSyncStrategy.new(integration: @integration).call }
9
+ end
10
+
11
+ # override
12
+ def sync_incremental!
13
+ run_execution(:incremental) { SyncStrategies::IncrementalSyncStrategy.new(integration: @integration).call }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ class SpreeCmCommissioner::Integrations::SwitchCm
2
+ module SyncStrategies
3
+ # Daily upkeep of the pushed-night bookkeeping (PushedNights):
4
+ #
5
+ # - Reconciles unpushed nights with who owns the rooms: closed while switch.cm owns them, open
6
+ # otherwise. The connection's own save does this when ownership changes; this catches nights
7
+ # that slipped past it, e.g. written by a path that skips callbacks.
8
+ # - Deletes mappings for past nights, which nothing reads and would otherwise grow by rooms ×
9
+ # 365 rows a year per hotel.
10
+ #
11
+ # Reconciling takes the hotel's push lock, so it never races a push that is opening a night.
12
+ # A busy lock fails the session, and PollingScheduler retries a failed full sync on its next tick.
13
+ class FullSyncStrategy
14
+ LOCK_WAIT = 5.seconds
15
+
16
+ class LockBusyError < StandardError; end
17
+
18
+ def initialize(integration:)
19
+ @integration = integration
20
+ @sync_result = SpreeCmCommissioner::Integrations::Base::SyncResult.new
21
+ end
22
+
23
+ def call
24
+ pushed_nights = PushedNights.new(@integration)
25
+
26
+ @sync_result.increment_metric(:pushed_nights, :pruned, pushed_nights.prune_past!)
27
+
28
+ reconciled = AccommodationLock.with_lock(@integration, wait: LOCK_WAIT) { pushed_nights.reconcile_unpushed! }
29
+ raise LockBusyError, 'Another push for this hotel held the lock' if reconciled == AccommodationLock::BUSY
30
+
31
+ @sync_result.increment_metric(:unpushed_nights, @integration.owns_inventory? ? :closed : :reopened, reconciled)
32
+ @sync_result
33
+ rescue StandardError => e
34
+ @sync_result.record_error(e)
35
+ @sync_result
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ class SpreeCmCommissioner::Integrations::SwitchCm
2
+ module SyncStrategies
3
+ # switch.cm pulls reservations, so a connector that stops polling fails silently: the hotel
4
+ # never hears about new bookings or cancellations. This alerts ops about the hotel's
5
+ # reservations still unseen after UNSEEN_AFTER, the same point at which Booking.com falls back
6
+ # to other delivery. It alerts on every run until they're picked up, so the hotel's
7
+ # incremental interval (SwitchCm defaults it to UNSEEN_AFTER) is also the alert's repeat rate.
8
+ class IncrementalSyncStrategy
9
+ UNSEEN_AFTER = 30.minutes
10
+ LISTED = 10
11
+
12
+ def initialize(integration:)
13
+ @integration = integration
14
+ @sync_result = SpreeCmCommissioner::Integrations::Base::SyncResult.new
15
+ end
16
+
17
+ def call
18
+ stale = stale_reservations
19
+ stale.group_by(&:status).each { |status, reservations| @sync_result.increment_metric(:unseen_reservations, status, reservations.size) }
20
+ alert(stale) if stale.any?
21
+
22
+ @sync_result
23
+ rescue StandardError => e
24
+ @sync_result.record_error(e)
25
+ @sync_result
26
+ end
27
+
28
+ private
29
+
30
+ def stale_reservations
31
+ feed = ReservationFeed.new([@integration])
32
+ limit = ReservationFeed::LIMIT_RANGE.max
33
+ cutoff = UNSEEN_AFTER.ago
34
+
35
+ (feed.new_reservations(limit: limit) + feed.cancelled_reservations(limit: limit)).select { |reservation| reservation.changed_at <= cutoff }
36
+ end
37
+
38
+ def alert(reservations)
39
+ data = {
40
+ integration_id: @integration.id,
41
+ hotel: "#{@integration.vendor.name} (#{@integration.vendor_id})",
42
+ reservations: reservations.first(LISTED).map { |reservation| "#{reservation.number} #{reservation.status}" }.join(', ')
43
+ }
44
+ message = "switch.cm hasn't picked up #{reservations.size} reservation(s) for #{UNSEEN_AFTER.inspect} or more"
45
+
46
+ CmAppLogger.error(label: "#{self.class.name}#alert", data: data.merge(message: message))
47
+ SpreeCmCommissioner::TelegramAlerts::IntegrationSyncFailure.call(error_message: message, data: data)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -28,14 +28,12 @@
28
28
  <%= Spree.t(:integration_sync_sessions) %>
29
29
  </h5>
30
30
 
31
- <% if @integration.supports_polling? %>
32
- <div class="card-header-actions">
33
- <%= button_to Spree.t(:enqueue_full_sync),
34
- enqueue_polling_admin_integration_path(@integration, sync_type: :full),
35
- method: :post,
36
- class: 'btn btn-primary btn-sm' %>
37
- </div>
38
- <% end %>
31
+ <div class="card-header-actions">
32
+ <%= button_to Spree.t(:enqueue_full_sync),
33
+ enqueue_polling_admin_integration_path(@integration, sync_type: :full),
34
+ method: :post,
35
+ class: 'btn btn-primary btn-sm' %>
36
+ </div>
39
37
  </div>
40
38
 
41
39
  <div class="card-body">
@@ -672,7 +672,6 @@ en:
672
672
  import_already_exists: "Import Already Exists"
673
673
  integration_sync_enqueued: "Integration sync has been enqueued"
674
674
  integration_inactive: "Integration is inactive"
675
- integration_polling_not_supported: "This integration calls BookMe+ itself, so there is nothing to sync"
676
675
  inventory_external_wins: "Managed by %{name}: its stock and prices are set by the channel manager"
677
676
  external_integrations:
678
677
  view_result: "View Result"
@@ -445,7 +445,6 @@ km:
445
445
  import_already_exists: "ឯកសារយោងមានរួចហើយ"
446
446
  integration_sync_enqueued: "ការធ្វើសមកាលកម្មបានដាក់ក្នុងជួរ"
447
447
  integration_inactive: "ការភ្ជាប់មិនដំណើរការ"
448
- integration_polling_not_supported: "ការភ្ជាប់នេះហៅមក BookMe+ ដោយខ្លួនឯង ដូច្នេះមិនមានអ្វីត្រូវធ្វើសមកាលកម្មទេ"
449
448
  inventory_external_wins: "គ្រប់គ្រងដោយ %{name}៖ ស្តុក និងតម្លៃ ត្រូវបានកំណត់ដោយ channel manager"
450
449
  external_integrations:
451
450
  view_result: "មើលលទ្ធផល"
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.13.3-pre1'.freeze
2
+ VERSION = '2.13.3-pre2'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.13.3.pre.pre1
4
+ version: 2.13.3.pre.pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -1426,7 +1426,6 @@ files:
1426
1426
  - app/jobs/spree_cm_commissioner/integrations/cleanup_sync_sessions_job.rb
1427
1427
  - app/jobs/spree_cm_commissioner/integrations/polling_job.rb
1428
1428
  - app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb
1429
- - app/jobs/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert_job.rb
1430
1429
  - app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb
1431
1430
  - app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_job.rb
1432
1431
  - app/jobs/spree_cm_commissioner/inventory_holds/bulk_release_stale_payment_locked_job.rb
@@ -2434,7 +2433,9 @@ files:
2434
2433
  - app/services/spree_cm_commissioner/integrations/switch_cm/reservation.rb
2435
2434
  - app/services/spree_cm_commissioner/integrations/switch_cm/reservation_feed.rb
2436
2435
  - app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb
2437
- - app/services/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert.rb
2436
+ - app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb
2437
+ - app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb
2438
+ - app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb
2438
2439
  - app/services/spree_cm_commissioner/integrations/switch_cm/unseen_room_nights.rb
2439
2440
  - app/services/spree_cm_commissioner/integrations/switch_cm/validate_ari_updates.rb
2440
2441
  - app/services/spree_cm_commissioner/integrations/switch_cm/xml/availability_parser.rb
@@ -1,7 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::SwitchCm
2
- class UnseenReservationsAlertJob < SpreeCmCommissioner::ApplicationUniqueJob
3
- def perform(_options = {})
4
- SpreeCmCommissioner::Integrations::SwitchCm::UnseenReservationsAlert.call
5
- end
6
- end
7
- end
@@ -1,45 +0,0 @@
1
- class SpreeCmCommissioner::Integrations::SwitchCm
2
- # switch.cm pulls reservations, so a connector that stops polling fails silently: the hotel
3
- # never hears about new bookings or cancellations. This alerts ops about reservations still
4
- # unseen after UNSEEN_AFTER, the same point at which Booking.com falls back to other delivery.
5
- class UnseenReservationsAlert
6
- prepend ::Spree::ServiceModule::Base
7
-
8
- UNSEEN_AFTER = 30.minutes
9
- LISTED_PER_HOTEL = 10
10
-
11
- def call
12
- stale = SpreeCmCommissioner::Integrations::SwitchCm.active.includes(:vendor).filter_map do |integration|
13
- reservations = stale_reservations(integration)
14
- next if reservations.empty?
15
-
16
- { integration: integration, reservations: reservations }
17
- end
18
-
19
- stale.each { |entry| alert(entry[:integration], entry[:reservations]) }
20
- success(alerted_hotels: stale.size)
21
- end
22
-
23
- private
24
-
25
- def stale_reservations(integration)
26
- feed = ReservationFeed.new([integration])
27
- cutoff = UNSEEN_AFTER.ago
28
-
29
- (feed.new_reservations(limit: ReservationFeed::LIMIT_RANGE.max) + feed.cancelled_reservations(limit: ReservationFeed::LIMIT_RANGE.max))
30
- .select { |reservation| reservation.changed_at <= cutoff }
31
- end
32
-
33
- def alert(integration, reservations)
34
- data = {
35
- integration_id: integration.id,
36
- hotel: "#{integration.vendor.name} (#{integration.vendor_id})",
37
- reservations: reservations.first(LISTED_PER_HOTEL).map { |reservation| "#{reservation.number} #{reservation.status}" }.join(', ')
38
- }
39
- message = "switch.cm hasn't picked up #{reservations.size} reservation(s) for #{UNSEEN_AFTER.inspect} or more"
40
-
41
- CmAppLogger.error(label: "#{self.class.name}#alert", data: data.merge(message: message))
42
- SpreeCmCommissioner::TelegramAlerts::IntegrationSyncFailure.call(error_message: message, data: data)
43
- end
44
- end
45
- end