spree_cm_commissioner 2.14.6 → 2.14.7

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.
Files changed (20) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/models/spree_cm_commissioner/integration.rb +1 -1
  4. data/app/models/spree_cm_commissioner/integration_mapping.rb +27 -0
  5. data/app/models/spree_cm_commissioner/integrations/book_me_bus_v1.rb +1 -0
  6. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_inventory.rb +33 -11
  7. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_product.rb +3 -2
  8. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trip_stop.rb +7 -3
  9. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +44 -32
  10. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_variant.rb +1 -1
  11. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_vehicle_types.rb +1 -1
  12. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/resources/trip.rb +12 -1
  13. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/full_sync_strategy.rb +2 -2
  14. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/sync_strategies/incremental_sync_strategy.rb +1 -1
  15. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_product.rb +2 -1
  16. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_product.rb +2 -1
  17. data/app/views/spree/admin/integrations/_book_me_bus_v1_fields.html.erb +10 -0
  18. data/app/views/spree/admin/integrations/_form.html.erb +1 -1
  19. data/lib/spree_cm_commissioner/version.rb +1 -1
  20. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 743a7213a36a799ebd7d68c8e2a770a9a253b9ca9e544c53dc89114344c35eec
4
- data.tar.gz: c9697afd92b4175c73c1291224522b4b56fdf4056da7c8b4f441b33f0c828af4
3
+ metadata.gz: 2828d409efc450a1771a72ddee9866379996fbe09b29ee4eec5b446ed4d6acfc
4
+ data.tar.gz: 0cf499cd5d3bb7947385761cfff6c569a9180f96fe60426fa3ba802dece704a9
5
5
  SHA512:
6
- metadata.gz: 0bc416a5439a60e3daaa23cb031fee13bd2a5ab82795e05aa52005713bd27adf298f0b09ae793d7109f2b3b55bd8fd0b937d50323b3f53031c64217ad1396bff
7
- data.tar.gz: '073149c66ec7fc0b4b32d0c9b76ee35c934a96e489ae68ca6116adc4b60f831dc4271dd696ce996dbe6f80cd1db2658d0ffdeabfc25630e238d9c7cff5301bbd'
6
+ metadata.gz: c2531492454f38d71fbcb6115b97d442b29631b49b92cc5c8961cc395b761014bf24c53a99301150a5480ac42484838319dbac8c7d8f3d924708a6585bc7f9b1
7
+ data.tar.gz: 7e4bf0769cfdc8364a492fdb6fabbce2d72f1da7df9289dbbf68562312d3604880918f5eab80aa4b3e5f4098366fa8bc7c2ae38907bd8fc2a9b5fae60e92bcc0
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.14.6)
37
+ spree_cm_commissioner (2.14.7)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -11,7 +11,7 @@ module SpreeCmCommissioner
11
11
  has_many :integration_mappings, class_name: 'SpreeCmCommissioner::IntegrationMapping', dependent: :destroy, inverse_of: :integration
12
12
  has_many :integration_sync_sessions, class_name: 'SpreeCmCommissioner::IntegrationSyncSession', dependent: :destroy, inverse_of: :integration
13
13
 
14
- validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 3600 }
14
+ validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0 }
15
15
  validates :full_sync_interval_hours, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 168 }
16
16
 
17
17
  # Listed from the folder rather than `descendants`, which misses subclasses Zeitwerk hasn't
@@ -2,6 +2,8 @@ module SpreeCmCommissioner
2
2
  class IntegrationMapping < Base
3
3
  include StoreMetadata
4
4
 
5
+ CONFIRM_INTERVAL = 1.hour
6
+
5
7
  enum status: { active: 0, archived: 1 }
6
8
 
7
9
  # polymorphic (Spree::Taxon, Spree::Product, Spree::Variant, Spree::Vendor)
@@ -30,8 +32,33 @@ module SpreeCmCommissioner
30
32
  save!
31
33
  end
32
34
 
35
+ # For polling, which confirms the same mappings every few seconds. last_synced_at is indexed, so each save rewrites
36
+ # every index on this table; this skips the save while nothing a reader uses has changed. `ignoring` lists payload
37
+ # keys that move on every poll (seats, for one), at any depth.
38
+ def confirm_active!(external_payload:, ignoring: [])
39
+ return mark_as_active!(external_payload: external_payload) unless recently_confirmed?(external_payload, ignoring)
40
+
41
+ # as_json gives the string keys a save would: SyncTripStop reads this payload straight after.
42
+ self.external_payload = external_payload.as_json
43
+ end
44
+
33
45
  private
34
46
 
47
+ def recently_confirmed?(new_payload, ignoring)
48
+ return false unless persisted? && active? && last_synced_at.present?
49
+ return false if last_synced_at <= CONFIRM_INTERVAL.ago
50
+
51
+ stable_payload(new_payload.as_json, ignoring) == stable_payload(external_payload.as_json, ignoring)
52
+ end
53
+
54
+ def stable_payload(value, ignoring)
55
+ case value
56
+ when Hash then value.except(*ignoring).transform_values { |nested| stable_payload(nested, ignoring) }
57
+ when Array then value.map { |nested| stable_payload(nested, ignoring) }
58
+ else value
59
+ end
60
+ end
61
+
35
62
  def validate_internal_exists
36
63
  return if internal_type.safe_constantize&.exists?(id: internal_id)
37
64
 
@@ -6,6 +6,7 @@ class SpreeCmCommissioner::Integrations::BookMeBusV1 < SpreeCmCommissioner::Inte
6
6
  store_private_metadata :access_token, :string
7
7
  store_private_metadata :token_expires_at, :datetime
8
8
  store_private_metadata :operator_id, :string
9
+ store_private_metadata :sync_days, :integer, default: 7
9
10
  # override
10
11
  def sync_manager
11
12
  SpreeCmCommissioner::Integrations::BookMeBusV1::SyncManager.new(integration: self)
@@ -41,31 +41,53 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
41
41
  available_seats = external_trip.available_seats
42
42
  return if available_seats.nil? # unknown, not zero - don't zero out real inventory
43
43
 
44
- current_stock = stock_item.count_on_hand.to_i
45
- difference = available_seats.to_i - current_stock
44
+ sync_stock_item(stock_item, vehicle_capacity(external_trip, available_seats.to_i))
45
+ sync_inventory_item_for_date(stock_item.variant, external_trip, available_seats.to_i)
46
+ end
47
+
48
+ # count_on_hand is one number shared by every date and seeds new inventory items, so it holds the vehicle's seats.
49
+ # A date's own seats would rewrite it (and enqueue VendorJob) on most polls, and could seed new dates with 0.
50
+ def vehicle_capacity(external_trip, available_seats)
51
+ number_of_seats = external_trip.number_of_seats.to_i
52
+
53
+ number_of_seats.positive? ? number_of_seats : available_seats
54
+ end
55
+
56
+ def sync_stock_item(stock_item, seats)
57
+ difference = seats - stock_item.count_on_hand.to_i
46
58
  return if difference.zero?
47
59
 
48
60
  @sync_result.track(:stock_item, stock_item) do
49
61
  stock_item.adjust_count_on_hand(difference)
50
62
  @sync_result.increment_metric(:inventory, :adjusted, difference.abs)
51
63
  end
52
-
53
- adjust_inventory_item_for_date(stock_item.variant, external_trip, difference)
54
64
  end
55
65
 
56
66
  # StockItem.count_on_hand isn't date-aware and isn't read for real availability checks
57
- # (see docs/concepts/inventory-system.md) - apply the same delta to the specific date's
58
- # InventoryItem so ongoing BookMeBus availability changes actually reach bookable inventory.
59
- # If it doesn't exist yet, PermanentInventoryItemsGeneratorJob will seed it later from
60
- # count_on_hand, so there's nothing to adjust yet.
61
- def adjust_inventory_item_for_date(variant, external_trip, difference)
67
+ # (see docs/concepts/inventory-system.md), so each date's InventoryItem is set to that date's own seats_remaining,
68
+ # never moved by its difference from another date's seats: an item seeded from a different count (0 after a sold-out
69
+ # first date) would go negative and fail the whole trip's sync. If the item doesn't exist yet, the permanent items
70
+ # job seeds it later from count_on_hand.
71
+ def sync_inventory_item_for_date(variant, external_trip, available_seats)
62
72
  trip_date = parse_trip_date(external_trip.on_date)
63
73
  return unless trip_date
64
74
 
65
75
  inventory_item = variant.inventory_items.find_by(inventory_date: trip_date)
66
- return unless inventory_item
76
+ return if inventory_item.nil? || inventory_item.quantity_available == seats_for_sale(inventory_item, available_seats)
77
+
78
+ inventory_item.with_lock do
79
+ SpreeCmCommissioner::InventoryItems::AdjustAvailableQuantity.call!(
80
+ inventory_item: inventory_item,
81
+ delta: seats_for_sale(inventory_item, available_seats) - inventory_item.quantity_available,
82
+ caller_source: self.class.name,
83
+ lock: false
84
+ )
85
+ end
86
+ end
67
87
 
68
- inventory_item.adjust_quantity!(difference)
88
+ # BookMeBus doesn't know about seats an operator withheld here, so its count still includes them.
89
+ def seats_for_sale(inventory_item, available_seats)
90
+ [available_seats - inventory_item.quantity_locked - inventory_item.locked_count, 0].max
69
91
  end
70
92
 
71
93
  def parse_trip_date(date_string)
@@ -33,7 +33,8 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
33
33
  status: :active,
34
34
  vendor: @vendor,
35
35
  product_type: :transit,
36
- available_on: Time.current,
36
+ # Keeps the first value: a fresh time counts as a change, so every poll would save the product and purge its API cache.
37
+ available_on: product.available_on || Time.current,
37
38
  shipping_category: default_shipping_category,
38
39
  stores: [Spree::Store.default],
39
40
  option_types: [seat_type_option].compact,
@@ -42,7 +43,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
42
43
  tracker.save_if_changed!(product)
43
44
  end
44
45
 
45
- mapping.mark_as_active!(external_payload: { trip_id: external_trip.trip_id })
46
+ mapping.confirm_active!(external_payload: { trip_id: external_trip.trip_id })
46
47
  product
47
48
  end
48
49
 
@@ -135,11 +135,12 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
135
135
  tracker.save_if_changed!(trip_stop)
136
136
  end
137
137
 
138
- mapping.mark_as_active!(
138
+ mapping.confirm_active!(
139
139
  external_payload: stop_time_data.merge(
140
140
  'stop_name' => stop_data['name'],
141
141
  'location_name' => location_data['name']
142
- )
142
+ ),
143
+ ignoring: SpreeCmCommissioner::Integrations::BookMeBusV1::Resources::Trip::VOLATILE_PAYLOAD_KEYS
143
144
  )
144
145
 
145
146
  mapping
@@ -177,7 +178,10 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
177
178
  tracker.save_if_changed!(stop_vendor_place)
178
179
  end
179
180
 
180
- mapping.mark_as_active!(external_payload: external_stop.to_h.merge('place_type' => external_stop.vendor_place_type.to_s))
181
+ mapping.confirm_active!(
182
+ external_payload: external_stop.to_h.merge('place_type' => external_stop.vendor_place_type.to_s),
183
+ ignoring: SpreeCmCommissioner::Integrations::BookMeBusV1::Resources::Trip::VOLATILE_PAYLOAD_KEYS
184
+ )
181
185
 
182
186
  # Return the appropriate place based on stop type:
183
187
  # - BRANCHES: return location_vendor_place.place (the city/location)
@@ -279,6 +279,10 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
279
279
  class SyncTrips # rubocop:disable Metrics/ClassLength
280
280
  attr_reader :integration, :sync_result, :route, :vendor
281
281
 
282
+ # Booked seats are only fetched for the dates the incremental sync keeps fresh (today and tomorrow); further out,
283
+ # SyncItemAvailability fetches them when a customer opens the trip.
284
+ SEAT_STATUS_DAYS_AHEAD = 1
285
+
282
286
  def initialize(integration:, sync_result: nil, route: nil)
283
287
  @integration = integration
284
288
  @sync_result = sync_result || SpreeCmCommissioner::Integrations::Base::SyncResult.new
@@ -286,6 +290,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
286
290
  @vendor = integration.vendor
287
291
  @location_mappings = {}
288
292
  @location_id_cache = {}
293
+ @seat_layout_synced_variant_ids = Set.new
289
294
  @variant_ids_needing_items = Set.new
290
295
  end
291
296
 
@@ -293,7 +298,11 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
293
298
  # sync or [today, tomorrow] for incremental. Cleanup/route-stops/touch run once for the
294
299
  # whole window (see sync_items!) so a trip that doesn't run every day doesn't get archived
295
300
  # and reactivated on each date it's absent/present within one sync run.
296
- def call(on_dates:)
301
+ # @param full_sync [Boolean] false on incremental sync. It runs every few seconds over too few dates to tell: a trip
302
+ # missing from today and tomorrow may simply not run those days, so only a full sync archives trips, and a layout
303
+ # rarely changes, so only a full sync refreshes one already in place.
304
+ def call(on_dates:, full_sync: true)
305
+ @full_sync = full_sync
297
306
  preload_location_mappings!
298
307
  preload_location_id_cache!
299
308
  sync_items!(Array(on_dates))
@@ -368,7 +377,12 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
368
377
 
369
378
  # Nothing came back for any date in the window - don't touch route stops/cleanup based
370
379
  # on an empty result (could be a transient outage rather than a genuinely dead route).
371
- return if all_external_trips.empty?
380
+ if all_external_trips.empty?
381
+ # Incremental sync takes the route synced longest ago, so a route BookMeBus confirmed empty must still move to
382
+ # the back of the line, or every poll would pick it again and no other route would ever refresh.
383
+ touch_route_mapping if bookable_ids_by_date.any?
384
+ return
385
+ end
372
386
 
373
387
  # A trip that's still active overall (present on some dates in the window) but confirmed
374
388
  # absent/unbookable on ONE of them needs that specific date's inventory corrected - the
@@ -382,7 +396,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
382
396
 
383
397
  # Clean up stale trip mappings using the full window's synced ids - archiving per single
384
398
  # date would flap a non-daily trip's mapping active/archived across this run.
385
- cleanup_stale_mappings!(synced_trip_ids)
399
+ cleanup_stale_mappings!(synced_trip_ids) if @full_sync
386
400
 
387
401
  # Update route mapping timestamp
388
402
  touch_route_mapping
@@ -493,8 +507,6 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
493
507
  sync_result: sync_result,
494
508
  vendor: vendor
495
509
  ).call(variant: variant, external_trip: external_trip)
496
-
497
- restore_inventory_if_previously_zeroed!(variant, external_trip)
498
510
  end
499
511
 
500
512
  @variant_ids_needing_items << variant.id if permanent_items_needed?(service_calendar, variant, external_trip)
@@ -566,24 +578,46 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
566
578
  tracker.save_if_changed!(trip)
567
579
  end
568
580
 
569
- mapping.mark_as_active!(external_payload: external_trip.to_h)
581
+ mapping.confirm_active!(
582
+ external_payload: external_trip.to_h,
583
+ ignoring: SpreeCmCommissioner::Integrations::BookMeBusV1::Resources::Trip::VOLATILE_PAYLOAD_KEYS
584
+ )
570
585
  mapping
571
586
  end
572
587
 
588
+ # A trip's seat layout is the same on every date, so it is synced once per run (on the first bookable date, which
589
+ # also refreshes it every run). Later dates only matter for which seats are booked, so they are synced again only
590
+ # when BookMeBus reports that for the trip and the date is close enough to matter. The trip is marked done only
591
+ # after a successful sync, so a failed first fetch is retried on the next date.
573
592
  def sync_seat_layout!(vehicle_type, external_trip, variant)
574
- inventory_item = find_inventory_item_for_date(variant, external_trip)
575
-
576
593
  return unless vehicle_type
594
+ return if seat_layout_current?(variant) && !booked_seats_needed?(external_trip)
577
595
 
578
- SyncSeatLayout.new(
596
+ layout = SyncSeatLayout.new(
579
597
  integration: integration,
580
598
  sync_result: sync_result
581
599
  ).call(
582
600
  vehicle_type: vehicle_type,
583
601
  external_trip: external_trip,
584
602
  variant: variant,
585
- inventory_item: inventory_item
603
+ inventory_item: find_inventory_item_for_date(variant, external_trip)
586
604
  )
605
+
606
+ @seat_layout_synced_variant_ids << variant.id if layout
607
+ end
608
+
609
+ # Checks for assigned blocks, not just a layout: a trip copies its vehicle's layout on save, before any seat is
610
+ # assigned to it, and that first sync must still run.
611
+ def seat_layout_current?(variant)
612
+ return true if @seat_layout_synced_variant_ids.include?(variant.id)
613
+
614
+ !@full_sync && variant.trip&.seat_layout&.blocks&.exists?(variant_id: variant.id)
615
+ end
616
+
617
+ def booked_seats_needed?(external_trip)
618
+ trip_date = parse_trip_date(external_trip.on_date)
619
+
620
+ external_trip.per_seat_status? && trip_date.present? && trip_date <= Date.current + SEAT_STATUS_DAYS_AHEAD.days
587
621
  end
588
622
 
589
623
  def sync_trip_stop!(trip_mapping)
@@ -684,28 +718,6 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
684
718
  end
685
719
  end
686
720
 
687
- # Mirrors SyncItemAvailability#restore_inventory_if_previously_zeroed! - SyncInventory's own
688
- # delta is computed against StockItem#count_on_hand, which zero_out_inventory_for_date! never
689
- # touches (deliberately - it's a date-independent aggregate), so once available_seats matches
690
- # that unrelated baseline again the delta comes back 0 and a previously-zeroed InventoryItem
691
- # would otherwise stay stuck at 0 even after the trip is genuinely bookable again.
692
- def restore_inventory_if_previously_zeroed!(variant, external_trip)
693
- return unless variant&.persisted?
694
-
695
- available_seats = external_trip.available_seats
696
- return if available_seats.to_i.zero?
697
-
698
- inventory_item = find_inventory_item_for_date(variant, external_trip)
699
- return unless inventory_item
700
-
701
- inventory_item.with_lock do
702
- next unless inventory_item.quantity_available.to_i.zero?
703
-
704
- set_quantity_available_with_redis_sync!(inventory_item, available_seats.to_i)
705
- sync_result.increment_metric(:inventory, :restored_after_unavailable_date)
706
- end
707
- end
708
-
709
721
  # See SyncItemAvailability#set_quantity_available_with_redis_sync! for the full reasoning:
710
722
  # the real checkout-time availability check reads Redis, not this DB row directly, so
711
723
  # quantity_available must go through adjust_quantity! (which keeps both in sync) rather than
@@ -41,7 +41,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
41
41
  tracker.save_if_changed!(variant)
42
42
  end
43
43
 
44
- mapping.mark_as_active!(external_payload: { trip_id: external_trip.trip_id })
44
+ mapping.confirm_active!(external_payload: { trip_id: external_trip.trip_id })
45
45
  variant
46
46
  end
47
47
 
@@ -59,7 +59,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Polling
59
59
  tracker.save_if_changed!(vehicle_type)
60
60
  end
61
61
 
62
- mapping.mark_as_active!(external_payload: external_trip.vehicle_type)
62
+ mapping.confirm_active!(external_payload: external_trip.vehicle_type)
63
63
  vehicle_type
64
64
  end
65
65
 
@@ -1,5 +1,12 @@
1
1
  module SpreeCmCommissioner::Integrations::BookMeBusV1::Resources
2
- class Trip < Base
2
+ class Trip < Base # rubocop:disable Metrics/ClassLength
3
+ # Parts of a stored trip or stop-time payload that move between dates and polls. Nothing reads them back from the
4
+ # stored mapping, so a change in only these needs no write. See IntegrationMapping#confirm_active!.
5
+ VOLATILE_PAYLOAD_KEYS = %w[
6
+ on_date seats_remaining number_of_seats total_sold local_price non_local_price price service_available not_allowed departure_over
7
+ hits_origin hits_destination
8
+ ].freeze
9
+
3
10
  attr_accessor :id,
4
11
  :type,
5
12
  :trip_id,
@@ -84,6 +91,10 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::Resources
84
91
  seats_remaining
85
92
  end
86
93
 
94
+ # Whether BookMeBus reports which seats are booked on a date: false for auto-assigned-seat trips, and a private
95
+ # taxi is booked as a whole vehicle, so seat status does not apply there either.
96
+ def per_seat_status? = is_selectable == true && route_type != 'private_taxi'
97
+
87
98
  # BookMe+ charges the non-local (foreigner) price by default, falling back to local_price
88
99
  # only when BookMeBus doesn't provide one. Single source of truth for both trip-creation
89
100
  # (SyncTrips) and per-date availability refresh (SyncItemAvailability) so they can't diverge.
@@ -103,9 +103,9 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::SyncStrategies
103
103
  end
104
104
 
105
105
  def sync_trips_for_route!(route)
106
- # Sync trips for the next 7 days by default
106
+ # Sync trips for the configured number of days (default: 7)
107
107
  today = Date.current
108
- on_dates = (0...7).map { |days_ahead| (today + days_ahead).strftime('%Y-%m-%d') }
108
+ on_dates = (0...@integration.sync_days).map { |days_ahead| (today + days_ahead).strftime('%Y-%m-%d') }
109
109
 
110
110
  SpreeCmCommissioner::Integrations::BookMeBusV1::Polling::SyncTrips.new(
111
111
  integration: @integration,
@@ -57,7 +57,7 @@ module SpreeCmCommissioner::Integrations::BookMeBusV1::SyncStrategies
57
57
  integration: @integration,
58
58
  sync_result: @sync_result,
59
59
  route: vendor_route
60
- ).call(on_dates: on_dates)
60
+ ).call(on_dates: on_dates, full_sync: false)
61
61
  end
62
62
  end
63
63
  end
@@ -38,7 +38,8 @@ module SpreeCmCommissioner::Integrations::Larryta::Polling
38
38
  status: :active,
39
39
  vendor: @vendor,
40
40
  product_type: :transit,
41
- available_on: Time.current,
41
+ # Keeps the first value: a fresh time counts as a change, so every poll would save the product and purge its API cache.
42
+ available_on: product.available_on || Time.current,
42
43
  shipping_category: default_shipping_category,
43
44
  stores: [Spree::Store.default],
44
45
  option_types: [seat_type_option].compact,
@@ -35,7 +35,8 @@ module SpreeCmCommissioner::Integrations::VireakBuntham::Polling
35
35
  status: :active,
36
36
  vendor: @vendor,
37
37
  product_type: :transit,
38
- available_on: Time.current,
38
+ # Keeps the first value: a fresh time counts as a change, so every poll would save the product and purge its API cache.
39
+ available_on: product.available_on || Time.current,
39
40
  shipping_category: default_shipping_category,
40
41
  stores: [Spree::Store.default],
41
42
  option_types: [seat_type_option].compact,
@@ -28,6 +28,16 @@
28
28
  <% end %>
29
29
  </div>
30
30
 
31
+ <div class="col-6">
32
+ <%= f.field_container :sync_days do %>
33
+ <%= f.label :sync_days, raw(I18n.t('spree.admin.external_integrations.sync_days', default: 'Sync Days') + required_span_tag) %>
34
+ <%= number_field_tag 'spree_cm_commissioner_integration[private_metadata][sync_days]', f.object.sync_days || 7, class: 'form-control', min: 1, max: 30, required: true %>
35
+ <small class="form-text text-muted">
36
+ <%= I18n.t('spree.admin.external_integrations.sync_days_description', default: 'Number of days to sync trips for (default: 7)') %>
37
+ </small>
38
+ <% end %>
39
+ </div>
40
+
31
41
  <div class="col-12">
32
42
  <hr class="my-4">
33
43
 
@@ -85,7 +85,7 @@
85
85
  <div class="col-6">
86
86
  <%= f.field_container :incremental_sync_interval_seconds do %>
87
87
  <%= f.label :incremental_sync_interval_seconds, raw(I18n.t('spree.admin.external_integrations.incremental_sync_interval_seconds', default: 'Incremental sync interval (seconds)') + required_span_tag) %>
88
- <%= f.number_field :incremental_sync_interval_seconds, class: 'form-control', min: 1, max: 3600, required: true %>
88
+ <%= f.number_field :incremental_sync_interval_seconds, class: 'form-control', min: 1, required: true %>
89
89
  <%= f.error_message_on :incremental_sync_interval_seconds %>
90
90
  <% end %>
91
91
  </div>
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.14.6'.freeze
2
+ VERSION = '2.14.7'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.14.6
4
+ version: 2.14.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-11-02 00:00:00.000000000 Z
11
+ date: 2026-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree