spree_cm_commissioner 2.5.0.pre.pre13 → 2.5.0.pre.pre14
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/.env.example +4 -0
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/admin/base_controller_decorator.rb +7 -33
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +7 -51
- data/app/controllers/spree/api/v2/storefront/intercity_taxi/distance_calculator_controller.rb +47 -0
- data/app/controllers/spree/api/v2/storefront/intercity_taxi/draft_orders_controller.rb +7 -5
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +2 -2
- data/app/controllers/spree/api/v2/tenant/intercity_taxi/distance_calculator_controller.rb +47 -0
- data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +7 -5
- data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +2 -2
- data/app/interactors/spree_cm_commissioner/google_routes_distance_calculator.rb +0 -13
- data/app/interactors/spree_cm_commissioner/invalidate_cache_request.rb +3 -3
- data/app/interactors/spree_cm_commissioner/vehicle_type_updater.rb +41 -0
- data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +1 -1
- data/app/mailers/spree/order_mailer_decorator.rb +22 -1
- data/app/models/concerns/spree_cm_commissioner/line_item_seat_selection.rb +40 -0
- data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +1 -1
- data/app/models/concerns/spree_cm_commissioner/vehicle_kind.rb +21 -0
- data/app/models/spree_cm_commissioner/adjustable/adjuster/pricing_action.rb +25 -0
- data/app/models/spree_cm_commissioner/guest.rb +1 -1
- data/app/models/spree_cm_commissioner/guest_dynamic_field.rb +3 -0
- data/app/models/spree_cm_commissioner/homepage_background_app_image.rb +1 -1
- data/app/models/spree_cm_commissioner/invite_guest.rb +2 -5
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +2 -2
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +4 -1
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +4 -1
- data/app/models/spree_cm_commissioner/{option_value_vehicle.rb → option_value_vehicle_type.rb} +2 -2
- data/app/models/spree_cm_commissioner/trip.rb +7 -6
- data/app/models/spree_cm_commissioner/vehicle.rb +8 -20
- data/app/models/spree_cm_commissioner/vehicle_type.rb +28 -0
- data/app/models/spree_cm_commissioner/vehicle_type_option_type.rb +6 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/overrides/spree/admin/products/edit/clear_cache_button.html.erb.deface +1 -1
- data/app/overrides/spree/admin/taxons/edit/clear_cache_button.html.erb.deface +3 -2
- data/app/queries/spree_cm_commissioner/trip_query.rb +5 -5
- data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +2 -0
- data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +12 -0
- data/app/serializers/spree/v2/tenant/line_item_serializer.rb +14 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/distance_serializer.rb +26 -0
- data/app/serializers/spree_cm_commissioner/v2/storefront/intercity_taxi_line_item_serializer.rb +3 -3
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_result_serializer.rb +2 -2
- data/app/serializers/spree_cm_commissioner/v2/storefront/trip_serializer.rb +1 -1
- data/app/serializers/spree_cm_commissioner/v2/storefront/{trip_vehicle_serializer.rb → trip_vehicle_type_serializer.rb} +2 -4
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +98 -0
- data/app/services/spree_cm_commissioner/calculate_distance.rb +279 -0
- data/app/services/spree_cm_commissioner/cart/recalculate_decorator.rb +28 -1
- data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +65 -0
- data/app/services/spree_cm_commissioner/intercity_taxi_order/calculate_distance.rb +224 -0
- data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +30 -3
- data/app/services/spree_cm_commissioner/line_items/apply_pricing_models.rb +27 -0
- data/app/services/spree_cm_commissioner/pricing_models/apply.rb +4 -12
- data/app/services/spree_cm_commissioner/signing/sign_data.rb +42 -0
- data/app/services/spree_cm_commissioner/signing/verify_signature.rb +52 -0
- data/app/services/spree_cm_commissioner/transit/export_order.rb +146 -0
- data/app/services/spree_cm_commissioner/update_guest_service.rb +129 -0
- data/app/views/spree/admin/homepage_section/edit.html.erb +7 -2
- data/app/views/spree/order_mailer/cancel_email.html.erb +19 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_cancel_email.html.erb +12 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_footer.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_header.html.erb +3 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_mailer_stylesheets.html.erb +139 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/_your_booking.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_items.html.erb +53 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/purchased_items/_summary.html.erb +108 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_cancel_email.html.erb +12 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_footer.html.erb +8 -0
- data/app/views/spree_cm_commissioner/cancel_order_mailer/tenant/_header.html.erb +15 -0
- data/app/views/spree_cm_commissioner/layouts/cancel_order_mailer.html.erb +16 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/km.yml +27 -2
- data/config/routes.rb +2 -0
- data/db/migrate/20251127074809_change_guest_dynamic_fields_value_from_jsonb_to_text.rb +40 -0
- data/db/migrate/20251219035243_add_migrations_to_support_vehicle_types.rb +36 -0
- data/lib/spree_cm_commissioner/distance.rb +88 -0
- data/lib/spree_cm_commissioner/engine.rb +3 -0
- data/lib/spree_cm_commissioner/test_helper/factories/guest_factory.rb +2 -2
- data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +3 -3
- data/lib/spree_cm_commissioner/test_helper/factories/pricing_model_variant_factory.rb +6 -0
- data/lib/spree_cm_commissioner/test_helper/factories/seat_layout_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/trip_factory.rb +1 -0
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_factory.rb +4 -1
- data/lib/spree_cm_commissioner/test_helper/factories/vehicle_type_factory.rb +7 -0
- data/lib/spree_cm_commissioner/trip_query_result.rb +1 -1
- data/lib/spree_cm_commissioner/trip_result.rb +7 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +1 -1
- metadata +39 -10
- data/app/interactors/spree_cm_commissioner/trip_distance_calculator.rb +0 -165
- data/app/interactors/spree_cm_commissioner/vehicle_updater.rb +0 -41
- data/app/models/concerns/spree_cm_commissioner/vehicle_type.rb +0 -11
- data/app/models/spree_cm_commissioner/vehicle_option_type.rb +0 -6
- data/lib/spree_cm_commissioner/intercity_taxi/distance.rb +0 -38
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_route_factory.rb +0 -7
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Computes trip distance details for intercity taxi trips.
|
|
2
|
+
#
|
|
3
|
+
# == Purpose
|
|
4
|
+
# Calculate total distance, detour distances, and chargeable "extra km"
|
|
5
|
+
# when pickups/dropoffs deviate from the standard intercity route.
|
|
6
|
+
#
|
|
7
|
+
# == INPUT (keyword args)
|
|
8
|
+
# trip_id: [Integer] REQUIRED - SpreeCmCommissioner::Trip id
|
|
9
|
+
# pickups_attributes: [Array] Optional waypoints BEFORE the route (e.g. hotel pickup)
|
|
10
|
+
# dropoffs_attributes: [Array] Required waypoints AT/NEAR destination (e.g. specific address)
|
|
11
|
+
# pickup_oob_confirmed: [Boolean] If true, charge extra km for pickup detours beyond boundary
|
|
12
|
+
# dropoff_oob_confirmed:[Boolean] If true, charge extra km for dropoff detours beyond boundary
|
|
13
|
+
#
|
|
14
|
+
# Waypoint format (understood by GoogleRoutesDistanceCalculator):
|
|
15
|
+
# { lat: Float, lng: Float }
|
|
16
|
+
#
|
|
17
|
+
# == OUTPUT (Result object)
|
|
18
|
+
# result.success? => true/false
|
|
19
|
+
# result.failure? => true/false
|
|
20
|
+
# result.value => SpreeCmCommissioner::Distance (on success)
|
|
21
|
+
# result.error => String error message (on failure)
|
|
22
|
+
#
|
|
23
|
+
# == Distance Value Object Fields
|
|
24
|
+
# distance_km: Total route distance (with all waypoints)
|
|
25
|
+
# ordered_points: Array of { lat:, lng: } in travel order
|
|
26
|
+
# estimated_time_minutes: ETA from Google (nil for simple routes)
|
|
27
|
+
# base_km: Distance without any waypoints (origin → destination)
|
|
28
|
+
# detour_pickup_km: Distance when only pickups are added (nil if not calculated)
|
|
29
|
+
# detour_dropoff_km: Distance when only dropoffs are added (nil if not calculated)
|
|
30
|
+
# extra_pickup_km: Chargeable km beyond pickup boundary (0.0 if within free zone)
|
|
31
|
+
# extra_dropoff_km: Chargeable km beyond dropoff boundary (0.0 if within free zone)
|
|
32
|
+
#
|
|
33
|
+
# == Example Usage
|
|
34
|
+
# result = SpreeCmCommissioner::CalculateDistance.call(
|
|
35
|
+
# trip_id: 123,
|
|
36
|
+
# pickups_attributes: [{ lat: 11.55, lng: 104.91 }],
|
|
37
|
+
# dropoffs_attributes: [{ lat: 13.36, lng: 103.86 }],
|
|
38
|
+
# pickup_oob_confirmed: true,
|
|
39
|
+
# dropoff_oob_confirmed: true
|
|
40
|
+
# )
|
|
41
|
+
# if result.success?
|
|
42
|
+
# distance = result.value
|
|
43
|
+
# puts "Total: #{distance.distance_km} km, Extra pickup charge: #{distance.extra_pickup_km} km"
|
|
44
|
+
# else
|
|
45
|
+
# puts "Error: #{result.error}"
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
module SpreeCmCommissioner
|
|
49
|
+
class CalculateDistance
|
|
50
|
+
prepend ::Spree::ServiceModule::Base
|
|
51
|
+
|
|
52
|
+
def call(
|
|
53
|
+
pickups_attributes: [],
|
|
54
|
+
dropoffs_attributes: [],
|
|
55
|
+
pickup_oob_confirmed: false,
|
|
56
|
+
dropoff_oob_confirmed: false,
|
|
57
|
+
trip_id: nil
|
|
58
|
+
)
|
|
59
|
+
return failure(nil, 'trip_id is required') if trip_id.blank?
|
|
60
|
+
|
|
61
|
+
@trip_id = trip_id
|
|
62
|
+
@pickup_oob_confirmed = pickup_oob_confirmed
|
|
63
|
+
@dropoff_oob_confirmed = dropoff_oob_confirmed
|
|
64
|
+
|
|
65
|
+
@pickups_points = Array(pickups_attributes).compact
|
|
66
|
+
@dropoffs_points = Array(dropoffs_attributes).compact
|
|
67
|
+
|
|
68
|
+
@final_destination = @dropoffs_points.last
|
|
69
|
+
|
|
70
|
+
return failure(nil, 'Trip origin location is not configured') if origin_point.blank?
|
|
71
|
+
return failure(nil, 'Trip destination location is not configured') if destination_point.blank?
|
|
72
|
+
return failure(nil, 'At least one dropoff is required') if @dropoffs_points.empty?
|
|
73
|
+
|
|
74
|
+
# Step 1: Calculate base route (no waypoints) - this is the reference for detour calculations
|
|
75
|
+
base_km = calculate_base_route_km
|
|
76
|
+
return failure(nil, 'Unable to calculate base route') if base_km.nil?
|
|
77
|
+
|
|
78
|
+
# Step 2: Calculate full route with all waypoints (optimized order)
|
|
79
|
+
details_ctx = fetch_full_route_details
|
|
80
|
+
return failure(nil, details_ctx.error || 'Unable to calculate route') unless details_ctx.success?
|
|
81
|
+
|
|
82
|
+
# Step 3: Calculate detour distances and derive extra chargeable km
|
|
83
|
+
detour_distances = calculate_detour_distances
|
|
84
|
+
build_result(details_ctx, base_km, detour_distances)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
failure(nil, e.message)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# ===========================================
|
|
92
|
+
# DISTANCE CALCULATIONS
|
|
93
|
+
# ===========================================
|
|
94
|
+
|
|
95
|
+
# Base route: origin → destination (no waypoints)
|
|
96
|
+
# This is the "ideal" shortest route the operator priced for.
|
|
97
|
+
def calculate_base_route_km
|
|
98
|
+
compute_km(origin: origin_point, destination: destination_point)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Full route: origin → pickups → dropoffs → final_destination
|
|
102
|
+
# Google optimizes waypoint order for shortest path.
|
|
103
|
+
def fetch_full_route_details
|
|
104
|
+
SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
|
|
105
|
+
origin: origin_point,
|
|
106
|
+
destination: @final_destination,
|
|
107
|
+
pickups: @pickups_points,
|
|
108
|
+
dropoffs: @dropoffs_points,
|
|
109
|
+
optimize: true
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Detour distances are calculated separately for pickups and dropoffs.
|
|
114
|
+
# These help us determine how much extra distance each type of waypoint adds.
|
|
115
|
+
def calculate_detour_distances
|
|
116
|
+
{
|
|
117
|
+
pickup: calculate_pickup_detour_km,
|
|
118
|
+
dropoff: calculate_dropoff_detour_km
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Pickup detour: origin → pickups → destination (no dropoffs)
|
|
123
|
+
# Only calculated if pickups exist AND user confirmed OOB charge.
|
|
124
|
+
def calculate_pickup_detour_km
|
|
125
|
+
return nil unless @pickups_points.any? && @pickup_oob_confirmed
|
|
126
|
+
|
|
127
|
+
compute_km(
|
|
128
|
+
origin: origin_point,
|
|
129
|
+
destination: destination_point,
|
|
130
|
+
pickups: @pickups_points,
|
|
131
|
+
optimize: false # Keep pickup order as specified
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Dropoff detour: origin → dropoffs → final_destination (no pickups)
|
|
136
|
+
# Only calculated if dropoffs exist AND user confirmed OOB charge.
|
|
137
|
+
def calculate_dropoff_detour_km
|
|
138
|
+
return nil unless @dropoffs_points.any? && @dropoff_oob_confirmed
|
|
139
|
+
|
|
140
|
+
compute_km(
|
|
141
|
+
origin: origin_point,
|
|
142
|
+
destination: @final_destination,
|
|
143
|
+
dropoffs: @dropoffs_points,
|
|
144
|
+
optimize: false # Keep dropoff order as specified
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ===========================================
|
|
149
|
+
# RESULT BUILDERS
|
|
150
|
+
# ===========================================
|
|
151
|
+
|
|
152
|
+
# Full result with waypoints - includes detour and extra km calculations
|
|
153
|
+
def build_result(details_ctx, base_km, detour_distances)
|
|
154
|
+
# Calculate how many extra km each detour adds compared to base route
|
|
155
|
+
# Example: If base = 50km and pickup_detour = 58km, then increment = 8km
|
|
156
|
+
pickup_increment_km = detour_distances[:pickup].present? ? detour_distances[:pickup] - base_km : 0.0
|
|
157
|
+
dropoff_increment_km = detour_distances[:dropoff].present? ? detour_distances[:dropoff] - base_km : 0.0
|
|
158
|
+
|
|
159
|
+
# Calculate chargeable extra km (beyond free boundary allowance)
|
|
160
|
+
extra_pickup_km = compute_extra_km(pickup_increment_km, pickup_boundary_km, @pickup_oob_confirmed)
|
|
161
|
+
extra_dropoff_km = compute_extra_km(dropoff_increment_km, dropoff_boundary_km, @dropoff_oob_confirmed)
|
|
162
|
+
|
|
163
|
+
distance = SpreeCmCommissioner::Distance.new(
|
|
164
|
+
distance_km: details_ctx.distance_km,
|
|
165
|
+
ordered_points: details_ctx.ordered_points,
|
|
166
|
+
estimated_time_minutes: details_ctx.estimated_time_minutes,
|
|
167
|
+
base_km: base_km,
|
|
168
|
+
detour_pickup_km: detour_distances[:pickup],
|
|
169
|
+
detour_dropoff_km: detour_distances[:dropoff],
|
|
170
|
+
extra_pickup_km: extra_pickup_km,
|
|
171
|
+
extra_dropoff_km: extra_dropoff_km
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
success(distance)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# ===========================================
|
|
178
|
+
# EXTRA KM CALCULATION
|
|
179
|
+
#
|
|
180
|
+
# "Extra km" = detour km that exceeds a free boundary allowance.
|
|
181
|
+
# - Operators allow some detour for free (e.g., 5km for pickups)
|
|
182
|
+
# - Only km beyond that boundary is charged
|
|
183
|
+
#
|
|
184
|
+
# Example:
|
|
185
|
+
# base_km = 50, detour_km = 58 → increment = 8km
|
|
186
|
+
# boundary = 5km → extra = 8 - 5 = 3km chargeable
|
|
187
|
+
# ===========================================
|
|
188
|
+
|
|
189
|
+
# Calculate chargeable extra km beyond the free boundary
|
|
190
|
+
#
|
|
191
|
+
# @param increment_km [Float] How much longer the detour is vs base
|
|
192
|
+
# @param boundary_km [Float] Free allowance (no charge below this)
|
|
193
|
+
# @param confirmed [Boolean] Whether OOB charge was confirmed
|
|
194
|
+
# @return [Float] Chargeable km (0.0 if within free zone or not confirmed)
|
|
195
|
+
def compute_extra_km(increment_km, boundary_km, confirmed)
|
|
196
|
+
return 0.0 unless confirmed && increment_km
|
|
197
|
+
|
|
198
|
+
extra = increment_km - boundary_km.to_f
|
|
199
|
+
extra.positive? ? extra.round(2) : 0.0
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# ===========================================
|
|
203
|
+
# GOOGLE ROUTES INTEGRATION
|
|
204
|
+
# ===========================================
|
|
205
|
+
|
|
206
|
+
# Wrapper to call GoogleRoutesDistanceCalculator and extract distance_km
|
|
207
|
+
# Returns nil if the calculation fails.
|
|
208
|
+
def compute_km(origin:, destination:, pickups: nil, dropoffs: nil, optimize: nil)
|
|
209
|
+
context = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
|
|
210
|
+
origin: origin,
|
|
211
|
+
destination: destination,
|
|
212
|
+
pickups: pickups,
|
|
213
|
+
dropoffs: dropoffs,
|
|
214
|
+
optimize: optimize
|
|
215
|
+
)
|
|
216
|
+
return nil unless context.success?
|
|
217
|
+
|
|
218
|
+
context.distance_km
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# ===========================================
|
|
222
|
+
# BOUNDARIES (Free Detour Allowance)
|
|
223
|
+
#
|
|
224
|
+
# TODO: Allow operators to configure these per trip/vendor (in the future)
|
|
225
|
+
# ===========================================
|
|
226
|
+
|
|
227
|
+
# Free pickup detour allowance (km)
|
|
228
|
+
def pickup_boundary_km
|
|
229
|
+
5.0
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Free dropoff detour allowance (km)
|
|
233
|
+
def dropoff_boundary_km
|
|
234
|
+
3.0
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# ===========================================
|
|
238
|
+
# TRIP HELPERS - Resolve origin/destination from Trip
|
|
239
|
+
# ===========================================
|
|
240
|
+
|
|
241
|
+
def trip
|
|
242
|
+
@trip ||= SpreeCmCommissioner::Trip.find(@trip_id)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Get origin point from trip's vendor location configuration
|
|
246
|
+
# Returns { lat:, lng: } hash or nil
|
|
247
|
+
def origin_point
|
|
248
|
+
@origin_point ||=
|
|
249
|
+
if trip.blank?
|
|
250
|
+
nil
|
|
251
|
+
else
|
|
252
|
+
location = trip.vendor.locations.find_by(place: trip.origin_place)
|
|
253
|
+
to_point(location&.branches&.first&.place)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Get destination point from trip's vendor location configuration
|
|
258
|
+
# Returns { lat:, lng: } hash or nil
|
|
259
|
+
def destination_point
|
|
260
|
+
@destination_point ||=
|
|
261
|
+
if trip.blank?
|
|
262
|
+
nil
|
|
263
|
+
else
|
|
264
|
+
location = trip.vendor.locations.find_by(place: trip.destination_place)
|
|
265
|
+
to_point(location&.branches&.first&.place)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Convert a Place model to { lat:, lng: } hash
|
|
270
|
+
def to_point(place)
|
|
271
|
+
return nil if place.blank?
|
|
272
|
+
|
|
273
|
+
{
|
|
274
|
+
lat: place.lat.to_f,
|
|
275
|
+
lng: place.lon.to_f
|
|
276
|
+
}
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -7,7 +7,34 @@ module SpreeCmCommissioner
|
|
|
7
7
|
# order should be reload before recaculate to avoid wrong caculation.
|
|
8
8
|
order.reload
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
# SPREE: Original Spree::Cart::Recalculate code starts here
|
|
11
|
+
order_updater = ::Spree::OrderUpdater.new(order)
|
|
12
|
+
|
|
13
|
+
order.payments.store_credits.checkout.destroy_all
|
|
14
|
+
order_updater.update
|
|
15
|
+
|
|
16
|
+
shipment = options[:shipment]
|
|
17
|
+
if shipment.present?
|
|
18
|
+
# ADMIN END SHIPMENT RATE FIX
|
|
19
|
+
# refresh shipments to ensure correct shipment amount is calculated when using price sack calculator
|
|
20
|
+
# for calculating shipment rates.
|
|
21
|
+
# Currently shipment rate is calculated on previous order total instead of current order total when updating a shipment from admin end.
|
|
22
|
+
order.refresh_shipment_rates(::Spree::ShippingMethod::DISPLAY_ON_BACK_END)
|
|
23
|
+
shipment.update_amounts
|
|
24
|
+
else
|
|
25
|
+
order.ensure_updated_shipments
|
|
26
|
+
end
|
|
27
|
+
# SPREE: Original Spree::Cart::Recalculate code ends here
|
|
28
|
+
|
|
29
|
+
# CUSTOM: Apply pricing models (intercity taxi, line item pricing) to the line item
|
|
30
|
+
SpreeCmCommissioner::LineItems::ApplyPricingModels.call(order: order, line_item: line_item)
|
|
31
|
+
|
|
32
|
+
# SPREE: Original Spree::Cart::Recalculate code continues here
|
|
33
|
+
::Spree::PromotionHandler::Cart.new(order, line_item).activate
|
|
34
|
+
::Spree::Adjustable::AdjustmentsUpdater.update(line_item)
|
|
35
|
+
::Spree::TaxRate.adjust(order, [line_item.reload]) if line_item_created
|
|
36
|
+
order_updater.update
|
|
37
|
+
success(line_item)
|
|
11
38
|
end
|
|
12
39
|
end
|
|
13
40
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Guests
|
|
3
|
+
class ClaimInviteGuestService
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(guest:, id_card_params:, line_item:, invite_guest:)
|
|
7
|
+
ApplicationRecord.transaction do
|
|
8
|
+
process_id_cards(guest, id_card_params)
|
|
9
|
+
claim_invite_if_needed(line_item, invite_guest)
|
|
10
|
+
send_claim_alert(guest, invite_guest.order)
|
|
11
|
+
|
|
12
|
+
success(guest: guest.reload)
|
|
13
|
+
end
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
failure(guest, e.message)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# ID Card Processing
|
|
21
|
+
def process_id_cards(guest, id_card_params)
|
|
22
|
+
return unless id_card_images_present?(id_card_params)
|
|
23
|
+
|
|
24
|
+
id_card = guest.id_card || guest.build_id_card(card_type: id_card_params[:card_type])
|
|
25
|
+
build_id_card_images(id_card, id_card_params)
|
|
26
|
+
id_card.save!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_id_card_images(id_card, id_card_params)
|
|
30
|
+
id_card.build_front_image(attachment: id_card_params.delete(:front_image)) if id_card_params[:front_image]
|
|
31
|
+
id_card.build_back_image(attachment: id_card_params.delete(:back_image)) if id_card_params[:back_image]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def id_card_images_present?(id_card_params)
|
|
35
|
+
id_card_params[:front_image].present? || id_card_params[:back_image].present?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Invite Claim Management
|
|
39
|
+
def claim_invite_if_needed(line_item, invite_guest)
|
|
40
|
+
return unless line_item.guests.count >= invite_guest.quantity
|
|
41
|
+
|
|
42
|
+
invite_guest.update(claimed_status: :claimed)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Telegram Alert
|
|
46
|
+
def send_claim_alert(guest, order)
|
|
47
|
+
vendor = guest.event&.vendor
|
|
48
|
+
return unless vendor&.preferred_telegram_chat_id
|
|
49
|
+
|
|
50
|
+
factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
|
|
51
|
+
title: '📣 --- [NEW GUEST CLAIMED INVITATION] ---',
|
|
52
|
+
order: order,
|
|
53
|
+
guest: guest,
|
|
54
|
+
vendor: vendor
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
SpreeCmCommissioner::TelegramNotificationSenderJob.perform_later(
|
|
58
|
+
chat_id: vendor.preferred_telegram_chat_id,
|
|
59
|
+
message: factory.message,
|
|
60
|
+
parse_mode: factory.parse_mode
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module IntercityTaxiOrder
|
|
3
|
+
# Computes trip distance details for intercity taxi orders
|
|
4
|
+
class CalculateDistance
|
|
5
|
+
prepend ::Spree::ServiceModule::Base
|
|
6
|
+
|
|
7
|
+
def call(
|
|
8
|
+
pickups_attributes: [],
|
|
9
|
+
dropoffs_attributes: [],
|
|
10
|
+
pickup_oob_confirmed: false,
|
|
11
|
+
dropoff_oob_confirmed: false,
|
|
12
|
+
trip_id: nil
|
|
13
|
+
)
|
|
14
|
+
return failure(nil, 'trip_id is required') if trip_id.blank?
|
|
15
|
+
|
|
16
|
+
initialize_context(pickup_oob_confirmed, dropoff_oob_confirmed, trip_id)
|
|
17
|
+
setup_waypoints(pickups_attributes, dropoffs_attributes)
|
|
18
|
+
|
|
19
|
+
return failure(nil, 'Trip origin location is not configured') if origin.blank?
|
|
20
|
+
return failure(nil, 'Trip destination location is not configured') if configured_destination.blank?
|
|
21
|
+
|
|
22
|
+
base_km = calculate_base_route
|
|
23
|
+
return failure(nil, 'Unable to calculate base route') if base_km.nil?
|
|
24
|
+
return build_simple_result(base_km) unless waypoints?
|
|
25
|
+
|
|
26
|
+
details_ctx = fetch_full_route_details
|
|
27
|
+
return failure(nil, details_ctx.message || 'Unable to calculate') unless details_ctx.success?
|
|
28
|
+
|
|
29
|
+
detour_distances = calculate_detour_distances
|
|
30
|
+
build_result(details_ctx, base_km, detour_distances)
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
failure(nil, e.message)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# -------------------------
|
|
38
|
+
# Setup
|
|
39
|
+
# -------------------------
|
|
40
|
+
|
|
41
|
+
def initialize_context(pickup_oob_confirmed, dropoff_oob_confirmed, trip_id)
|
|
42
|
+
@trip_id = trip_id
|
|
43
|
+
@pickup_oob_confirmed = pickup_oob_confirmed
|
|
44
|
+
@dropoff_oob_confirmed = dropoff_oob_confirmed
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def setup_waypoints(pickups_attributes, dropoffs_attributes)
|
|
48
|
+
@pickups_points = Array(pickups_attributes).compact
|
|
49
|
+
@dropoffs_points = Array(dropoffs_attributes).compact
|
|
50
|
+
@final_destination = @dropoffs_points.last || configured_destination
|
|
51
|
+
|
|
52
|
+
@has_pickups = @pickups_points.any?
|
|
53
|
+
@has_dropoffs = @dropoffs_points.any?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def waypoints?
|
|
57
|
+
@has_pickups || @has_dropoffs
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# -------------------------
|
|
61
|
+
# Distance calculations
|
|
62
|
+
# -------------------------
|
|
63
|
+
|
|
64
|
+
def calculate_base_route
|
|
65
|
+
compute_km(origin: origin, destination: configured_destination)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fetch_full_route_details
|
|
69
|
+
fetch_details(origin, @final_destination, @pickups_points, @dropoffs_points)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def calculate_detour_distances
|
|
73
|
+
{
|
|
74
|
+
pickup: calculate_pickup_detour,
|
|
75
|
+
dropoff: calculate_dropoff_detour
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def calculate_pickup_detour
|
|
80
|
+
return nil unless @has_pickups && @pickup_oob_confirmed
|
|
81
|
+
|
|
82
|
+
compute_km(
|
|
83
|
+
origin: origin,
|
|
84
|
+
destination: configured_destination,
|
|
85
|
+
pickups: @pickups_points,
|
|
86
|
+
optimize: false
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def calculate_dropoff_detour
|
|
91
|
+
return nil unless @has_dropoffs && @dropoff_oob_confirmed
|
|
92
|
+
|
|
93
|
+
compute_km(
|
|
94
|
+
origin: origin,
|
|
95
|
+
destination: @final_destination,
|
|
96
|
+
dropoffs: @dropoffs_points,
|
|
97
|
+
optimize: false
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# -------------------------
|
|
102
|
+
# Result builders
|
|
103
|
+
# -------------------------
|
|
104
|
+
|
|
105
|
+
def build_simple_result(base_km)
|
|
106
|
+
result = SpreeCmCommissioner::IntercityTaxi::DistanceCalculationResult.new(
|
|
107
|
+
distance_km: base_km,
|
|
108
|
+
ordered_points: [origin, configured_destination],
|
|
109
|
+
estimated_time_minutes: nil,
|
|
110
|
+
base_km: base_km,
|
|
111
|
+
detour_pickup_km: nil,
|
|
112
|
+
detour_dropoff_km: nil,
|
|
113
|
+
extra_pickup_km: 0.0,
|
|
114
|
+
extra_dropoff_km: 0.0
|
|
115
|
+
)
|
|
116
|
+
success(result)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_result(details_ctx, base_km, detour_distances)
|
|
120
|
+
# Incremental km = detour - base
|
|
121
|
+
pickup_increment_km = detour_distances[:pickup]&.-(base_km)
|
|
122
|
+
dropoff_increment_km = detour_distances[:dropoff]&.-(base_km)
|
|
123
|
+
|
|
124
|
+
extra_pickup_km = compute_extra_km(pickup_increment_km, pickup_boundary_km, @pickup_oob_confirmed)
|
|
125
|
+
extra_dropoff_km = compute_extra_km(dropoff_increment_km, dropoff_boundary_km, @dropoff_oob_confirmed)
|
|
126
|
+
|
|
127
|
+
result = SpreeCmCommissioner::IntercityTaxi::DistanceCalculationResult.new(
|
|
128
|
+
distance_km: details_ctx.distance_km,
|
|
129
|
+
ordered_points: details_ctx.ordered_points,
|
|
130
|
+
estimated_time_minutes: details_ctx.estimated_time_minutes,
|
|
131
|
+
base_km: base_km,
|
|
132
|
+
detour_pickup_km: detour_distances[:pickup],
|
|
133
|
+
detour_dropoff_km: detour_distances[:dropoff],
|
|
134
|
+
extra_pickup_km: extra_pickup_km,
|
|
135
|
+
extra_dropoff_km: extra_dropoff_km
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
success(result)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# -------------------------
|
|
142
|
+
# Extra km calculation
|
|
143
|
+
# -------------------------
|
|
144
|
+
|
|
145
|
+
# increment_km = detour_km - base_km
|
|
146
|
+
def compute_extra_km(increment_km, boundary_km, confirmed)
|
|
147
|
+
return 0.0 unless confirmed && increment_km
|
|
148
|
+
|
|
149
|
+
extra = increment_km - boundary_km.to_f
|
|
150
|
+
extra.positive? ? extra.round(2) : 0.0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# -------------------------
|
|
154
|
+
# Google Routes integration
|
|
155
|
+
# -------------------------
|
|
156
|
+
|
|
157
|
+
def compute_km(origin:, destination:, pickups: nil, dropoffs: nil, optimize: nil)
|
|
158
|
+
ctx = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
|
|
159
|
+
origin: origin,
|
|
160
|
+
destination: destination,
|
|
161
|
+
pickups: pickups,
|
|
162
|
+
dropoffs: dropoffs,
|
|
163
|
+
optimize: optimize
|
|
164
|
+
)
|
|
165
|
+
return nil unless ctx.success?
|
|
166
|
+
|
|
167
|
+
ctx.distance_km
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def fetch_details(origin, final_destination, pickups_points, dropoffs_points)
|
|
171
|
+
SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
|
|
172
|
+
origin: origin,
|
|
173
|
+
destination: final_destination,
|
|
174
|
+
pickups: pickups_points,
|
|
175
|
+
dropoffs: dropoffs_points,
|
|
176
|
+
optimize: true
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# -------------------------
|
|
181
|
+
# Boundaries
|
|
182
|
+
# -------------------------
|
|
183
|
+
|
|
184
|
+
def pickup_boundary_km
|
|
185
|
+
5.0
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def dropoff_boundary_km
|
|
189
|
+
3.0
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# -------------------------
|
|
193
|
+
# Trip helpers
|
|
194
|
+
# -------------------------
|
|
195
|
+
|
|
196
|
+
def trip
|
|
197
|
+
@trip ||= SpreeCmCommissioner::Trip.find(@trip_id)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def origin
|
|
201
|
+
return if trip.blank?
|
|
202
|
+
|
|
203
|
+
location = trip.vendor.locations.find_by(place: trip.origin_place)
|
|
204
|
+
to_point(location&.branches&.first)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def configured_destination
|
|
208
|
+
return if trip.blank?
|
|
209
|
+
|
|
210
|
+
location = trip.vendor.locations.find_by(place: trip.destination_place)
|
|
211
|
+
to_point(location&.branches&.first)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def to_point(location)
|
|
215
|
+
return if location.blank? || location.place.blank?
|
|
216
|
+
|
|
217
|
+
{
|
|
218
|
+
lat: location.place.lat.to_f,
|
|
219
|
+
lng: location.place.lon.to_f
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -3,6 +3,8 @@ module SpreeCmCommissioner
|
|
|
3
3
|
class Update
|
|
4
4
|
prepend ::Spree::ServiceModule::Base
|
|
5
5
|
|
|
6
|
+
include ::Spree::LineItems::Helper
|
|
7
|
+
|
|
6
8
|
def call(
|
|
7
9
|
order:,
|
|
8
10
|
remark: nil,
|
|
@@ -26,18 +28,43 @@ module SpreeCmCommissioner
|
|
|
26
28
|
)
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Verify signature and extract data for distance
|
|
29
32
|
if distance_attributes.present?
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
verified_data = verify_and_extract_distance(distance_attributes)
|
|
34
|
+
return failure(nil, verified_data[:error]) unless verified_data[:success]
|
|
35
|
+
|
|
36
|
+
line_item.distance = SpreeCmCommissioner::Distance.new(verified_data[:value])
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
line_item.update!(remark: remark)
|
|
40
|
+
recalculate_service.call(order: order, line_item: line_item)
|
|
36
41
|
|
|
37
42
|
success(order.reload)
|
|
38
43
|
rescue StandardError => e
|
|
39
44
|
failure(nil, e.message)
|
|
40
45
|
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Verifies signed distance data and extracts clean data (without signature).
|
|
50
|
+
# Signature is only used for verification, not stored.
|
|
51
|
+
def verify_and_extract_distance(attributes)
|
|
52
|
+
# Convert to Hash with string keys (works for both Hash and ActionController::Parameters)
|
|
53
|
+
# Controller already called .permit(), so .to_h is safe here
|
|
54
|
+
# HashWithIndifferentAccess.to_json works correctly, so we can use it directly
|
|
55
|
+
attributes = attributes.to_h.stringify_keys
|
|
56
|
+
|
|
57
|
+
return { success: false, error: 'Distance signature is required' } if attributes['signature'].blank?
|
|
58
|
+
|
|
59
|
+
# Verify signature before using for price calculations
|
|
60
|
+
verified = SpreeCmCommissioner::Distance.from_signed_hash(attributes)
|
|
61
|
+
return { success: false, error: 'Invalid distance signature or tampered data' } unless verified
|
|
62
|
+
|
|
63
|
+
# Extract clean data without signature and signed_at
|
|
64
|
+
clean_data = verified.to_h
|
|
65
|
+
|
|
66
|
+
{ success: true, value: clean_data }
|
|
67
|
+
end
|
|
41
68
|
end
|
|
42
69
|
end
|
|
43
70
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module LineItems
|
|
3
|
+
class ApplyPricingModels
|
|
4
|
+
prepend ::Spree::ServiceModule::Base
|
|
5
|
+
|
|
6
|
+
def call(order:, line_item:)
|
|
7
|
+
return success(nil) if line_item.blank?
|
|
8
|
+
|
|
9
|
+
return success(line_item) unless order&.persisted?
|
|
10
|
+
return success(line_item) if line_item.variant.blank?
|
|
11
|
+
|
|
12
|
+
active_pricing_models = line_item.variant.pricing_models.active
|
|
13
|
+
return success(line_item) unless active_pricing_models.exists?
|
|
14
|
+
|
|
15
|
+
line_item.adjustments.pricing_action.delete_all
|
|
16
|
+
|
|
17
|
+
active_pricing_models.each do |pricing_model|
|
|
18
|
+
SpreeCmCommissioner::PricingModels::Apply.new(
|
|
19
|
+
line_item: line_item,
|
|
20
|
+
pricing_model: pricing_model
|
|
21
|
+
).call
|
|
22
|
+
end
|
|
23
|
+
success(line_item)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|