spree_cm_commissioner 2.3.0.pre.pre20 → 2.3.0.pre.pre21

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: f73314825dc2db092e1255e49203ceb8e1071e0dac1c11803d8343b8dc37fafa
4
- data.tar.gz: 32c25a52977a2f20591b8650ece48a69e517845b3431b79e3c57da63cf8ddb50
3
+ metadata.gz: cfed0f91fdd9c96b14fdd179bfca7cdce626d8268468b3d6a22b7622c8aee3f5
4
+ data.tar.gz: b2e6802c655db5e83abb39e20f3478b0068a79a847744e1a812b7f22191ac345
5
5
  SHA512:
6
- metadata.gz: 1bd90e765ad3bdd9f28b6199900a7ea4f587dd11732af3a03c8bd3b13d234eb83765740de6619223dd03cbe77cb4b865c82c12c8a1845bc05157977463bf17d6
7
- data.tar.gz: ef7380007df4d1ee55da2fea5d2d9cb2b3b5d931c44d17a955b9925c338f8a6285df1df59459b643152cd03fda819bcdd264075bc87eaaff5961e93d08b84606
6
+ metadata.gz: 7cdfd95dbabf86d3201d45b5ba89987d572850707f652a4b55432c08b4245604dbb4f3b8b9edf62661d3c78c7b65b8c0cddfffb00a331bb1f4721c56ce034f97
7
+ data.tar.gz: c7c061df9dc72b3c0fc8cc49b01fbb424b65f75a67bca9586c743093a20508dc6a7be1adaa219d1d1ca32c0cef86c3976ae88abae5d19241f257b6b3e2098cd1
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.3.0.pre.pre20)
37
+ spree_cm_commissioner (2.3.0.pre.pre21)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -12,6 +12,7 @@ module Spree
12
12
  origin_id: params[:origin_id],
13
13
  destination_id: params[:destination_id],
14
14
  date: params[:date],
15
+ route_type: params[:route_type],
15
16
  vendor_id: params[:vendor_id],
16
17
  number_of_guests: params[:number_of_guests],
17
18
  params: params
@@ -36,6 +37,7 @@ module Spree
36
37
  params[:origin_id],
37
38
  params[:destination_id],
38
39
  params[:date],
40
+ params[:route_type],
39
41
  params[:vendor_id],
40
42
  params[:number_of_guests],
41
43
  resource_includes&.sort&.join(','),
@@ -62,6 +64,7 @@ module Spree
62
64
  'trips.vendor',
63
65
  'trips.vendor.logo',
64
66
  'trips.vehicle',
67
+ 'trips.vehicle.vehicle_photos',
65
68
  'trips.amenities'
66
69
  ]
67
70
  end
@@ -0,0 +1,211 @@
1
+ module SpreeCmCommissioner
2
+ # Interactor wrapper around Google Routes API v2 to compute trip details.
3
+ #
4
+ # Inputs via context:
5
+ # - origin: { lat:, lng: } | "lat,lng" | [lat, lng]
6
+ # - destination: { lat:, lng: } | "lat,lng" | [lat, lng]
7
+ # - Optional arrays: waypoints, pickups, dropoffs (same point formats)
8
+ # - Optional boolean: optimize (default: true) to let Google reorder waypoints
9
+ #
10
+ # Outputs via context on success:
11
+ # - distance_km (Float)
12
+ # - ordered_waypoints (Array<Hash>)
13
+ # - ordered_points (Array<Hash>)
14
+ # - directions_url (String)
15
+ # - estimated_time_minutes (Integer) when optimize is true
16
+ #
17
+ # On failure, context.fail!(message: ...) is called.
18
+ class GoogleRoutesDistanceCalculator < BaseInteractor
19
+ delegate :origin, :destination, :waypoints, :pickups, :dropoffs, :optimize, to: :context
20
+
21
+ def call
22
+ initialize_points_and_options
23
+
24
+ via_points = combined_via_points
25
+ json = fetch_routes_for(@origin, @destination, via_points)
26
+
27
+ if json.nil?
28
+ context.fail!(message: 'Failed to fetch routes from Google API')
29
+ return
30
+ end
31
+
32
+ if json['error']
33
+ error_message = json.dig('error', 'message') || json.dig('error', 'status')
34
+ context.fail!(message: "Google Routes API error: #{error_message}")
35
+ return
36
+ end
37
+
38
+ route = json['routes']&.first
39
+ context.fail!(message: 'No route found') if route.nil?
40
+
41
+ distance_meters = total_distance_of(route)
42
+ duration_seconds = total_duration_of(route)
43
+ ordered_waypoints = build_ordered_waypoints_from(route, via_points)
44
+
45
+ ordered_points = [@origin] + ordered_waypoints + [@destination]
46
+
47
+ context.distance_km = (distance_meters / 1000.0).round(3)
48
+ context.ordered_waypoints = ordered_waypoints
49
+ context.ordered_points = ordered_points
50
+ context.directions_url = build_human_link(ordered_waypoints)
51
+ # Only expose estimated time when optimize is enabled (per requirement)
52
+ context.estimated_time_minutes = (@optimize ? (duration_seconds / 60.0).round : nil)
53
+ end
54
+
55
+ private
56
+
57
+ def initialize_points_and_options
58
+ context.fail!(message: 'origin is required') if origin.blank?
59
+ context.fail!(message: 'destination is required') if destination.blank?
60
+
61
+ begin
62
+ @origin = normalize_point(origin)
63
+ @destination = normalize_point(destination)
64
+
65
+ extra_waypoints = Array(waypoints).compact
66
+ pickups_points = Array(pickups).compact
67
+ dropoff_points = Array(dropoffs).compact
68
+
69
+ @extra_waypoints = extra_waypoints.map { |p| normalize_point(p) }
70
+ @pickups_points = pickups_points.map { |p| normalize_point(p) }
71
+ @dropoff_points = dropoff_points.map { |p| normalize_point(p) }
72
+ rescue ArgumentError => e
73
+ context.fail!(message: e.message)
74
+ end
75
+
76
+ @optimize = optimize.nil? ? true : !!optimize
77
+ end
78
+
79
+ def google_map_key
80
+ @google_map_key ||= ENV.fetch('GOOGLE_MAP_KEY')
81
+ end
82
+
83
+ def fetch_routes_for(from, to_point, via_waypoints)
84
+ url = "https://routes.googleapis.com/directions/v2:computeRoutes?key=#{google_map_key}"
85
+ headers = {
86
+ 'Content-Type' => 'application/json',
87
+ 'X-Goog-FieldMask' => [
88
+ 'routes.duration',
89
+ 'routes.distanceMeters',
90
+ 'routes.legs',
91
+ 'routes.optimizedIntermediateWaypointIndex',
92
+ 'routes.legs.duration',
93
+ 'routes.legs.distanceMeters'
94
+ ].join(',')
95
+ }
96
+ body = build_request_body(from: from, to: to_point, via_waypoints: via_waypoints)
97
+
98
+ response = Faraday.post(url, body.to_json, headers)
99
+ return nil unless response.success?
100
+
101
+ JSON.parse(response.body)
102
+ end
103
+
104
+ def build_request_body(from:, to:, via_waypoints: [])
105
+ request = {
106
+ origin: {
107
+ location: {
108
+ latLng: {
109
+ latitude: from[:lat],
110
+ longitude: from[:lng]
111
+ }
112
+ }
113
+ },
114
+ destination: {
115
+ location: {
116
+ latLng: {
117
+ latitude: to[:lat],
118
+ longitude: to[:lng]
119
+ }
120
+ }
121
+ },
122
+ travelMode: 'DRIVE',
123
+ routingPreference: 'TRAFFIC_AWARE',
124
+ computeAlternativeRoutes: false,
125
+ units: 'METRIC'
126
+ }
127
+
128
+ if via_waypoints.any?
129
+ intermediates = via_waypoints.map do |point|
130
+ {
131
+ location: {
132
+ latLng: {
133
+ latitude: point[:lat],
134
+ longitude: point[:lng]
135
+ }
136
+ }
137
+ }
138
+ end
139
+
140
+ request[:optimizeWaypointOrder] = true if @optimize
141
+
142
+ request[:intermediates] = intermediates
143
+ end
144
+
145
+ request
146
+ end
147
+
148
+ def normalize_point(point)
149
+ return { lat: point[:lat].to_f, lng: point[:lng].to_f } if point.is_a?(Hash)
150
+ return { lat: point[0].to_f, lng: point[1].to_f } if point.is_a?(Array) && point.size == 2
151
+
152
+ if point.is_a?(String) && point.include?(',')
153
+ lat_str, lng_str = point.split(',', 2)
154
+ return { lat: lat_str.to_f, lng: lng_str.to_f }
155
+ end
156
+ raise ArgumentError, 'Invalid point format; expected { lat:, lng: }'
157
+ end
158
+
159
+ def same_point?(point_a, point_b)
160
+ (point_a[:lat].to_f - point_b[:lat].to_f).abs < 1e-9 && (point_a[:lng].to_f - point_b[:lng].to_f).abs < 1e-9
161
+ end
162
+
163
+ def combined_via_points
164
+ points = @extra_waypoints + @pickups_points + @dropoff_points
165
+ points.reject { |p| same_point?(p, @origin) || same_point?(p, @destination) }
166
+ end
167
+
168
+ def total_distance_of(route)
169
+ # Routes API v2 provides distanceMeters at route level
170
+ distance = route['distanceMeters']
171
+ return distance.to_i if distance
172
+
173
+ # Fallback: sum from legs if route-level distance not available
174
+ route.fetch('legs', []).sum { |leg| leg['distanceMeters'].to_i }
175
+ end
176
+
177
+ def total_duration_of(route)
178
+ # Routes API v2 provides duration at route level as string "3600s"
179
+ duration_str = route['duration']
180
+ return duration_str.to_s.gsub('s', '').to_i if duration_str
181
+
182
+ # Fallback: sum from legs if route-level duration not available
183
+ route.fetch('legs', []).sum do |leg|
184
+ leg_duration = leg['duration']
185
+ leg_duration ? leg_duration.to_s.gsub('s', '').to_i : 0
186
+ end
187
+ end
188
+
189
+ def build_ordered_waypoints_from(route, base_waypoints)
190
+ optimized_indices = route['optimizedIntermediateWaypointIndex'] || []
191
+ return base_waypoints if optimized_indices.empty? || !@optimize
192
+
193
+ optimized_indices.map { |idx| base_waypoints[idx] }
194
+ end
195
+
196
+ def format_point(point)
197
+ "#{point[:lat]},#{point[:lng]}"
198
+ end
199
+
200
+ def build_human_link(ordered_waypoints)
201
+ params = {
202
+ api: 1,
203
+ origin: format_point(@origin),
204
+ destination: format_point(@destination),
205
+ travelmode: 'driving'
206
+ }
207
+ params[:waypoints] = ordered_waypoints.map { |p| format_point(p) }.join('|') if ordered_waypoints.any?
208
+ "https://www.google.com/maps/dir/?#{URI.encode_www_form(params)}"
209
+ end
210
+ end
211
+ end
@@ -49,10 +49,21 @@ module SpreeCmCommissioner
49
49
  :passenger_count,
50
50
  :pickup_oob_confirmed,
51
51
  :drop_off_oob_confirmed,
52
+ :distance_km,
53
+ :directions_url,
54
+ :ordered_points,
55
+ :base_km,
56
+ :detour_pickup_km,
57
+ :detour_dropoff_km,
58
+ :extra_pickup_km,
59
+ :extra_dropoff_km,
60
+ :extra_pickup_charge_usd,
61
+ :extra_dropoff_charge_usd,
62
+ :estimated_time_minutes,
52
63
  { guests_attributes: %i[
53
64
  first_name last_name gender age nationality_id
54
65
  ]
55
- }
66
+ }
56
67
  ]
57
68
  )
58
69
 
@@ -61,25 +72,54 @@ module SpreeCmCommissioner
61
72
  end
62
73
 
63
74
  def process_line_items_attributes(params)
64
- date_param = raw_params[:date] || Date.current.to_s
75
+ date_param = request_date
65
76
 
66
77
  load_variant
67
78
 
68
79
  params[:line_items_attributes].each_value do |item|
80
+ normalize_coordinate_group!(item, :pickup, :pickup_lat, :pickup_lng)
81
+ normalize_coordinate_group!(item, :drop_off, :drop_off_lat, :drop_off_lng)
82
+
69
83
  pickup_time_minutes = item[:from_date]
70
- combined_datetime = "#{date_param} #{pickup_time_minutes}"
71
-
72
- item[:from_date] = combined_datetime
73
- item[:to_date] = combined_datetime
74
- item[:variant_id] = @variant.id
75
- item[:vendor_id] = current_vendor.id
76
- item[:quantity] = 1
77
- item[:currency] = 'USD'
78
- item[:direction] = 'outbound'
79
- item[:trip_id] = trip.id
84
+ combined_datetime = build_datetime(date_param, pickup_time_minutes)
85
+
86
+ assign_default_line_item_fields!(item, combined_datetime)
80
87
  end
81
88
  end
82
89
 
90
+ def request_date
91
+ raw_params[:date] || Date.current.to_s
92
+ end
93
+
94
+ def build_datetime(date_string, time_string)
95
+ "#{date_string} #{time_string}"
96
+ end
97
+
98
+ def normalize_coordinate_group!(item, group_key, lat_key, lng_key)
99
+ group = item[group_key]
100
+ return if group.blank?
101
+
102
+ if group.is_a?(Array)
103
+ item[lat_key], item[lng_key] = group
104
+ elsif group.is_a?(ActionController::Parameters) || group.is_a?(Hash)
105
+ item[lat_key] = group[:lat] || group['lat']
106
+ item[lng_key] = group[:lng] || group['lng']
107
+ end
108
+
109
+ item.delete(group_key)
110
+ end
111
+
112
+ def assign_default_line_item_fields!(item, combined_datetime)
113
+ item[:from_date] = combined_datetime
114
+ item[:to_date] = combined_datetime
115
+ item[:variant_id] = @variant.id
116
+ item[:vendor_id] = current_vendor.id
117
+ item[:quantity] = 1
118
+ item[:currency] = 'USD'
119
+ item[:direction] = 'outbound'
120
+ item[:trip_id] = trip.id
121
+ end
122
+
83
123
  def create_order
84
124
  context.order = Spree::Order.create(context.order_params)
85
125
 
@@ -0,0 +1,165 @@
1
+ module SpreeCmCommissioner
2
+ # Computes trip distance details and pricing payload.
3
+ #
4
+ # Inputs via context:
5
+ # - origin: { lat:, lng: }
6
+ # - configured_destination: { lat:, lng: }
7
+ # - pickups: Array<{ lat:, lng: }> (optional)
8
+ # - dropoffs: Array<{ lat:, lng: }> (optional)
9
+ # - pickup_oob_confirmed: Boolean (optional)
10
+ # - dropoff_oob_confirmed: Boolean (optional)
11
+ # - base_price_usd: Numeric
12
+ # - boundary_km: Numeric (optional, default 0)
13
+ #
14
+ # Outputs via context on success:
15
+ # - payload: Hash (compatible with previous controller response)
16
+ # - base_km, detour_pickup_km, detour_dropoff_km: Floats or nil
17
+ # - distance_km, directions_url, ordered_points, estimated_time_minutes (when optimize true)
18
+ class TripDistanceCalculator < BaseInteractor
19
+ delegate :origin,
20
+ :configured_destination,
21
+ :pickups,
22
+ :dropoffs,
23
+ :pickup_oob_confirmed,
24
+ :dropoff_oob_confirmed,
25
+ :base_price_usd,
26
+ :boundary_km,
27
+ to: :context
28
+
29
+ def call
30
+ validate_inputs
31
+
32
+ pickups_points, dropoffs_points, final_destination = prepare_points
33
+ base_km, detour_pickup_km, detour_dropoff_km = compute_route_kms(pickups_points, dropoffs_points, final_destination)
34
+ details_ctx = fetch_details(final_destination, pickups_points, dropoffs_points)
35
+ extra_data = compute_extra_data(base_km, detour_pickup_km, detour_dropoff_km)
36
+ payload = build_payload(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, extra_data)
37
+ assign_context(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, payload)
38
+ end
39
+
40
+ private
41
+
42
+ def validate_inputs
43
+ context.fail!(message: 'origin is required') if origin.blank?
44
+ context.fail!(message: 'configured_destination is required') if configured_destination.blank?
45
+ end
46
+
47
+ def compute_km(origin:, destination:, pickups: nil, dropoffs: nil, optimize: nil)
48
+ ctx = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
49
+ origin: origin,
50
+ destination: destination,
51
+ pickups: pickups,
52
+ dropoffs: dropoffs,
53
+ optimize: optimize
54
+ )
55
+ return nil unless ctx.success?
56
+
57
+ ctx.distance_km
58
+ end
59
+
60
+ def prepare_points
61
+ pickups_points = Array(pickups).compact
62
+ dropoffs_points = Array(dropoffs).compact
63
+ final_destination = dropoffs_points.last || configured_destination
64
+ [pickups_points, dropoffs_points, final_destination]
65
+ end
66
+
67
+ def compute_route_kms(pickups_points, dropoffs_points, final_destination)
68
+ base_km = compute_km(origin: origin, destination: configured_destination)
69
+ detour_pickup_km =
70
+ if pickups_points.any?
71
+ compute_km(
72
+ origin: origin,
73
+ destination: configured_destination,
74
+ pickups: pickups_points,
75
+ optimize: false
76
+ )
77
+ end
78
+ detour_dropoff_km =
79
+ if dropoffs_points.any?
80
+ compute_km(
81
+ origin: origin,
82
+ destination: final_destination,
83
+ dropoffs: dropoffs_points,
84
+ optimize: false
85
+ )
86
+ end
87
+ [base_km, detour_pickup_km, detour_dropoff_km]
88
+ end
89
+
90
+ def fetch_details(final_destination, pickups_points, dropoffs_points)
91
+ ctx = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
92
+ origin: origin,
93
+ destination: final_destination,
94
+ pickups: pickups_points,
95
+ dropoffs: dropoffs_points,
96
+ optimize: true
97
+ )
98
+ context.fail!(message: ctx.message || 'Unable to calculate') unless ctx.success?
99
+ ctx
100
+ end
101
+
102
+ def compute_extra_data(base_km, detour_pickup_km, detour_dropoff_km)
103
+ extra_pickup_km = compute_extra_km(detour_pickup_km, base_km, pickup_oob_confirmed)
104
+ extra_dropoff_km = compute_extra_km(detour_dropoff_km, base_km, dropoff_oob_confirmed)
105
+ extra_pickup_charge_usd = charge_for_extra_km(extra_pickup_km)
106
+ extra_dropoff_charge_usd = charge_for_extra_km(extra_dropoff_km)
107
+ extra_charge_usd = (extra_pickup_charge_usd + extra_dropoff_charge_usd).round(2)
108
+ total_price_usd = (base_price_usd.to_f + extra_charge_usd).round(2)
109
+
110
+ {
111
+ extra_pickup_km: extra_pickup_km,
112
+ extra_dropoff_km: extra_dropoff_km,
113
+ extra_pickup_charge_usd: extra_pickup_charge_usd,
114
+ extra_dropoff_charge_usd: extra_dropoff_charge_usd,
115
+ total_price_usd: total_price_usd
116
+ }
117
+ end
118
+
119
+ def build_payload(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, data)
120
+ {
121
+ distance_km: details_ctx.distance_km,
122
+ directions_url: details_ctx.directions_url,
123
+ ordered_points: details_ctx.ordered_points,
124
+ estimated_time_minutes: details_ctx.estimated_time_minutes,
125
+ base_price_usd: base_price_usd.to_f,
126
+ total_price_usd: data[:total_price_usd],
127
+ extra_pickup_km: data[:extra_pickup_km],
128
+ extra_dropoff_km: data[:extra_dropoff_km],
129
+ extra_pickup_charge_usd: data[:extra_pickup_charge_usd],
130
+ extra_dropoff_charge_usd: data[:extra_dropoff_charge_usd],
131
+ extra_charge_usd: data[:extra_charge_usd],
132
+ base_km: base_km,
133
+ detour_pickup_km: detour_pickup_km,
134
+ detour_dropoff_km: detour_dropoff_km
135
+ }
136
+ end
137
+
138
+ def assign_context(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, payload)
139
+ context.payload = payload
140
+ context.distance_km = details_ctx.distance_km
141
+ context.directions_url = details_ctx.directions_url
142
+ context.ordered_points = details_ctx.ordered_points
143
+ context.estimated_time_minutes = details_ctx.estimated_time_minutes
144
+ context.base_km = base_km
145
+ context.detour_pickup_km = detour_pickup_km
146
+ context.detour_dropoff_km = detour_dropoff_km
147
+ end
148
+
149
+ def compute_extra_km(detour_km, base_km, confirmed)
150
+ return 0.0 unless confirmed && detour_km && base_km
151
+
152
+ [(detour_km - base_km - boundary_km), 0.0].max
153
+ end
154
+
155
+ def charge_for_extra_km(extra_km)
156
+ x = extra_km.to_f
157
+ return 0.0 if x <= 0
158
+ return 2.0 if x <= 5.0
159
+ return 5.0 if x <= 10.0
160
+ return 10.0 if x <= 20.0
161
+
162
+ (10.0 + ((x - 20.0) * 0.5)).round(2)
163
+ end
164
+ end
165
+ end
@@ -17,16 +17,15 @@ module SpreeCmCommissioner
17
17
  # Inactive orders scopes with parameterized dates
18
18
  # Usage: Spree::Order.inactive_incomplete_all(threshold: 14.days.ago)
19
19
  scope :inactive_incomplete_all, lambda { |threshold: 14.days.ago|
20
- where(archived_at: nil, completed_at: nil)
21
- .where('updated_at < ?', threshold)
20
+ where('updated_at < ?', threshold)
21
+ .where(archived_at: nil, completed_at: nil)
22
22
  }
23
23
 
24
24
  # Usage: Spree::Order.inactive_incomplete(threshold: 14.days.ago, window: 7.days)
25
25
  # Returns orders updated between (threshold - window) and threshold
26
26
  scope :inactive_incomplete, lambda { |threshold: 14.days.ago, window: 7.days|
27
- where(archived_at: nil, completed_at: nil)
28
- .where('updated_at >= ?', threshold - window)
29
- .where('updated_at < ?', threshold)
27
+ where('updated_at >= ? AND updated_at < ?', threshold - window, threshold)
28
+ .where(archived_at: nil, completed_at: nil)
30
29
  }
31
30
 
32
31
  # Filter scopes
@@ -1,13 +1,14 @@
1
1
  module SpreeCmCommissioner
2
2
  class TripQuery
3
- attr_reader :origin_id, :destination_id, :date, :vendor_id, :number_of_guests, :params
3
+ attr_reader :origin_id, :destination_id, :date, :vendor_id, :number_of_guests, :route_type, :params
4
4
 
5
- def initialize(origin_id:, destination_id:, date:, vendor_id: nil, number_of_guests: nil, params: {}) # rubocop:disable Metrics/ParameterLists
5
+ def initialize(origin_id:, destination_id:, date:, route_type: nil, vendor_id: nil, number_of_guests: nil, params: {}) # rubocop:disable Metrics/ParameterLists
6
6
  @origin_id = origin_id
7
7
  @destination_id = destination_id
8
8
  @date = date.to_date == Time.zone.now.to_date ? Time.zone.now : Time.zone.parse(date.to_s)
9
9
  @vendor_id = vendor_id
10
10
  @number_of_guests = number_of_guests || 1
11
+ @route_type = route_type
11
12
  @params = params
12
13
  @page = (params[:page] || 1).to_i
13
14
  @per_page = params[:per_page]
@@ -34,37 +35,10 @@ module SpreeCmCommissioner
34
35
  end
35
36
 
36
37
  def direct_trips
37
- @inventory_sql ||= product_inventory_totals
38
- result = SpreeCmCommissioner::Trip
39
- .select(<<~SQL.squish)
40
- cm_trips.*,
41
- boarding.departure_time AS boarding_departure_time,
42
- boarding.stop_place_id AS boarding_stop_id, boarding.stop_name AS boarding_stop_name,
43
- drop_off.stop_name AS drop_off_stop_name, drop_off.stop_place_id AS drop_off_stop_id,
44
- drop_off.arrival_time AS drop_off_arrival_time,
45
- COALESCE(iv.quantity_available, 0) AS quantity_available,
46
- COALESCE(iv.max_capacity, 0) AS max_capacity,
47
- origin_places.name AS origin_place_name, dest_places.name AS destination_place_name,
48
- prices.amount AS amount, prices.compare_at_amount AS compare_at_amount,
49
- prices.currency AS currency
50
- SQL
51
- .includes(vendor: :logo, vehicle: :option_values)
52
- .joins(<<~SQL.squish)
53
- INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = cm_trips.id AND boarding.stop_type = 0
54
- INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = cm_trips.id AND drop_off.stop_type = 1
55
- INNER JOIN cm_places AS origin_places ON origin_places.id = cm_trips.origin_place_id
56
- INNER JOIN cm_places AS dest_places ON dest_places.id = cm_trips.destination_place_id
57
- INNER JOIN spree_variants AS master ON master.product_id = cm_trips.product_id AND master.is_master = true
58
- INNER JOIN spree_prices AS prices ON prices.variant_id = master.id AND prices.deleted_at IS NULL
59
- SQL
60
- .where('(boarding.location_place_id = ? OR boarding.stop_place_id = ? OR cm_trips.origin_place_id = ?)
61
- AND (drop_off.location_place_id = ? OR drop_off.stop_place_id = ? OR cm_trips.destination_place_id = ?)',
62
- origin_id, origin_id, origin_id, destination_id, destination_id, destination_id
63
- )
64
- .joins("INNER JOIN (#{@inventory_sql.to_sql}) iv ON cm_trips.product_id = iv.product_id")
65
-
66
- result = result.where(vendor_id: vendor_id) if vendor_id.present?
67
- @per_page.to_i.positive? ? result.page(@page).per(@per_page) : result.page(@page)
38
+ result = trip_scope
39
+ result = result.where({ vendor_id: vendor_id }.compact)
40
+ result = result.where(route_type: route_type) if route_type.present?
41
+ paginate(result)
68
42
  end
69
43
 
70
44
  def product_inventory_totals
@@ -81,6 +55,44 @@ module SpreeCmCommissioner
81
55
 
82
56
  private
83
57
 
58
+ def trip_scope
59
+ SpreeCmCommissioner::Trip
60
+ .select(<<~SQL.squish)
61
+ cm_trips.*,
62
+ boarding.departure_time AS boarding_departure_time,
63
+ boarding.stop_place_id AS boarding_stop_id, boarding.stop_name AS boarding_stop_name,
64
+ drop_off.stop_name AS drop_off_stop_name, drop_off.stop_place_id AS drop_off_stop_id,
65
+ drop_off.arrival_time AS drop_off_arrival_time,
66
+ COALESCE(iv.quantity_available, 0) AS quantity_available,
67
+ COALESCE(iv.max_capacity, 0) AS max_capacity,
68
+ origin_places.name AS origin_place_name, dest_places.name AS destination_place_name,
69
+ prices.amount AS amount, prices.compare_at_amount AS compare_at_amount,
70
+ prices.currency AS currency
71
+ SQL
72
+ .includes(vendor: :logo, vehicle: :option_values)
73
+ .joins(<<~SQL.squish)
74
+ INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = cm_trips.id AND boarding.stop_type = 0
75
+ INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = cm_trips.id AND drop_off.stop_type = 1
76
+ INNER JOIN cm_places AS origin_places ON origin_places.id = cm_trips.origin_place_id
77
+ INNER JOIN cm_places AS dest_places ON dest_places.id = cm_trips.destination_place_id
78
+ INNER JOIN spree_variants AS master ON master.product_id = cm_trips.product_id AND master.is_master = true
79
+ INNER JOIN spree_prices AS prices ON prices.variant_id = master.id AND prices.deleted_at IS NULL
80
+ SQL
81
+ .where('(boarding.location_place_id = ? OR boarding.stop_place_id = ? OR cm_trips.origin_place_id = ?)
82
+ AND (drop_off.location_place_id = ? OR drop_off.stop_place_id = ? OR cm_trips.destination_place_id = ?)',
83
+ origin_id, origin_id, origin_id, destination_id, destination_id, destination_id
84
+ )
85
+ .joins("INNER JOIN (#{inventory_sql.to_sql}) iv ON cm_trips.product_id = iv.product_id")
86
+ end
87
+
88
+ def paginate(result)
89
+ @per_page.to_i.positive? ? result.page(@page).per(@per_page) : result.page(@page)
90
+ end
91
+
92
+ def inventory_sql
93
+ @inventory_sql ||= product_inventory_totals
94
+ end
95
+
84
96
  def build_trip_result(trip)
85
97
  vehicle = trip&.vehicle
86
98
  vendor = trip&.vendor
@@ -12,8 +12,8 @@ module SpreeCmCommissioner
12
12
  # - completed_at IS NULL (incomplete/unfinished orders)
13
13
  # - updated_at < 14 days ago (inactive for 2+ weeks)
14
14
  #
15
- # Difference from ArchiveInactiveOrders:
16
- # - ArchiveInactiveOrders: Runs daily, archives only 1-week window (21-14 days)
15
+ # Difference from DailyArchiveInactiveOrders:
16
+ # - DailyArchiveInactiveOrders: Runs daily, archives only 1-week window (21-14 days)
17
17
  # - BulkArchiveInactiveOrders: Manual job, archives ALL orders older than 14 days
18
18
  #
19
19
  # Use case: Bulk cleanup when needed, triggered manually via Sidekiq
@@ -45,6 +45,14 @@ module SpreeCmCommissioner
45
45
 
46
46
  success(archived_count: count)
47
47
  rescue StandardError => e
48
+ CmAppLogger.error(
49
+ label: 'SpreeCmCommissioner::Orders::BulkArchiveInactiveOrders#call failed',
50
+ data: {
51
+ error_class: e.class.name,
52
+ error_message: e.message,
53
+ backtrace: e.backtrace&.first(5)&.join("\n")
54
+ }
55
+ )
48
56
  failure(nil, e.message)
49
57
  end
50
58
  end
@@ -51,6 +51,14 @@ module SpreeCmCommissioner
51
51
 
52
52
  success(archived_count: count)
53
53
  rescue StandardError => e
54
+ CmAppLogger.error(
55
+ label: 'SpreeCmCommissioner::Orders::DailyArchiveInactiveOrders#call failed',
56
+ data: {
57
+ error_class: e.class.name,
58
+ error_message: e.message,
59
+ backtrace: e.backtrace&.first(5)&.join("\n")
60
+ }
61
+ )
54
62
  failure(nil, e.message)
55
63
  end
56
64
  end
@@ -10,21 +10,23 @@ module SpreeCmCommissioner
10
10
  new.send(:cache_key, params, vendor)
11
11
  end
12
12
 
13
- def call(params:, vendor:, is_intercity_taxi: false)
13
+ def call(params:, vendor:)
14
14
  return success(results: []) unless valid_search?(params)
15
15
 
16
16
  cache_key = cache_key(params, vendor)
17
17
 
18
18
  results = Rails.cache.fetch(cache_key, expires_in: SEARCH_CACHE_TTL) do
19
+ route_type = intercity_taxi?(params) ? 'intercity_taxi' : nil
19
20
  trips = SpreeCmCommissioner::TripQuery.new(
20
21
  origin_id: origin_id(params),
21
22
  destination_id: destination_id(params),
23
+ date: search_date(params).to_date,
24
+ route_type: route_type,
22
25
  vendor_id: vendor.id,
23
- number_of_guests: params[:number_of_guests] || 1,
24
- date: search_date(params).to_date
26
+ number_of_guests: params[:number_of_guests] || 1
25
27
  ).call
26
28
 
27
- is_intercity_taxi || taxi?(params) ? filter_intercity(trips) : trips
29
+ trips
28
30
  end
29
31
 
30
32
  success(results: results)
@@ -38,17 +40,11 @@ module SpreeCmCommissioner
38
40
  params[:origin].present? && params[:destination].present? && params[:outbound_date].present?
39
41
  end
40
42
 
41
- def filter_intercity(results)
42
- Array(results).select do |res|
43
- res.respond_to?(:trips) && res.trips.any? { |t| t.route_type.to_s == 'intercity_taxi' }
44
- end
45
- end
46
-
47
43
  def origin_id(params) = outbound?(params) ? params[:origin] : params[:destination]
48
44
  def destination_id(params) = outbound?(params) ? params[:destination] : params[:origin]
49
45
  def search_date(params) = outbound?(params) ? params[:outbound_date] : params[:inbound_date]
50
46
  def outbound?(params) = params[:direction] == 'outbound' || params[:direction].blank?
51
- def taxi?(params) = params[:service_type] == 'intercity_taxi'
47
+ def intercity_taxi?(params) = params[:service_type] == 'intercity_taxi'
52
48
 
53
49
  def cache_key(params, vendor)
54
50
  [
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.3.0-pre20'.freeze
2
+ VERSION = '2.3.0-pre21'.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.3.0.pre.pre20
4
+ version: 2.3.0.pre.pre21
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-13 00:00:00.000000000 Z
11
+ date: 2025-11-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -1182,6 +1182,7 @@ files:
1182
1182
  - app/interactors/spree_cm_commissioner/firebase_email_fetcher_cron_executor.rb
1183
1183
  - app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb
1184
1184
  - app/interactors/spree_cm_commissioner/google_places_fetcher.rb
1185
+ - app/interactors/spree_cm_commissioner/google_routes_distance_calculator.rb
1185
1186
  - app/interactors/spree_cm_commissioner/guest_dynamic_field_notification_sender.rb
1186
1187
  - app/interactors/spree_cm_commissioner/guest_dynamic_fields_manager.rb
1187
1188
  - app/interactors/spree_cm_commissioner/guest_id_card_manager.rb
@@ -1252,6 +1253,7 @@ files:
1252
1253
  - app/interactors/spree_cm_commissioner/transactional_email_sender.rb
1253
1254
  - app/interactors/spree_cm_commissioner/transit/draft_order_creator.rb
1254
1255
  - app/interactors/spree_cm_commissioner/trip_clone_creator.rb
1256
+ - app/interactors/spree_cm_commissioner/trip_distance_calculator.rb
1255
1257
  - app/interactors/spree_cm_commissioner/trip_stops_creator.rb
1256
1258
  - app/interactors/spree_cm_commissioner/unique_device_token_cron_executor.rb
1257
1259
  - app/interactors/spree_cm_commissioner/update_payment_gateway_status.rb