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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba19c5f6b8ae1e8a80fb0128bf1f58b1a653d6430aa11d46ae3722b13e2d17be
4
- data.tar.gz: 7993749d255ca3800e9d200adc21820bad68ac350a60b88882a5bda4ee054701
3
+ metadata.gz: cfed0f91fdd9c96b14fdd179bfca7cdce626d8268468b3d6a22b7622c8aee3f5
4
+ data.tar.gz: b2e6802c655db5e83abb39e20f3478b0068a79a847744e1a812b7f22191ac345
5
5
  SHA512:
6
- metadata.gz: 83ffc09e4828acf4abb37b4d10e537f2302f967dca20fdf86a806f260fe733dbd9da4097a2f813650e29f2f1420350c538ec4aa8eedc7d9b7543a68e9c535d07
7
- data.tar.gz: 40448782e45555c79e319cca373607a53ca34125ce1c5e13f45a74aa0014992a7294c768fdca9826287e645b2073a2f953eb2956ce983d1147d810f68a31c608
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.pre19)
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
@@ -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
- if firebase_context.success?
8
- ActiveRecord::Base.transaction do
9
- register_user!(firebase_context.provider[:name], firebase_context.provider[:email])
10
- link_user_account!(firebase_context.provider)
11
- end
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
- context.fail!(message: firebase_context.message)
28
+ register_user_without_email!(name)
14
29
  end
15
30
  end
16
31
 
17
- def register_user!(name, email)
18
- user = Spree.user_class.new(
19
- password: SecureRandom.base64(16),
20
- email: email,
21
- tenant_id: context.tenant_id,
22
- **name_attributes(name)
23
- )
24
- user.confirmed_at = Time.zone.now
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
- split = full_name.split
37
- first_name = split[0]
38
- last_name = split[1..].join(' ')
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 = SpreeCmCommissioner::UserIdentityProvider.where(
58
- user_id: context.user,
59
- identity_type: identity_type
60
- ).first_or_initialize
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 ArchiveInactiveOrdersJob < ApplicationJob
3
+ class DailyArchiveInactiveOrdersJob < ApplicationJob
4
4
  # Scheduled job that runs daily to archive incomplete orders inactive for 14+ days.
5
- # Thin wrapper that calls ArchiveInactiveOrders service.
5
+ # Thin wrapper that calls DailyArchiveInactiveOrders service.
6
6
  # ApplicationJob handles error logging via around_perform hook.
7
7
  def perform
8
- SpreeCmCommissioner::Orders::ArchiveInactiveOrders.new.call
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,Metrics/AbcSize
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
- @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
@@ -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 ArchiveInactiveOrders
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
- inactive_orders = Spree::Order
28
- .where(archived_at: nil)
29
- .where(completed_at: nil)
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::ArchiveInactiveOrders#call completed',
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:, 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
  [
@@ -0,0 +1,7 @@
1
+ class AddIndexToSpreeOrdersUpdatedAt < ActiveRecord::Migration[7.0]
2
+ def change
3
+ unless index_exists?(:spree_orders, :updated_at)
4
+ add_index :spree_orders, :updated_at, name: 'index_spree_orders_on_updated_at'
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.3.0-pre19'.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.pre19
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
@@ -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/archive_inactive_orders_job.rb
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/archive_inactive_orders.rb
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