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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/integrations_controller.rb +1 -3
- data/app/models/spree_cm_commissioner/integration.rb +0 -12
- data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +10 -4
- data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +1 -1
- data/app/services/spree_cm_commissioner/integrations/switch_cm/pushed_nights.rb +10 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb +16 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb +39 -0
- data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb +51 -0
- data/app/views/spree/admin/integration_sessions/index.html.erb +6 -8
- data/config/locales/en.yml +0 -1
- data/config/locales/km.yml +0 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +4 -3
- data/app/jobs/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert_job.rb +0 -7
- data/app/services/spree_cm_commissioner/integrations/switch_cm/unseen_reservations_alert.rb +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c04efc07b0defa6eedb7b800bf8c5504401b7de9042751e32a6eb40c1ec6897
|
|
4
|
+
data.tar.gz: 414a5fb2a306e1a1e948c9e08cbb14e412752b112c2509332ba6c15132870369
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d658bc4bfad3c59f0fb602afefd148e1ba7edc05a95925e4f7da564780f434178b16a910f747a58a21d9fe3644089ebd77e38a9b5751ec77804a6e91fb427ad5
|
|
7
|
+
data.tar.gz: fc0f5fb8a6c7ceed9874f78d2428f700afb508f1589d9ac1477c3e5768ce19885819719b74f7151309a671ec5eb51a0020a981f27da5fc977818bf4a5b067bb6
|
data/Gemfile.lock
CHANGED
|
@@ -12,9 +12,7 @@ module Spree
|
|
|
12
12
|
def enqueue_polling
|
|
13
13
|
sync_type = params[:sync_type].presence || :full
|
|
14
14
|
|
|
15
|
-
if
|
|
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
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb
ADDED
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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">
|
data/config/locales/en.yml
CHANGED
|
@@ -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"
|
data/config/locales/km.yml
CHANGED
|
@@ -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: "មើលលទ្ធផល"
|
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.
|
|
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/
|
|
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,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
|