spree_cm_commissioner 2.3.0.pre.pre19 → 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/interactors/spree_cm_commissioner/user_registration_with_id_token.rb +72 -36
- data/app/jobs/spree_cm_commissioner/orders/bulk_archive_inactive_orders_job.rb +13 -0
- data/app/jobs/spree_cm_commissioner/orders/{archive_inactive_orders_job.rb → daily_archive_inactive_orders_job.rb} +3 -3
- data/app/models/concerns/spree_cm_commissioner/order_scopes.rb +45 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +2 -21
- data/app/queries/spree_cm_commissioner/trip_query.rb +45 -33
- data/app/services/spree_cm_commissioner/orders/bulk_archive_inactive_orders.rb +60 -0
- data/app/services/spree_cm_commissioner/orders/{archive_inactive_orders.rb → daily_archive_inactive_orders.rb} +18 -7
- data/app/services/spree_cm_commissioner/trips/search.rb +7 -11
- data/db/migrate/20251113081853_add_index_to_spree_orders_updated_at.rb +7 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +10 -4
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
|
|
@@ -2,26 +2,54 @@ module SpreeCmCommissioner
|
|
|
2
2
|
class UserRegistrationWithIdToken < BaseInteractor
|
|
3
3
|
# :id_token
|
|
4
4
|
def call
|
|
5
|
+
firebase_context = validate_firebase_token!
|
|
6
|
+
return if firebase_context.nil?
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Base.transaction do
|
|
9
|
+
find_or_register_user!(firebase_context.provider[:name], firebase_context.provider[:email])
|
|
10
|
+
link_user_account!(firebase_context.provider)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def validate_firebase_token!
|
|
5
17
|
firebase_context = SpreeCmCommissioner::FirebaseIdTokenProvider.call(id_token: context.id_token)
|
|
18
|
+
return firebase_context if firebase_context.success?
|
|
6
19
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
context.fail!(message: firebase_context.message)
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_or_register_user!(name, email)
|
|
25
|
+
if email.present?
|
|
26
|
+
register_user_with_email!(name, email)
|
|
12
27
|
else
|
|
13
|
-
|
|
28
|
+
register_user_without_email!(name)
|
|
14
29
|
end
|
|
15
30
|
end
|
|
16
31
|
|
|
17
|
-
def
|
|
18
|
-
user = Spree.user_class.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
def register_user_with_email!(name, email)
|
|
33
|
+
user = Spree.user_class.find_by(email: email, tenant_id: context.tenant_id)
|
|
34
|
+
return context.user = ensure_user_confirmed!(user) if user.present?
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
context.user = Spree.user_class.find_or_create_by!(email: email, tenant_id: context.tenant_id) do |u|
|
|
38
|
+
assign_user_attributes(u, name)
|
|
39
|
+
end
|
|
40
|
+
# Handle potential race condition: Another request might have created the user
|
|
41
|
+
# between our find_by and find_or_create_by calls. If we get a RecordNotUnique,
|
|
42
|
+
# it means the user was created by another process, so we find and return it.
|
|
43
|
+
rescue ActiveRecord::RecordNotUnique
|
|
44
|
+
user = Spree.user_class.find_by!(email: email, tenant_id: context.tenant_id)
|
|
45
|
+
context.user = ensure_user_confirmed!(user)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def register_user_without_email!(name)
|
|
50
|
+
user = Spree.user_class.new(email: nil, tenant_id: context.tenant_id)
|
|
51
|
+
assign_user_attributes(user, name)
|
|
52
|
+
|
|
25
53
|
if user.save(validate: false)
|
|
26
54
|
context.user = user
|
|
27
55
|
else
|
|
@@ -29,39 +57,32 @@ module SpreeCmCommissioner
|
|
|
29
57
|
end
|
|
30
58
|
end
|
|
31
59
|
|
|
60
|
+
def assign_user_attributes(user, name)
|
|
61
|
+
user.password = SecureRandom.base64(16)
|
|
62
|
+
user.confirmed_at = Time.zone.now
|
|
63
|
+
user.assign_attributes(name_attributes(name))
|
|
64
|
+
end
|
|
65
|
+
|
|
32
66
|
def name_attributes(name)
|
|
33
67
|
full_name = name&.strip
|
|
34
68
|
return {} if full_name.blank?
|
|
35
69
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
last_name =
|
|
39
|
-
|
|
40
|
-
attributes = {}
|
|
41
|
-
attributes[:first_name] = first_name if first_name.present?
|
|
42
|
-
attributes[:last_name] = last_name if last_name.present?
|
|
70
|
+
parts = full_name.split
|
|
71
|
+
attributes = { first_name: parts[0] }
|
|
72
|
+
attributes[:last_name] = parts[1..].join(' ') if parts.size > 1
|
|
43
73
|
|
|
44
74
|
attributes
|
|
45
75
|
end
|
|
46
76
|
|
|
47
|
-
# provider object
|
|
48
|
-
|
|
49
|
-
# {
|
|
50
|
-
# identity_type: identity_type,
|
|
51
|
-
# sub: sub
|
|
52
|
-
# }
|
|
53
|
-
|
|
54
77
|
def link_user_account!(provider)
|
|
55
78
|
identity_type = SpreeCmCommissioner::UserIdentityProvider.identity_types[provider[:identity_type]]
|
|
79
|
+
user_identity_provider = find_or_initialize_identity_provider(identity_type)
|
|
56
80
|
|
|
57
|
-
user_identity_provider
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
user_identity_provider.sub = provider[:sub]
|
|
63
|
-
user_identity_provider.email = provider[:email]
|
|
64
|
-
user_identity_provider.name = provider[:name]
|
|
81
|
+
user_identity_provider.assign_attributes(
|
|
82
|
+
sub: provider[:sub],
|
|
83
|
+
email: provider[:email],
|
|
84
|
+
name: provider[:name]
|
|
85
|
+
)
|
|
65
86
|
|
|
66
87
|
if user_identity_provider.save
|
|
67
88
|
context.user_identity_provider = user_identity_provider
|
|
@@ -69,5 +90,20 @@ module SpreeCmCommissioner
|
|
|
69
90
|
context.fail!(message: user_identity_provider.errors.full_messages)
|
|
70
91
|
end
|
|
71
92
|
end
|
|
93
|
+
|
|
94
|
+
def find_or_initialize_identity_provider(identity_type)
|
|
95
|
+
SpreeCmCommissioner::UserIdentityProvider.where(
|
|
96
|
+
user_id: context.user,
|
|
97
|
+
identity_type: identity_type
|
|
98
|
+
).first_or_initialize
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Ensure user is confirmed when linking with identity provider.
|
|
102
|
+
# Users created via OAuth should be auto-confirmed since they've proven
|
|
103
|
+
# their identity through the OAuth provider.
|
|
104
|
+
def ensure_user_confirmed!(user)
|
|
105
|
+
user.update(confirmed_at: Time.zone.now) if user.confirmed_at.nil?
|
|
106
|
+
user
|
|
107
|
+
end
|
|
72
108
|
end
|
|
73
109
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Orders
|
|
3
|
+
class BulkArchiveInactiveOrdersJob < ApplicationJob
|
|
4
|
+
# Manual job that archives ALL incomplete orders inactive for 14+ days.
|
|
5
|
+
# Thin wrapper that calls BulkArchiveInactiveOrders service.
|
|
6
|
+
# ApplicationJob handles error logging via around_perform hook.
|
|
7
|
+
# Triggered manually via Sidekiq (not scheduled).
|
|
8
|
+
def perform
|
|
9
|
+
SpreeCmCommissioner::Orders::BulkArchiveInactiveOrders.new.call
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module Orders
|
|
3
|
-
class
|
|
3
|
+
class DailyArchiveInactiveOrdersJob < ApplicationJob
|
|
4
4
|
# Scheduled job that runs daily to archive incomplete orders inactive for 14+ days.
|
|
5
|
-
# Thin wrapper that calls
|
|
5
|
+
# Thin wrapper that calls DailyArchiveInactiveOrders service.
|
|
6
6
|
# ApplicationJob handles error logging via around_perform hook.
|
|
7
7
|
def perform
|
|
8
|
-
SpreeCmCommissioner::Orders::
|
|
8
|
+
SpreeCmCommissioner::Orders::DailyArchiveInactiveOrders.new.call
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module OrderScopes
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
# Archiving scopes
|
|
7
|
+
scope :archived, -> { where.not(archived_at: nil) }
|
|
8
|
+
scope :not_archived, -> { where(archived_at: nil) }
|
|
9
|
+
|
|
10
|
+
# Payment and state scopes
|
|
11
|
+
scope :subscription, -> { where.not(subscription_id: nil) }
|
|
12
|
+
scope :paid, -> { where(payment_state: :paid) }
|
|
13
|
+
scope :complete_or_canceled, -> { complete.or(where(state: 'canceled')) }
|
|
14
|
+
scope :payment, -> { incomplete.where(state: 'payment') }
|
|
15
|
+
scope :without_user, -> { where(user_id: nil) }
|
|
16
|
+
|
|
17
|
+
# Inactive orders scopes with parameterized dates
|
|
18
|
+
# Usage: Spree::Order.inactive_incomplete_all(threshold: 14.days.ago)
|
|
19
|
+
scope :inactive_incomplete_all, lambda { |threshold: 14.days.ago|
|
|
20
|
+
where('updated_at < ?', threshold)
|
|
21
|
+
.where(archived_at: nil, completed_at: nil)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Usage: Spree::Order.inactive_incomplete(threshold: 14.days.ago, window: 7.days)
|
|
25
|
+
# Returns orders updated between (threshold - window) and threshold
|
|
26
|
+
scope :inactive_incomplete, lambda { |threshold: 14.days.ago, window: 7.days|
|
|
27
|
+
where('updated_at >= ? AND updated_at < ?', threshold - window, threshold)
|
|
28
|
+
.where(archived_at: nil, completed_at: nil)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Filter scopes
|
|
32
|
+
scope :filter_by_match_user_contact, lambda { |user|
|
|
33
|
+
complete.where(
|
|
34
|
+
'(email = :email OR intel_phone_number = :intel_phone_number) AND user_id IS NULL',
|
|
35
|
+
email: user.email,
|
|
36
|
+
intel_phone_number: user.intel_phone_number
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
scope :filter_by_vendor, lambda { |vendor|
|
|
41
|
+
joins(:line_items).where(spree_line_items: { vendor_id: vendor }).distinct
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -1,31 +1,12 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module OrderDecorator
|
|
3
|
-
def self.prepended(base) # rubocop:disable Metrics/MethodLength
|
|
3
|
+
def self.prepended(base) # rubocop:disable Metrics/MethodLength
|
|
4
4
|
base.include SpreeCmCommissioner::StoreMetadata
|
|
5
5
|
base.include SpreeCmCommissioner::PhoneNumberSanitizer
|
|
6
6
|
base.include SpreeCmCommissioner::OrderSeatable
|
|
7
7
|
base.include SpreeCmCommissioner::OrderStateMachine
|
|
8
8
|
base.include SpreeCmCommissioner::RouteOrderCountable
|
|
9
|
-
|
|
10
|
-
base.scope :subscription, -> { where.not(subscription_id: nil) }
|
|
11
|
-
base.scope :paid, -> { where(payment_state: :paid) }
|
|
12
|
-
base.scope :complete_or_canceled, -> { complete.or(where(state: 'canceled')) }
|
|
13
|
-
base.scope :payment, -> { incomplete.where(state: 'payment') }
|
|
14
|
-
base.scope :archived, -> { where.not(archived_at: nil) }
|
|
15
|
-
base.scope :not_archived, -> { where(archived_at: nil) }
|
|
16
|
-
base.scope :without_user, -> { where(user_id: nil) }
|
|
17
|
-
|
|
18
|
-
base.scope :filter_by_match_user_contact, lambda { |user|
|
|
19
|
-
complete.where(
|
|
20
|
-
'(email = :email OR intel_phone_number = :intel_phone_number) AND user_id IS NULL',
|
|
21
|
-
email: user.email,
|
|
22
|
-
intel_phone_number: user.intel_phone_number
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
base.scope :filter_by_vendor, lambda { |vendor|
|
|
27
|
-
joins(:line_items).where(spree_line_items: { vendor_id: vendor }).distinct
|
|
28
|
-
}
|
|
9
|
+
base.include SpreeCmCommissioner::OrderScopes
|
|
29
10
|
|
|
30
11
|
base.before_create :link_by_phone_number
|
|
31
12
|
base.before_create :associate_customer
|
|
@@ -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
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Orders
|
|
3
|
+
class BulkArchiveInactiveOrders
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
# Archives ALL incomplete orders inactive for 14+ days (no time range limit).
|
|
7
|
+
# This is a bulk operation for manual cleanup via Sidekiq.
|
|
8
|
+
# Archived orders are hidden from users (Orders::Find filters them out).
|
|
9
|
+
#
|
|
10
|
+
# Criteria for archiving:
|
|
11
|
+
# - archived_at IS NULL (not already archived)
|
|
12
|
+
# - completed_at IS NULL (incomplete/unfinished orders)
|
|
13
|
+
# - updated_at < 14 days ago (inactive for 2+ weeks)
|
|
14
|
+
#
|
|
15
|
+
# Difference from DailyArchiveInactiveOrders:
|
|
16
|
+
# - DailyArchiveInactiveOrders: Runs daily, archives only 1-week window (21-14 days)
|
|
17
|
+
# - BulkArchiveInactiveOrders: Manual job, archives ALL orders older than 14 days
|
|
18
|
+
#
|
|
19
|
+
# Use case: Bulk cleanup when needed, triggered manually via Sidekiq
|
|
20
|
+
def call
|
|
21
|
+
# Archives ALL orders inactive for 14+ days (no time range limit)
|
|
22
|
+
# Uses parameterized scope: threshold=14.days.ago
|
|
23
|
+
inactive_orders = Spree::Order.inactive_incomplete_all(threshold: 14.days.ago)
|
|
24
|
+
|
|
25
|
+
count = inactive_orders.count
|
|
26
|
+
|
|
27
|
+
# Archive all inactive orders with reason in internal_note.
|
|
28
|
+
# We use bulk update_all instead of find_each because:
|
|
29
|
+
# - We rarely use internal_note, so preserving history is not critical
|
|
30
|
+
# - Bulk update is significantly faster (1 query vs N queries)
|
|
31
|
+
# - For bulk operations, performance is critical
|
|
32
|
+
inactive_orders.update_all( # rubocop:disable Rails/SkipsModelValidations
|
|
33
|
+
archived_at: Time.current,
|
|
34
|
+
internal_note: 'Auto-archived: inactive for 14 days',
|
|
35
|
+
updated_at: Time.current
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
CmAppLogger.log(
|
|
39
|
+
label: 'SpreeCmCommissioner::Orders::BulkArchiveInactiveOrders#call completed',
|
|
40
|
+
data: {
|
|
41
|
+
archived_count: count,
|
|
42
|
+
threshold_days: 14
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
success(archived_count: count)
|
|
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
|
+
)
|
|
56
|
+
failure(nil, e.message)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module Orders
|
|
3
|
-
class
|
|
3
|
+
class DailyArchiveInactiveOrders
|
|
4
4
|
prepend ::Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
6
|
# Archives incomplete orders that haven't been updated for 14 days.
|
|
@@ -24,14 +24,17 @@ module SpreeCmCommissioner
|
|
|
24
24
|
# - Keeps user-facing order history clean (hidden from Orders::Find queries)
|
|
25
25
|
# - Reduces clutter in active carts/orders
|
|
26
26
|
def call
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.where('updated_at < ?', 14.days.ago)
|
|
27
|
+
# Archives orders from the 1-week window (21-14 days ago)
|
|
28
|
+
# Uses parameterized scope: threshold=14.days.ago, window=7.days
|
|
29
|
+
inactive_orders = Spree::Order.inactive_incomplete(threshold: 14.days.ago, window: 7.days)
|
|
31
30
|
|
|
32
31
|
count = inactive_orders.count
|
|
33
32
|
|
|
34
|
-
# Archive all inactive orders with reason in internal_note
|
|
33
|
+
# Archive all inactive orders with reason in internal_note.
|
|
34
|
+
# We use bulk update_all instead of find_each because:
|
|
35
|
+
# - We rarely use internal_note, so preserving history is not critical
|
|
36
|
+
# - Bulk update is significantly faster (1 query vs N queries)
|
|
37
|
+
# - For typical 100-500 orders/day, bulk update is worth the performance gain
|
|
35
38
|
inactive_orders.update_all( # rubocop:disable Rails/SkipsModelValidations
|
|
36
39
|
archived_at: Time.current,
|
|
37
40
|
internal_note: 'Auto-archived: inactive for 14 days',
|
|
@@ -39,7 +42,7 @@ module SpreeCmCommissioner
|
|
|
39
42
|
)
|
|
40
43
|
|
|
41
44
|
CmAppLogger.log(
|
|
42
|
-
label: 'SpreeCmCommissioner::Orders::
|
|
45
|
+
label: 'SpreeCmCommissioner::Orders::DailyArchiveInactiveOrders#call completed',
|
|
43
46
|
data: {
|
|
44
47
|
archived_count: count,
|
|
45
48
|
threshold_days: 14
|
|
@@ -48,6 +51,14 @@ module SpreeCmCommissioner
|
|
|
48
51
|
|
|
49
52
|
success(archived_count: count)
|
|
50
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
|
+
)
|
|
51
62
|
failure(nil, e.message)
|
|
52
63
|
end
|
|
53
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
|
|
@@ -1310,7 +1312,8 @@ files:
|
|
|
1310
1312
|
- app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb
|
|
1311
1313
|
- app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb
|
|
1312
1314
|
- app/jobs/spree_cm_commissioner/order_complete_telegram_sender_job.rb
|
|
1313
|
-
- app/jobs/spree_cm_commissioner/orders/
|
|
1315
|
+
- app/jobs/spree_cm_commissioner/orders/bulk_archive_inactive_orders_job.rb
|
|
1316
|
+
- app/jobs/spree_cm_commissioner/orders/daily_archive_inactive_orders_job.rb
|
|
1314
1317
|
- app/jobs/spree_cm_commissioner/product_event_id_to_children_syncer_job.rb
|
|
1315
1318
|
- app/jobs/spree_cm_commissioner/queue_order_webhooks_requests_job.rb
|
|
1316
1319
|
- app/jobs/spree_cm_commissioner/reports_assigner_job.rb
|
|
@@ -1358,6 +1361,7 @@ files:
|
|
|
1358
1361
|
- app/models/concerns/spree_cm_commissioner/metafield.rb
|
|
1359
1362
|
- app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb
|
|
1360
1363
|
- app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb
|
|
1364
|
+
- app/models/concerns/spree_cm_commissioner/order_scopes.rb
|
|
1361
1365
|
- app/models/concerns/spree_cm_commissioner/order_seatable.rb
|
|
1362
1366
|
- app/models/concerns/spree_cm_commissioner/order_state_machine.rb
|
|
1363
1367
|
- app/models/concerns/spree_cm_commissioner/parameterize_name.rb
|
|
@@ -1953,7 +1957,8 @@ files:
|
|
|
1953
1957
|
- app/services/spree_cm_commissioner/intercity_taxi_order/update.rb
|
|
1954
1958
|
- app/services/spree_cm_commissioner/metafields/product_metadata_service.rb
|
|
1955
1959
|
- app/services/spree_cm_commissioner/order_params_checker.rb
|
|
1956
|
-
- app/services/spree_cm_commissioner/orders/
|
|
1960
|
+
- app/services/spree_cm_commissioner/orders/bulk_archive_inactive_orders.rb
|
|
1961
|
+
- app/services/spree_cm_commissioner/orders/daily_archive_inactive_orders.rb
|
|
1957
1962
|
- app/services/spree_cm_commissioner/orders/generate_commissions_decorator.rb
|
|
1958
1963
|
- app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb
|
|
1959
1964
|
- app/services/spree_cm_commissioner/organizer/export_invite_guest_csv_service.rb
|
|
@@ -2844,6 +2849,7 @@ files:
|
|
|
2844
2849
|
- db/migrate/20251009033331_add_registered_by_to_spree_users.rb
|
|
2845
2850
|
- db/migrate/20251009073040_add_lock_version_to_cm_routes.rb
|
|
2846
2851
|
- db/migrate/20251009073929_add_lock_version_to_cm_vendor_routes.rb
|
|
2852
|
+
- db/migrate/20251113081853_add_index_to_spree_orders_updated_at.rb
|
|
2847
2853
|
- docker-compose.yml
|
|
2848
2854
|
- docs/api/scoped-access-token-endpoints.md
|
|
2849
2855
|
- docs/option_types/attr_types.md
|