Class: SpreeCmCommissioner::TransitOrder::Create
- Inherits:
-
Object
- Object
- SpreeCmCommissioner::TransitOrder::Create
- Includes:
- Spree::ServiceModule::Base
- Defined in:
- app/services/spree_cm_commissioner/transit_order/create.rb
Instance Method Summary collapse
-
#add_return_line_item!(order, outbound_legs, quantity, outbound_date) ⇒ Object
Attach the outbound trip’s open-return add-on (a plain ecommerce product) as a regular line item.
- #build_guests_for!(line_item, seat_selections) ⇒ Object
- #build_line_items_for!(leg, order, date, connected_trip_id = nil) ⇒ Object
-
#build_line_items_for_legs!(order:, legs:, initial_date:) ⇒ Object
Build line items for each leg.
-
#build_parent_stops_map(legs) ⇒ Object
Builds a mapping of leg trip_id => { sequence:, offset_days: } from the parent trip’s branch stops.
- #calculate_line_item_duration!(date:, departure_time:, arrival_time:, departure_offset_days: 0, arrival_offset_days: 0) ⇒ Object
-
#call(outbound_date:, inbound_date:, outbound_legs:, inbound_legs: [], user: nil, include_return: false) ⇒ Object
rubocop:disable Metrics/ParameterLists.
-
#create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, include_return) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists.
- #insert_saved_guests_per_line_items_leg(line_items) ⇒ Object
-
#preload_trips_and_stops!(legs) ⇒ Object
Preloads all trips and their relevant trip stops in 2 queries total, avoiding N+1 queries in build_line_items_for! and resolve_leg_dates.
- #resolve_leg_dates(trip, leg, date) ⇒ Object
-
#validity_window(addon, outbound_date) ⇒ Object
- valid_from, valid_until
-
for the entitlement.
Instance Method Details
#add_return_line_item!(order, outbound_legs, quantity, outbound_date) ⇒ Object
Attach the outbound trip’s open-return add-on (a plain ecommerce product) as a regular line item. The add-on master is backorderable, so no inventory wiring is needed.
The entitlement stays redeemable for the add-on’s open_dated_validity_days (default 90), counted from the outbound journey date; that window is stored as valid_until (to_date) so LineItem#expired? can enforce it. Redemption later overwrites these with the real trip dates.
We persist the return journey’s endpoints (already reversed from the outbound trip) so redemption can match a chosen return trip directly, with no flip and no trip/route load. Setup is single-leg today, so the endpoints come straight off the outbound trip; when multi-leg outbound is supported, the return origin is the last leg’s drop-off place and the return destination is the first leg’s boarding place.
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 164 def add_return_line_item!(order, outbound_legs, quantity, outbound_date) return if quantity <= 0 return unless outbound_legs.size == 1 trip = @preloaded_trips[outbound_legs.first.trip_id] || SpreeCmCommissioner::Trip.find(outbound_legs.first.trip_id) addon = trip.open_dated_product return if addon.nil? from_date, to_date = validity_window(addon, outbound_date) order.line_items.new( product_type: :ecommerce, variant_id: addon.master.id, quantity: quantity, from_date: from_date, to_date: to_date, open_dated_product_id: addon.id, open_dated_return_origin_place_id: trip.destination_place_id, open_dated_return_destination_place_id: trip.origin_place_id ) end |
#build_guests_for!(line_item, seat_selections) ⇒ Object
212 213 214 215 216 217 218 219 220 221 222 223 224 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 212 def build_guests_for!(line_item, seat_selections) block_ids = seat_selections.flat_map(&:block_ids) if block_ids.any? && block_ids.size != line_item.quantity raise StandardError, "Number of blocks (#{block_ids.size}) does not match quantity (#{line_item.quantity})" end Array.new(line_item.quantity) do |index| line_item.guests.new(block_id: block_ids[index]) end line_item end |
#build_line_items_for!(leg, order, date, connected_trip_id = nil) ⇒ Object
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 125 def build_line_items_for!(leg, order, date, connected_trip_id = nil) trip = @preloaded_trips[leg.trip_id] || SpreeCmCommissioner::Trip.find(leg.trip_id) from_date, to_date = resolve_leg_dates(trip, leg, date) leg.seat_selections.group_by(&:variant_id).map do |variant_id, seat_selections| = { direction: leg.direction, trip_id: leg.trip_id.to_s, boarding_trip_stop_id: leg.boarding_trip_stop_id&.to_s, drop_off_trip_stop_id: leg.drop_off_trip_stop_id&.to_s, connected_trip_id: connected_trip_id.presence&.to_s, service_origin_id: trip.service_origin_id.presence&.to_s }.compact line_item = order.line_items.new( product_type: :transit, from_date: from_date, to_date: to_date, variant_id: variant_id, quantity: seat_selections.sum(&:quantity), private_metadata: ) build_guests_for!(line_item, seat_selections) end end |
#build_line_items_for_legs!(order:, legs:, initial_date:) ⇒ Object
Build line items for each leg. Legs arrive in sometimes not by order from the client, so we sort by the parent trip’s branch stop sequence to ensure chronological processing. Each leg’s date is computed via offset_days from initial_date; falls back to chaining from the previous leg’s to_date for direct (single-leg) trips.
Example: “Phnom Penh → Siem Reap → Poipet”, initial_date = 2026-04-01
input: [leg(trip=201, seq=2), leg(trip=200, seq=1)]
sorted: [leg(trip=200, seq=1), leg(trip=201, seq=2)]
dates: trip 200 → 2026-04-01 (offset 0), trip 201 → 2026-04-02 (offset 1)
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 74 def build_line_items_for_legs!(order:, legs:, initial_date:) all_line_items = [] parent_stops_map = build_parent_stops_map(legs) current_leg_date = initial_date # Sort legs by parent trip's branch stop sequence to ensure chronological order. sorted_legs = parent_stops_map.any? ? legs.sort_by { |leg| parent_stops_map.dig(leg.trip_id, :sequence) || 0 } : legs sorted_legs.each do |leg| connected_trip_id = leg.main_trip_id.to_s if leg.main_trip_id.present? offset_days = parent_stops_map.dig(leg.trip_id, :offset_days) leg_date = offset_days.present? ? initial_date + offset_days.days : current_leg_date leg_line_items = build_line_items_for!(leg, order, leg_date, connected_trip_id) leg_line_items = insert_saved_guests_per_line_items_leg(leg_line_items) all_line_items.concat(leg_line_items) current_leg_date = leg_line_items.last.to_date.to_date if leg_line_items.any? end all_line_items end |
#build_parent_stops_map(legs) ⇒ Object
Builds a mapping of leg trip_id => { sequence:, offset_days: } from the parent trip’s branch stops. For direct trips (no main_trip_id), returns an empty hash (falls back to date chaining).
Example: parent trip 100 has branch stops pointing to child trips:
TripStop(trip_id: 100, board_to_trip_id: 200, sequence: 1, offset_days: 0)
TripStop(trip_id: 100, board_to_trip_id: 201, sequence: 2, offset_days: 1)
Result: { 200 => { sequence: 1, offset_days: 0 },
201 => { sequence: 2, offset_days: 1 } }
105 106 107 108 109 110 111 112 113 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 105 def build_parent_stops_map(legs) parent_trip_id = legs.first&.main_trip_id return {} if parent_trip_id.blank? SpreeCmCommissioner::TripStop .where(trip_id: parent_trip_id, board_to_trip_id: legs.map(&:trip_id)) .pluck(:board_to_trip_id, :sequence, :offset_days) .to_h { |trip_id, sequence, offset_days| [trip_id, { sequence: sequence, offset_days: offset_days }] } end |
#calculate_line_item_duration!(date:, departure_time:, arrival_time:, departure_offset_days: 0, arrival_offset_days: 0) ⇒ Object
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 237 def calculate_line_item_duration!(date:, departure_time:, arrival_time:, departure_offset_days: 0, arrival_offset_days: 0) raise StandardError, 'Departure or arrival time in trip stop is missing' if departure_time.blank? || arrival_time.blank? dep_offset = departure_offset_days.to_i arr_offset = arrival_offset_days.to_i # Build exact departure datetime using departure offset dep_date = date + dep_offset.days from_date = Time.zone.local(dep_date.year, dep_date.month, dep_date.day, departure_time.hour, departure_time.min, departure_time.sec) # First, trust the actual time-of-day/datetime difference stored in the stop record. # This handles the common case where `arrival_time` already rolls to the next day. duration_seconds = arrival_time - departure_time to_date = from_date + duration_seconds.seconds # If arrival_time is still earlier than departure_time (clock-only mismatch), # correct using the provided offset-days difference. to_date += (arr_offset - dep_offset).days if to_date < from_date raise StandardError, 'Arrival time cannot be before departure time' if to_date < from_date [from_date, to_date] end |
#call(outbound_date:, inbound_date:, outbound_legs:, inbound_legs: [], user: nil, include_return: false) ⇒ Object
rubocop:disable Metrics/ParameterLists
19 20 21 22 23 24 25 26 27 28 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 19 def call(outbound_date:, inbound_date:, outbound_legs:, inbound_legs: [], user: nil, include_return: false) # rubocop:disable Metrics/ParameterLists return failure(nil, 'Outbound legs are missing') if outbound_legs.blank? begin order = create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, include_return) success(order: order) rescue StandardError => e failure(nil, e.) end end |
#create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, include_return) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 30 def create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, include_return) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists order = Spree::Order.new(state: 'cart', use_billing: true, user: user) all_legs = outbound_legs + inbound_legs preload_trips_and_stops!(all_legs) outbound_line_items = build_line_items_for_legs!(order: order, legs: outbound_legs, initial_date: outbound_date) inbound_line_items = inbound_legs.blank? ? [] : build_line_items_for_legs!(order: order, legs: inbound_legs, initial_date: inbound_date) # For connected trips, sum of quantities will be (passengers * legs). # We need to ensure the number of passengers per leg matching between outbound and inbound. outbound_qty_per_leg = outbound_legs.any? ? outbound_line_items.sum(&:quantity) / outbound_legs.size : 0 inbound_qty_per_leg = inbound_legs.any? ? inbound_line_items.sum(&:quantity) / inbound_legs.size : 0 if inbound_line_items.any? && outbound_qty_per_leg != inbound_qty_per_leg raise StandardError, "Outbound & inbound passenger counts do not match (#{outbound_qty_per_leg} vs #{inbound_qty_per_leg})" end add_return_line_item!(order, outbound_legs, outbound_qty_per_leg, outbound_date) if include_return order.preload_trip_ids = (outbound_legs.map(&:trip_id) + inbound_legs.map(&:trip_id)).flatten.uniq order.preload_main_trip_ids = (outbound_legs.map(&:main_trip_id) + inbound_legs.map(&:main_trip_id)).flatten.compact.uniq ActiveRecord::Base.transaction do raise StandardError, order.errors..to_sentence unless order.save order.update_with_updater! # Move the order from 'cart' to 'address' & hold the selected seats. order.next! end order end |
#insert_saved_guests_per_line_items_leg(line_items) ⇒ Object
226 227 228 229 230 231 232 233 234 235 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 226 def insert_saved_guests_per_line_items_leg(line_items) line_items.flat_map(&:guests).each_with_index do |guest, index| @saved_guests ||= [] @saved_guests << SpreeCmCommissioner::SavedGuest.new if @saved_guests[index].blank? guest.saved_guest = @saved_guests[index] end line_items end |
#preload_trips_and_stops!(legs) ⇒ Object
Preloads all trips and their relevant trip stops in 2 queries total, avoiding N+1 queries in build_line_items_for! and resolve_leg_dates.
117 118 119 120 121 122 123 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 117 def preload_trips_and_stops!(legs) trip_ids = legs.map(&:trip_id).uniq @preloaded_trips = SpreeCmCommissioner::Trip.where(id: trip_ids).index_by(&:id) stop_ids = legs.flat_map { |l| [l.boarding_trip_stop_id, l.drop_off_trip_stop_id] }.compact.uniq @preloaded_trip_stops = SpreeCmCommissioner::TripStop.where(id: stop_ids).index_by(&:id) end |
#resolve_leg_dates(trip, leg, date) ⇒ Object
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 195 def resolve_leg_dates(trip, leg, date) boarding_stop = @preloaded_trip_stops[leg.boarding_trip_stop_id] if leg.boarding_trip_stop_id.present? drop_off_stop = @preloaded_trip_stops[leg.drop_off_trip_stop_id] if leg.drop_off_trip_stop_id.present? # Fallback to querying trip stops if IDs were not provided boarding_stop ||= trip.trip_stops.first drop_off_stop ||= trip.trip_stops.last calculate_line_item_duration!( date: date, departure_time: boarding_stop&.departure_time, arrival_time: drop_off_stop&.arrival_time, departure_offset_days: boarding_stop&.offset_days.to_i, arrival_offset_days: drop_off_stop&.offset_days.to_i ) end |
#validity_window(addon, outbound_date) ⇒ Object
- valid_from, valid_until
-
for the entitlement. Returns nils when no outbound date is given,
leaving the entitlement non-expiring rather than guessing a window.
188 189 190 191 192 193 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 188 def validity_window(addon, outbound_date) return [nil, nil] if outbound_date.blank? from_date = outbound_date.in_time_zone.beginning_of_day [from_date, from_date + addon.open_dated_validity_days.days] end |