spree_cm_commissioner 2.8.2 → 2.8.3.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +6 -3
  4. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +31 -17
  5. data/app/factory/spree_cm_commissioner/telegram_message_factory.rb +6 -0
  6. data/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb +5 -1
  7. data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +4 -1
  8. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +4 -2
  9. data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +4 -2
  10. data/app/jobs/spree_cm_commissioner/route_metrics/decrease_trip_count_job.rb +8 -2
  11. data/app/jobs/spree_cm_commissioner/route_metrics/increase_trip_count_job.rb +5 -2
  12. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  13. data/app/models/spree_cm_commissioner/trip.rb +10 -2
  14. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +22 -0
  15. data/app/serializers/spree_cm_commissioner/v2/storefront/amenity_serializer.rb +3 -0
  16. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_query_result_serializer.rb +1 -1
  17. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +1 -1
  18. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_vehicle_type_serializer.rb +6 -3
  19. data/app/serializers/spree_cm_commissioner/v2/storefront/trip_vendor_serializer.rb +6 -2
  20. data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +3 -0
  21. data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +7 -2
  22. data/app/services/spree_cm_commissioner/inventory_holds/release.rb +7 -2
  23. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +36 -4
  24. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +39 -4
  25. data/app/services/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items.rb +4 -6
  26. data/app/services/spree_cm_commissioner/oauth_access_tokens/cleanup_expired.rb +13 -7
  27. data/app/services/spree_cm_commissioner/redis_stock/base.rb +2 -1
  28. data/app/services/spree_cm_commissioner/route_metrics/decrease_trip_count.rb +3 -15
  29. data/app/services/spree_cm_commissioner/route_metrics/increase_trip_count.rb +7 -15
  30. data/app/services/spree_cm_commissioner/transit_order/create.rb +8 -2
  31. data/app/views/spree/admin/line_items/_form.html.erb +2 -3
  32. data/lib/spree_cm_commissioner/trip_query_result.rb +25 -1
  33. data/lib/spree_cm_commissioner/version.rb +1 -1
  34. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc101fbc1fb46a184c311e3cdf1827cd6515ae3b0399d77ca67c48560b3417bc
4
- data.tar.gz: 5437f4e82b8c48ce55753432d2ce529935b0c63d472946f03153e817ba3c3038
3
+ metadata.gz: 6113b7370f204cdd539916ac0e1881763beec50c577ba5973160f10941015f99
4
+ data.tar.gz: 356ebf217c53946872067544869cec5695e063df4092ae8b05d3c5e6a5dbef4e
5
5
  SHA512:
6
- metadata.gz: 74c7a8c0fbb21ae704c905b4f14b8d878b701adf82fc569a0db9d3bb780cae702eb93303c29db6bd08f602e9270fb89caed7bf5ebdb3a21d474057cef17d2157
7
- data.tar.gz: 981dc574b4b68c1fec6ed7a665ec47ef52bfb60ef89476ab119e72da1311c9e2ae01e78a69436a8663ba185b460df46c6fd5836ccc52531c5e4a13eeaa5c3209
6
+ metadata.gz: d99e1e40d83080a9487bc6929fedb7f3f0e2e847635fde43f5b96479e0f19386e17357e0bb798dae74f3302429e166d851a0247ab5b0c1a49bb62e38fc83a29f
7
+ data.tar.gz: 1560f7edd9efb1eb30351da6a44081f4eee124be109351d835631b0b1f82db176de3a8d4cdfa1112d1f2c96be55f7268dfa6a2c5ec617864146ecea5cd5d1c8b
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.8.2)
37
+ spree_cm_commissioner (2.8.3.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -64,8 +64,7 @@ module Spree
64
64
  'trips.vendor',
65
65
  'trips.vendor.logo',
66
66
  'trips.vehicle_type',
67
- 'trips.vehicle_type.vehicle_photos',
68
- 'trips.amenities'
67
+ 'trips.vehicle_type.vehicle_photos'
69
68
  ]
70
69
  end
71
70
 
@@ -99,7 +98,11 @@ module Spree
99
98
 
100
99
  # override
101
100
  def serializer_params
102
- params.permit(:include).to_hash
101
+ super.merge(
102
+ exclude_vendor_kind_option_types: true,
103
+ exclude_vendor_kind_option_values: true,
104
+ exclude_vehicle_type_amenities: true
105
+ )
103
106
  end
104
107
 
105
108
  # override
@@ -48,7 +48,7 @@ module SpreeCmCommissioner
48
48
  end
49
49
 
50
50
  def line_item_content(line_item)
51
- return intercity_taxi_line_item_content(line_item) if intercity_taxi?(line_item)
51
+ return transit_line_item_content(line_item) if line_item.transit?
52
52
 
53
53
  text = []
54
54
 
@@ -64,8 +64,8 @@ module SpreeCmCommissioner
64
64
  def pretty_date_for(line_item)
65
65
  return nil unless line_item.date_present?
66
66
 
67
- from_date = pretty_date(line_item.from_date)
68
- to_date = pretty_date(line_item.to_date)
67
+ from_date = pretty_datetime(line_item.from_date)
68
+ to_date = pretty_datetime(line_item.to_date)
69
69
 
70
70
  if from_date == to_date
71
71
  "🗓️ #{from_date}"
@@ -84,7 +84,7 @@ module SpreeCmCommissioner
84
84
  text << "Email: #{inline_code(order.email)}" if order.email.present?
85
85
  text << "Delivery Address: #{formatted_shipping_address.presence || 'N/A'}" if order.delivery_required?
86
86
 
87
- append_intercity_taxi_footer(text) if intercity_taxi_order?
87
+ append_transit_footer(text) if transit_order?
88
88
 
89
89
  if show_details_link && order.guests.any?
90
90
  text << ''
@@ -132,12 +132,12 @@ module SpreeCmCommissioner
132
132
 
133
133
  private
134
134
 
135
- def intercity_taxi?(line_item)
136
- trip_for(line_item)&.intercity_taxi?
135
+ def transit_order?
136
+ selected_line_items.any?(&:transit?)
137
137
  end
138
138
 
139
139
  def intercity_taxi_order?
140
- selected_line_items.any? { |li| intercity_taxi?(li) }
140
+ selected_line_items.any? { |li| trip_for(li)&.intercity_taxi? }
141
141
  end
142
142
 
143
143
  def trip_for(line_item)
@@ -147,10 +147,10 @@ module SpreeCmCommissioner
147
147
  @trip_cache[line_item.trip_id] ||= SpreeCmCommissioner::Trip.find_by(id: line_item.trip_id)
148
148
  end
149
149
 
150
- def intercity_taxi_line_item_content(line_item)
150
+ def transit_line_item_content(line_item)
151
151
  text = []
152
- route_text = taxi_route_text(line_item)
153
- departure_text = taxi_departure_text(line_item)
152
+ route_text = transit_route_text(line_item)
153
+ departure_text = transit_departure_text(line_item)
154
154
  text << bold('🚗 Trip Details')
155
155
  text << "Route: #{route_text}" if route_text.present?
156
156
  text << "Passengers: #{line_item.passenger_count || line_item.quantity}"
@@ -159,7 +159,7 @@ module SpreeCmCommissioner
159
159
  text.compact.join("\n")
160
160
  end
161
161
 
162
- def taxi_route_text(line_item)
162
+ def transit_route_text(line_item)
163
163
  trip = trip_for(line_item)
164
164
  return line_item.product.name if trip.blank?
165
165
 
@@ -171,21 +171,35 @@ module SpreeCmCommissioner
171
171
  vehicle.present? ? "#{parts} (#{vehicle})" : parts
172
172
  end
173
173
 
174
- def taxi_departure_text(line_item)
174
+ def transit_departure_text(line_item)
175
175
  return nil unless line_item.date_present?
176
176
 
177
177
  line_item.from_date.strftime('%b %d, %Y · %H:%M')
178
178
  end
179
179
 
180
180
  def nationality_text
181
- values = order.saved_guests.map { |sg| sg.nationality&.name.presence || sg.nationality_group&.humanize }.compact.uniq
182
- return nil if values.empty?
181
+ grouped = order.saved_guests.group_by { |sg| sg.nationality_group&.humanize }
182
+ grouped.delete(nil)
183
+ return nil if grouped.empty?
183
184
 
184
- values.join(', ')
185
+ grouped.map { |nationality_group, guests| customer_type_summary(nationality_group, guests) }.join(', ')
185
186
  end
186
187
 
187
- def append_intercity_taxi_footer(text)
188
- text << "Nationality: #{nationality_text}" if nationality_text.present?
188
+ # e.g. "Local (Adult x1, Child x1)". age_group is NOT NULL, so it is always present.
189
+ def customer_type_summary(nationality_group, guests)
190
+ age_breakdown = guests
191
+ .group_by { |sg| sg.age_group.humanize }
192
+ .map { |age_group, age_guests| "#{age_group} x#{age_guests.count}" }
193
+ .join(', ')
194
+
195
+ "#{nationality_group} (#{age_breakdown})"
196
+ end
197
+
198
+ def append_transit_footer(text)
199
+ nationality = nationality_text
200
+ text << "Nationality: #{nationality}" if nationality.present?
201
+
202
+ return unless intercity_taxi_order?
189
203
 
190
204
  pickup = selected_line_items.filter_map(&:pickup_map_place).first
191
205
  dropoff = selected_line_items.filter_map(&:dropoff_map_place).first
@@ -46,5 +46,11 @@ module SpreeCmCommissioner
46
46
 
47
47
  date.to_date.strftime('%b %d, %Y')
48
48
  end
49
+
50
+ def pretty_datetime(datetime)
51
+ return '' if datetime.blank?
52
+
53
+ datetime&.in_time_zone&.strftime('%b %d, %Y - %H:%M')
54
+ end
49
55
  end
50
56
  end
@@ -19,13 +19,17 @@ module SpreeCmCommissioner
19
19
  context.response = client.create_invalidation(
20
20
  distribution_id: ENV.fetch('ASSETS_SYNC_CF_DIST_ID'),
21
21
  invalidation_batch: {
22
- caller_reference: Time.now.to_i.to_s,
22
+ # Must be globally unique per distribution,
23
+ # Time.now.to_i collides when multiple jobs fire in the same second.
24
+ caller_reference: SecureRandom.uuid,
23
25
  paths: {
24
26
  quantity: patterns.size,
25
27
  items: patterns
26
28
  }
27
29
  }
28
30
  )
31
+ rescue Aws::CloudFront::Errors::TooManyInvalidationsInProgress => e
32
+ context.fail!(message: "Too many invalidations in progress: #{e}")
29
33
  end
30
34
  end
31
35
  end
@@ -1,7 +1,10 @@
1
1
  module SpreeCmCommissioner
2
2
  class InvalidateCacheRequestJob < ApplicationUniqueJob
3
+ queue_as :cache_invalidation
4
+
3
5
  def perform(options = {})
4
- SpreeCmCommissioner::InvalidateCacheRequest.call(patterns: options[:patterns])
6
+ result = SpreeCmCommissioner::InvalidateCacheRequest.call(patterns: options[:patterns])
7
+ raise result.error if result.failure?
5
8
  end
6
9
  end
7
10
  end
@@ -1,13 +1,15 @@
1
1
  module SpreeCmCommissioner
2
2
  module InventoryItems
3
3
  class BulkAdjustQuantitiesJob < ApplicationUniqueJob
4
- # :line_item_ids, :inventory_id_and_quantities
4
+ # :line_item_ids, :inventory_id_and_quantities, :caller_source
5
5
  #
6
6
  # :line_item_ids is included for unique job key generation to prevent duplicate jobs,
7
7
  # though it's not used in the perform method implementation.
8
+ # :caller_source is a string like "ClassName#method" identifying who enqueued the job.
8
9
  def perform(options = {})
9
10
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantities.call!(
10
- inventory_id_and_quantities: options[:inventory_id_and_quantities]
11
+ inventory_id_and_quantities: options[:inventory_id_and_quantities],
12
+ caller_source: options[:caller_source]
11
13
  )
12
14
  end
13
15
  end
@@ -3,16 +3,18 @@ module SpreeCmCommissioner
3
3
  class BulkAdjustQuantitiesOnHoldJob < ApplicationUniqueJob
4
4
  queue_as :default
5
5
 
6
- # :order_id, :inventory_id_and_quantities
6
+ # :order_id, :inventory_id_and_quantities, :caller_source
7
7
  #
8
8
  # :order_id is included for unique job key generation to prevent duplicate jobs,
9
9
  # though it's not used in the perform method implementation.
10
+ # :caller_source is a string like "ClassName#method" identifying who enqueued the job.
10
11
  def perform(options = {})
11
12
  raise ArgumentError, 'order_id is required' if options[:order_id].blank?
12
13
  raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
13
14
 
14
15
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHold.call(
15
- inventory_id_and_quantities: options[:inventory_id_and_quantities]
16
+ inventory_id_and_quantities: options[:inventory_id_and_quantities],
17
+ caller_source: options[:caller_source]
16
18
  )
17
19
  end
18
20
  end
@@ -2,8 +2,14 @@ module SpreeCmCommissioner
2
2
  module RouteMetrics
3
3
  class DecreaseTripCountJob < ApplicationUniqueJob
4
4
  def perform(options = {})
5
- trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
- SpreeCmCommissioner::RouteMetrics::DecreaseTripCount.call(trip: trip)
5
+ route_metric = SpreeCmCommissioner::RouteMetric.find_by(
6
+ origin_place_id: options[:origin_place_id],
7
+ destination_place_id: options[:destination_place_id],
8
+ route_type: options[:route_type]
9
+ )
10
+ return if route_metric.nil?
11
+
12
+ SpreeCmCommissioner::RouteMetrics::DecreaseTripCount.call(route_metric: route_metric)
7
13
  end
8
14
  end
9
15
  end
@@ -2,8 +2,11 @@ module SpreeCmCommissioner
2
2
  module RouteMetrics
3
3
  class IncreaseTripCountJob < ApplicationUniqueJob
4
4
  def perform(options = {})
5
- trip = SpreeCmCommissioner::Trip.find(options[:trip_id])
6
- SpreeCmCommissioner::RouteMetrics::IncreaseTripCount.call(trip: trip)
5
+ SpreeCmCommissioner::RouteMetrics::IncreaseTripCount.call(
6
+ origin_place_id: options[:origin_place_id],
7
+ destination_place_id: options[:destination_place_id],
8
+ route_type: options[:route_type]
9
+ )
7
10
  end
8
11
  end
9
12
  end
@@ -34,6 +34,7 @@ module SpreeCmCommissioner
34
34
  base.has_many :guests, through: :line_items, class_name: 'SpreeCmCommissioner::Guest'
35
35
 
36
36
  base.has_many :saved_guests,
37
+ -> { reorder(nil).distinct },
37
38
  through: :guests,
38
39
  source: :saved_guest,
39
40
  class_name: 'SpreeCmCommissioner::SavedGuest'
@@ -162,11 +162,19 @@ module SpreeCmCommissioner
162
162
  end
163
163
 
164
164
  def increment_route_metric_trip_count
165
- SpreeCmCommissioner::RouteMetrics::IncreaseTripCountJob.perform_later(trip_id: id)
165
+ SpreeCmCommissioner::RouteMetrics::IncreaseTripCountJob.perform_later(
166
+ origin_place_id: origin_place_id,
167
+ destination_place_id: destination_place_id,
168
+ route_type: route_type
169
+ )
166
170
  end
167
171
 
168
172
  def decrement_route_metric_trip_count
169
- SpreeCmCommissioner::RouteMetrics::DecreaseTripCountJob.perform_later(trip_id: id)
173
+ SpreeCmCommissioner::RouteMetrics::DecreaseTripCountJob.perform_later(
174
+ origin_place_id: origin_place_id,
175
+ destination_place_id: destination_place_id,
176
+ route_type: route_type
177
+ )
170
178
  end
171
179
 
172
180
  def update_route_price_range
@@ -56,6 +56,8 @@ module SpreeCmCommissioner
56
56
  return [] if trip_legs.empty?
57
57
 
58
58
  legs_by_parent = group_legs(trip_legs: trip_legs)
59
+ legs_by_parent = filter_complete_parents(legs_by_parent)
60
+ return [] if legs_by_parent.empty?
59
61
 
60
62
  leg_ids = legs_by_parent.values.flatten.uniq
61
63
  legs_by_id = preload_leg_trips(leg_ids: leg_ids)
@@ -159,6 +161,26 @@ module SpreeCmCommissioner
159
161
  .having('SUM(cm_inventory_items.quantity_available) >= ?', number_of_guests)
160
162
  end
161
163
 
164
+ # Discards parent trips whose found-leg count doesn't match their total leg count.
165
+ # This handles the case where one leg lacks sufficient inventory — the parent would
166
+ # otherwise be returned with only the legs that passed the HAVING filter, producing
167
+ # a partial (incorrect) multi-leg result.
168
+ #
169
+ # @return [Hash] { parent_trip_id => [leg_ids] } with incomplete parents removed
170
+ def filter_complete_parents(legs_by_parent)
171
+ parent_ids = legs_by_parent.keys
172
+ total_leg_counts = SpreeCmCommissioner::TripStop
173
+ .where(trip_id: parent_ids)
174
+ .where.not(board_to_trip_id: nil)
175
+ .group(:trip_id)
176
+ .count('DISTINCT board_to_trip_id')
177
+
178
+ legs_by_parent.select do |parent_id, leg_ids|
179
+ total = total_leg_counts[parent_id].to_i
180
+ total.positive? && leg_ids.size == total
181
+ end
182
+ end
183
+
162
184
  # @return [Hash] { parent_trip_id => [leg_ids] }
163
185
  def group_legs(trip_legs:)
164
186
  trip_legs.group_by(&:parent_trip_id).transform_values { |r| r.map(&:leg_id).uniq }
@@ -3,12 +3,15 @@ module SpreeCmCommissioner
3
3
  module Storefront
4
4
  class AmenitySerializer < BaseSerializer
5
5
  set_type :amenity
6
+
6
7
  attributes :name, :presentation
7
8
  attribute :display_icon do |option_value|
8
9
  ::ActionController::Base.helpers.image_url(option_value.display_icon)
9
10
  rescue StandardError
10
11
  nil
11
12
  end
13
+
14
+ has_one :option_type, serializer: ::Spree::V2::Storefront::OptionTypeSerializer
12
15
  end
13
16
  end
14
17
  end
@@ -4,7 +4,7 @@ module SpreeCmCommissioner
4
4
  class TripQueryResultSerializer < BaseSerializer
5
5
  set_type :trip_query_result
6
6
 
7
- attributes :quantity_available, :max_capacity
7
+ attributes :quantity_available, :max_capacity, :price, :compare_at_price, :currency, :display_price, :display_compare_at_price
8
8
 
9
9
  attribute :direct, &:direct?
10
10
  has_many :trips, serializer: ::SpreeCmCommissioner::V2::Storefront::TripResultSerializer
@@ -24,9 +24,9 @@ module SpreeCmCommissioner
24
24
 
25
25
  attribute :duration_in_hms, &:duration_in_hms
26
26
  attribute :arrival_time, &:arrival_time
27
+
27
28
  belongs_to :vendor, serializer: ::SpreeCmCommissioner::V2::Storefront::TripVendorSerializer
28
29
  belongs_to :vehicle_type, serializer: ::SpreeCmCommissioner::V2::Storefront::TripVehicleTypeSerializer
29
- has_many :amenities, serializer: ::SpreeCmCommissioner::V2::Storefront::AmenitySerializer
30
30
  end
31
31
  end
32
32
  end
@@ -5,9 +5,12 @@ module SpreeCmCommissioner
5
5
  attributes :id, :name, :kind, :number_of_seats
6
6
 
7
7
  has_many :vehicle_photos, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer
8
- has_many :amenities, serializer: ::SpreeCmCommissioner::V2::Storefront::AmenitySerializer
9
- has_many :option_types, serializer: ::Spree::V2::Storefront::OptionTypeSerializer,
10
- if: proc { |_record, params| params.nil? || params[:include_option_types] != false }
8
+
9
+ has_many :option_types, serializer: ::Spree::V2::Storefront::OptionTypeSerializer
10
+
11
+ # Included by default. Excluded ONLY when param is explicitly true or "true".
12
+ has_many :amenities, serializer: ::SpreeCmCommissioner::V2::Storefront::AmenitySerializer,
13
+ if: proc { |_, params| !params || params[:exclude_vehicle_type_amenities].to_s != 'true' }
11
14
  end
12
15
  end
13
16
  end
@@ -8,8 +8,12 @@ module SpreeCmCommissioner
8
8
 
9
9
  has_one :logo, serializer: ::SpreeCmCommissioner::V2::Storefront::AssetSerializer
10
10
 
11
- has_many :vendor_kind_option_types, serializer: ::Spree::V2::Storefront::OptionTypeSerializer
12
- has_many :vendor_kind_option_values, serializer: ::Spree::V2::Storefront::OptionValueSerializer
11
+ # Included by default. Excluded ONLY when param is explicitly true or "true".
12
+ has_many :vendor_kind_option_types, serializer: ::Spree::V2::Storefront::OptionTypeSerializer,
13
+ if: proc { |_, params| !params || params[:exclude_vendor_kind_option_types].to_s != 'true' }
14
+
15
+ has_many :vendor_kind_option_values, serializer: ::Spree::V2::Storefront::OptionValueSerializer,
16
+ if: proc { |_, params| !params || params[:exclude_vendor_kind_option_values].to_s != 'true' }
13
17
  end
14
18
  end
15
19
  end
@@ -144,8 +144,11 @@ module SpreeCmCommissioner
144
144
  end
145
145
  end
146
146
 
147
+ return if inventory_id_and_quantities.blank?
148
+
147
149
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
148
150
  order_id: order.id,
151
+ caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
149
152
  inventory_id_and_quantities: inventory_id_and_quantities
150
153
  )
151
154
  end
@@ -36,7 +36,8 @@ module SpreeCmCommissioner
36
36
  order.line_items.each do |li|
37
37
  next unless li.should_hold_inventory?
38
38
 
39
- li.inventory_items.active.each do |item|
39
+ # Skip .active scope: holds on past-dated items must also be cleared (e.g. delayed conversion after trip date).
40
+ li.inventory_items.each do |item|
40
41
  keys << item.redis_hold_key
41
42
  quantities << li.quantity
42
43
  inventory_items << item
@@ -68,7 +69,8 @@ module SpreeCmCommissioner
68
69
  inventory_id_and_quantities = order.line_items.flat_map do |line_item|
69
70
  next [] unless line_item.should_hold_inventory?
70
71
 
71
- line_item.inventory_items.active.map do |inventory_item|
72
+ # Skip .active scope for the same reason as clear_hold_redis!.
73
+ line_item.inventory_items.map do |inventory_item|
72
74
  {
73
75
  inventory_id: inventory_item.id,
74
76
  quantity: -line_item.quantity
@@ -76,8 +78,11 @@ module SpreeCmCommissioner
76
78
  end
77
79
  end
78
80
 
81
+ return if inventory_id_and_quantities.blank?
82
+
79
83
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
80
84
  order_id: order.id,
85
+ caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
81
86
  inventory_id_and_quantities: inventory_id_and_quantities
82
87
  )
83
88
  end
@@ -64,7 +64,8 @@ module SpreeCmCommissioner
64
64
  order.line_items.includes(:variant).find_each do |li|
65
65
  next unless li.should_hold_inventory?
66
66
 
67
- li.inventory_items.active.each do |item|
67
+ # Skip .active scope: holds on past-dated items must also be released (e.g. delayed job fires after trip date).
68
+ li.inventory_items.each do |item|
68
69
  keys << item.redis_hold_key
69
70
  quantities << li.quantity
70
71
  inventory_items << item
@@ -82,7 +83,8 @@ module SpreeCmCommissioner
82
83
  inventory_id_and_quantities = order.line_items.flat_map do |line_item|
83
84
  next [] unless line_item.should_hold_inventory?
84
85
 
85
- line_item.inventory_items.active.map do |inventory_item|
86
+ # Skip .active scope for the same reason as hold_keys_and_quantities.
87
+ line_item.inventory_items.map do |inventory_item|
86
88
  {
87
89
  inventory_id: inventory_item.id,
88
90
  quantity: -line_item.quantity
@@ -90,8 +92,11 @@ module SpreeCmCommissioner
90
92
  end
91
93
  end
92
94
 
95
+ return if inventory_id_and_quantities.blank?
96
+
93
97
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesOnHoldJob.perform_later(
94
98
  order_id: order.id,
99
+ caller_source: "#{self.class.name}#enqueue_sync_inventory_on_hold",
95
100
  inventory_id_and_quantities: inventory_id_and_quantities
96
101
  )
97
102
  end
@@ -4,12 +4,17 @@ module SpreeCmCommissioner
4
4
  prepend ::Spree::ServiceModule::Base
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
- def call(inventory_id_and_quantities:)
7
+ def call(inventory_id_and_quantities:, caller_source: nil)
8
+ CmAppLogger.log(
9
+ label: "#{self.class.name}#call started",
10
+ data: { caller_source: caller_source, inventory_id_and_quantities: inventory_id_and_quantities }
11
+ )
12
+
8
13
  ActiveRecord::Base.transaction do
9
14
  inventory_items(inventory_id_and_quantities).each do |inventory_item|
10
15
  quantity = inventory_id_and_quantities.select { |item| item[:inventory_id] == inventory_item.id }
11
16
  .sum { |item| item[:quantity] }
12
- adjust_quantity_available(inventory_item, quantity)
17
+ adjust_quantity_available(inventory_item, quantity, caller_source)
13
18
  end
14
19
  end
15
20
 
@@ -18,7 +23,7 @@ module SpreeCmCommissioner
18
23
 
19
24
  private
20
25
 
21
- def adjust_quantity_available(inventory_item, quantity)
26
+ def adjust_quantity_available(inventory_item, quantity, caller_source)
22
27
  # IMPORTANT: Apply the quantity change directly without defensive clamping.
23
28
  # The model validation will catch any attempts to go negative, surfacing bugs
24
29
  # in upstream Redis deduction logic.
@@ -28,8 +33,35 @@ module SpreeCmCommissioner
28
33
  # 2. lock_version (Optimistic): Catches race conditions if row is modified between
29
34
  # SELECT and UPDATE (e.g., admin adjusts stock while job runs)
30
35
  inventory_item.with_lock do
31
- inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
36
+ before = inventory_item.quantity_available
37
+ after = before + quantity
38
+
39
+ CmAppLogger.log(
40
+ label: "#{self.class.name}#adjust_quantity_available",
41
+ data: {
42
+ caller_source: caller_source,
43
+ inventory_item_id: inventory_item.id,
44
+ quantity_available_before: before,
45
+ delta: quantity,
46
+ quantity_available_after: after
47
+ }
48
+ )
49
+
50
+ inventory_item.update!(quantity_available: after)
32
51
  end
52
+ rescue ActiveRecord::RecordInvalid
53
+ CmAppLogger.error(
54
+ label: "#{self.class.name}#adjust_quantity_available failed",
55
+ data: {
56
+ caller_source: caller_source,
57
+ inventory_item_id: inventory_item.id,
58
+ quantity_available_current: inventory_item.quantity_available,
59
+ delta: quantity,
60
+ would_result_in: inventory_item.quantity_available + quantity,
61
+ errors: inventory_item.errors.full_messages
62
+ }
63
+ )
64
+ raise
33
65
  end
34
66
 
35
67
  def inventory_items(inventory_id_and_quantities)
@@ -4,11 +4,19 @@ module SpreeCmCommissioner
4
4
  prepend ::Spree::ServiceModule::Base
5
5
  extend SpreeCmCommissioner::ServiceModuleThrowable
6
6
 
7
- def call(inventory_id_and_quantities:)
7
+ def call(inventory_id_and_quantities:, caller_source: nil)
8
+ CmAppLogger.log(
9
+ label: "#{self.class.name}#call started",
10
+ data: {
11
+ caller_source: caller_source,
12
+ inventory_id_and_quantities: inventory_id_and_quantities
13
+ }
14
+ )
15
+
8
16
  ActiveRecord::Base.transaction do
9
17
  inventory_items(inventory_id_and_quantities).each do |inventory_item|
10
18
  quantity = inventory_id_and_quantities.find { |item| item[:inventory_id] == inventory_item.id }&.dig(:quantity) || 0
11
- adjust_quantity_on_hold(inventory_item, quantity)
19
+ adjust_quantity_on_hold(inventory_item, quantity, caller_source)
12
20
  end
13
21
  end
14
22
 
@@ -17,7 +25,7 @@ module SpreeCmCommissioner
17
25
 
18
26
  private
19
27
 
20
- def adjust_quantity_on_hold(inventory_item, quantity)
28
+ def adjust_quantity_on_hold(inventory_item, quantity, caller_source)
21
29
  # IMPORTANT: Apply the quantity change directly without defensive clamping.
22
30
  # The model validation will catch any attempts to go negative, surfacing bugs
23
31
  # in upstream Redis deduction logic.
@@ -27,8 +35,35 @@ module SpreeCmCommissioner
27
35
  # 2. lock_version (Optimistic): Catches race conditions if row is modified between
28
36
  # SELECT and UPDATE (e.g., admin adjusts stock while job runs)
29
37
  inventory_item.with_lock do
30
- inventory_item.update!(quantity_on_hold: inventory_item.quantity_on_hold + quantity)
38
+ before = inventory_item.quantity_on_hold
39
+ after = before + quantity
40
+
41
+ CmAppLogger.log(
42
+ label: "#{self.class.name}#adjust_quantity_on_hold",
43
+ data: {
44
+ caller_source: caller_source,
45
+ inventory_item_id: inventory_item.id,
46
+ quantity_on_hold_before: before,
47
+ delta: quantity,
48
+ quantity_on_hold_after: after
49
+ }
50
+ )
51
+
52
+ inventory_item.update!(quantity_on_hold: after)
31
53
  end
54
+ rescue ActiveRecord::RecordInvalid
55
+ CmAppLogger.error(
56
+ label: "#{self.class.name}#adjust_quantity_on_hold failed",
57
+ data: {
58
+ caller_source: caller_source,
59
+ inventory_item_id: inventory_item.id,
60
+ quantity_on_hold_current: inventory_item.quantity_on_hold,
61
+ delta: quantity,
62
+ would_result_in: inventory_item.quantity_on_hold + quantity,
63
+ errors: inventory_item.errors.full_messages
64
+ }
65
+ )
66
+ raise
32
67
  end
33
68
 
34
69
  def inventory_items(inventory_id_and_quantities)
@@ -32,8 +32,6 @@ module SpreeCmCommissioner
32
32
 
33
33
  def generate_inventory_items_for_variant(variant, count_on_hand)
34
34
  inventory_dates_for(variant).each do |inventory_date|
35
- next if inventory_exist?(variant, inventory_date)
36
-
37
35
  create_inventory_item(variant, inventory_date, count_on_hand)
38
36
  end
39
37
  end
@@ -52,17 +50,17 @@ module SpreeCmCommissioner
52
50
  end
53
51
  end
54
52
 
55
- def inventory_exist?(variant, inventory_date)
56
- variant.inventory_items.exists?(inventory_date: inventory_date)
57
- end
58
-
59
53
  def create_inventory_item(variant, inventory_date, count_on_hand)
54
+ return if variant.inventory_items.exists?(inventory_date: inventory_date)
55
+
60
56
  variant.inventory_items.create!(
61
57
  inventory_date: inventory_date,
62
58
  quantity_available: count_on_hand,
63
59
  max_capacity: count_on_hand,
64
60
  product_type: variant.product_type
65
61
  )
62
+ rescue ActiveRecord::RecordNotUnique
63
+ # concurrent worker won the race; record already exists
66
64
  end
67
65
 
68
66
  # Returns a hash: { variant_id => total_on_hand, ... }
@@ -3,22 +3,28 @@ module SpreeCmCommissioner
3
3
  class CleanupExpired
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
- BATCH_SIZE = 1000
6
+ BATCH_SIZE = 500
7
+
8
+ # The access tokens set by doorkeeper to expire in 1 day (cm-market-server/config/initializers/doorkeeper.rb),
9
+ # so we set the threshold to 90 days to make sure all expired tokens are cleaned up.
7
10
  EXPIRATION_THRESHOLD_DAYS = 90
8
11
 
9
12
  def call
10
- cutoff_date = EXPIRATION_THRESHOLD_DAYS.days.ago
13
+ cutoff_time = EXPIRATION_THRESHOLD_DAYS.days.ago
11
14
  total_deleted = 0
12
15
 
16
+ # oauth_access_tokens is a standalone table with no foreign keys or associations
17
+ # pointing to it (checked cm-market-server/db/schema.rb), so it is safe to use
18
+ # delete_all here for faster bulk cleanup.
13
19
  Spree::OauthAccessToken
14
- .where('revoked_at IS NOT NULL OR (expires_in IS NOT NULL AND created_at + make_interval(secs => expires_in) < ?)', cutoff_date)
20
+ .where('created_at < ?', cutoff_time)
15
21
  .in_batches(of: BATCH_SIZE) do |relation|
16
22
  deleted_count = relation.delete_all
17
23
  total_deleted += deleted_count
18
24
  end
19
25
 
20
- log_cleanup_result(total_deleted, cutoff_date)
21
- success(total_deleted: total_deleted, cutoff_date: cutoff_date, batch_size: BATCH_SIZE, expiration_threshold_days: EXPIRATION_THRESHOLD_DAYS)
26
+ log_cleanup_result(total_deleted, cutoff_time)
27
+ success(total_deleted: total_deleted, cutoff_time: cutoff_time, batch_size: BATCH_SIZE, expiration_threshold_days: EXPIRATION_THRESHOLD_DAYS)
22
28
  rescue StandardError => e
23
29
  log_error(e)
24
30
  failure(nil, e.message)
@@ -26,12 +32,12 @@ module SpreeCmCommissioner
26
32
 
27
33
  private
28
34
 
29
- def log_cleanup_result(total_deleted, cutoff_date)
35
+ def log_cleanup_result(total_deleted, cutoff_time)
30
36
  CmAppLogger.log(
31
37
  label: 'SpreeCmCommissioner::OauthAccessTokens::CleanupExpired completed',
32
38
  data: {
33
39
  total_deleted: total_deleted,
34
- cutoff_date: cutoff_date,
40
+ cutoff_time: cutoff_time,
35
41
  batch_size: BATCH_SIZE,
36
42
  expiration_threshold_days: EXPIRATION_THRESHOLD_DAYS
37
43
  }
@@ -31,7 +31,8 @@ module SpreeCmCommissioner
31
31
  def schedule_sync_inventory(inventory_id_and_quantities)
32
32
  SpreeCmCommissioner::InventoryItems::BulkAdjustQuantitiesJob.perform_later(
33
33
  inventory_id_and_quantities: inventory_id_and_quantities,
34
- line_item_ids: @line_item_ids
34
+ line_item_ids: @line_item_ids,
35
+ caller_source: "#{self.class.name}#schedule_sync_inventory"
35
36
  )
36
37
  end
37
38
  end
@@ -3,29 +3,17 @@ module SpreeCmCommissioner
3
3
  class DecreaseTripCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
- def call(trip:)
7
- return failure(nil, 'Trip not found') unless trip
8
-
9
- route_metric = find_or_create_route_metric(trip)
6
+ def call(route_metric:)
7
+ return failure(nil, 'Route metric not found') unless route_metric
10
8
 
11
9
  route_metric.with_lock do
12
10
  route_metric.update!(trip_count: [route_metric.trip_count - 1, 0].max)
13
11
  end
14
12
 
15
- success(trip: trip)
13
+ success(route_metric: route_metric)
16
14
  rescue StandardError => e
17
15
  failure(nil, e.message)
18
16
  end
19
-
20
- private
21
-
22
- def find_or_create_route_metric(trip)
23
- SpreeCmCommissioner::RouteMetric.find_or_create_by!(
24
- origin_place_id: trip.origin_place_id,
25
- destination_place_id: trip.destination_place_id,
26
- route_type: trip.route_type
27
- )
28
- end
29
17
  end
30
18
  end
31
19
  end
@@ -3,29 +3,21 @@ module SpreeCmCommissioner
3
3
  class IncreaseTripCount
4
4
  prepend ::Spree::ServiceModule::Base
5
5
 
6
- def call(trip:)
7
- return failure(nil, 'Trip not found') unless trip
8
-
9
- route_metric = find_or_create_route_metric(trip)
6
+ def call(origin_place_id:, destination_place_id:, route_type:)
7
+ route_metric = SpreeCmCommissioner::RouteMetric.find_or_create_by!(
8
+ origin_place_id: origin_place_id,
9
+ destination_place_id: destination_place_id,
10
+ route_type: route_type
11
+ )
10
12
 
11
13
  route_metric.with_lock do
12
14
  route_metric.update!(trip_count: route_metric.trip_count + 1)
13
15
  end
14
16
 
15
- success(trip: trip)
17
+ success(route_metric: route_metric)
16
18
  rescue StandardError => e
17
19
  failure(nil, e.message)
18
20
  end
19
-
20
- private
21
-
22
- def find_or_create_route_metric(trip)
23
- SpreeCmCommissioner::RouteMetric.find_or_create_by!(
24
- origin_place_id: trip.origin_place_id,
25
- destination_place_id: trip.destination_place_id,
26
- route_type: trip.route_type
27
- )
28
- end
29
21
  end
30
22
  end
31
23
  end
@@ -49,9 +49,15 @@ module SpreeCmCommissioner
49
49
  order.preload_main_trip_ids = (outbound_legs.map(&:main_trip_id) + inbound_legs.map(&:main_trip_id)).flatten.compact.uniq
50
50
  order.order_options = order_options if order_options.present?
51
51
 
52
- raise StandardError, order.errors.full_messages.to_sentence unless order.save
52
+ ActiveRecord::Base.transaction do
53
+ raise StandardError, order.errors.full_messages.to_sentence unless order.save
54
+
55
+ order.update_with_updater!
56
+
57
+ # Move the order from 'cart' to 'address' & hold the selected seats.
58
+ order.next!
59
+ end
53
60
 
54
- order.update_with_updater!
55
61
  order
56
62
  end
57
63
 
@@ -12,7 +12,7 @@
12
12
  <div data-hook="admin_line_item_form_date">
13
13
  <%= f.field_container :from_date do %>
14
14
  <%= f.label :from_date, raw(Spree.t(:from_date) + required_span_tag) %>
15
- <%= f.text_field :from_date, class: 'form-control datePickerFrom mb-2', 'data-alt-input': true, 'data-enable-time': true, value: (@line_item.from_date ? @line_item.from_date.to_date : nil) %>
15
+ <%= f.text_field :from_date, class: 'form-control datePickerFrom mb-2', 'data-alt-input': true, 'data-enable-time': true, value: @line_item.from_date&.in_time_zone&.strftime('%Y-%m-%d %H:%M') %>
16
16
  <%= f.error_message_on :from_date %>
17
17
  <% end %>
18
18
  </div>
@@ -20,11 +20,10 @@
20
20
  <div data-hook="admin_line_item_to_date">
21
21
  <%= f.field_container :to_date do %>
22
22
  <%= f.label :to_date, raw(Spree.t(:to_date) + required_span_tag) %>
23
- <%= f.text_field :to_date, class: 'form-control datePickerTo', 'data-alt-input': true, 'data-enable-time': true, value: @line_item.to_date, 'data-min-date': @line_item.from_date %>
23
+ <%= f.text_field :to_date, class: 'form-control datePickerTo', 'data-alt-input': true, 'data-enable-time': true, value: @line_item.to_date&.in_time_zone&.strftime('%Y-%m-%d %H:%M'), 'data-min-date': @line_item.from_date&.in_time_zone&.strftime('%Y-%m-%d %H:%M') %>
24
24
  <%= f.error_message_on :to_date %>
25
25
  <% end %>
26
26
  </div>
27
27
  </div>
28
28
  </div>
29
29
  </div>
30
-
@@ -50,10 +50,34 @@ module SpreeCmCommissioner
50
50
  @trips.map(&:max_capacity).compact.min
51
51
  end
52
52
 
53
- def total_price
53
+ def price
54
54
  @trips.map(&:price).compact.sum
55
55
  end
56
56
 
57
+ # alias total_price to price for backward compatibility.
58
+ alias total_price price
59
+
60
+ def compare_at_price
61
+ values = @trips.map(&:compare_at_price).compact
62
+ values.empty? ? nil : values.sum
63
+ end
64
+
65
+ def currency
66
+ @trips.first&.currency
67
+ end
68
+
69
+ def display_price
70
+ return nil if price.nil? || currency.nil?
71
+
72
+ Spree::Money.new(price, currency: currency).to_s
73
+ end
74
+
75
+ def display_compare_at_price
76
+ return nil if compare_at_price.nil? || currency.nil?
77
+
78
+ Spree::Money.new(compare_at_price, currency: currency).to_s
79
+ end
80
+
57
81
  def total_open_return_price
58
82
  return total_price unless direct?
59
83
 
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.8.2'.freeze
2
+ VERSION = '2.8.3-pre1'.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.8.2
4
+ version: 2.8.3.pre.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-29 00:00:00.000000000 Z
11
+ date: 2026-06-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -3370,9 +3370,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
3370
3370
  version: '2.7'
3371
3371
  required_rubygems_version: !ruby/object:Gem::Requirement
3372
3372
  requirements:
3373
- - - ">="
3373
+ - - ">"
3374
3374
  - !ruby/object:Gem::Version
3375
- version: '0'
3375
+ version: 1.3.1
3376
3376
  requirements:
3377
3377
  - none
3378
3378
  rubygems_version: 3.4.1