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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +3 -0
- data/app/interactors/spree_cm_commissioner/google_routes_distance_calculator.rb +211 -0
- data/app/interactors/spree_cm_commissioner/intercity_taxi_order_creator.rb +52 -12
- data/app/interactors/spree_cm_commissioner/trip_distance_calculator.rb +165 -0
- data/app/models/concerns/spree_cm_commissioner/order_scopes.rb +4 -5
- data/app/queries/spree_cm_commissioner/trip_query.rb +45 -33
- data/app/services/spree_cm_commissioner/orders/bulk_archive_inactive_orders.rb +10 -2
- data/app/services/spree_cm_commissioner/orders/daily_archive_inactive_orders.rb +8 -0
- data/app/services/spree_cm_commissioner/trips/search.rb +7 -11
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cfed0f91fdd9c96b14fdd179bfca7cdce626d8268468b3d6a22b7622c8aee3f5
|
|
4
|
+
data.tar.gz: b2e6802c655db5e83abb39e20f3478b0068a79a847744e1a812b7f22191ac345
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cdfd95dbabf86d3201d45b5ba89987d572850707f652a4b55432c08b4245604dbb4f3b8b9edf62661d3c78c7b65b8c0cddfffb00a331bb1f4721c56ce034f97
|
|
7
|
+
data.tar.gz: c7c061df9dc72b3c0fc8cc49b01fbb424b65f75a67bca9586c743093a20508dc6a7be1adaa219d1d1ca32c0cef86c3976ae88abae5d19241f257b6b3e2098cd1
|
data/Gemfile.lock
CHANGED
|
@@ -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 =
|
|
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 =
|
|
71
|
-
|
|
72
|
-
item
|
|
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(
|
|
21
|
-
.where(
|
|
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(
|
|
28
|
-
.where(
|
|
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
|
-
|
|
38
|
-
result =
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
16
|
-
# -
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
+
def intercity_taxi?(params) = params[:service_type] == 'intercity_taxi'
|
|
52
48
|
|
|
53
49
|
def cache_key(params, vendor)
|
|
54
50
|
[
|
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.
|
|
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-
|
|
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
|