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
- #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, order_options: []) ⇒ Object
rubocop:disable Metrics/ParameterLists.
-
#create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, order_options = []) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/ParameterLists.
- #insert_saved_guests_per_line_items_leg(line_items) ⇒ Object
- #open_dated_leg_dates(trip, _leg, date) ⇒ 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
Instance Method Details
#build_guests_for!(line_item, seat_selections) ⇒ Object
172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 172 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
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 118 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), public_metadata: { is_open_dated: trip.open_dated? }, 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)
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 67 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 } }
98 99 100 101 102 103 104 105 106 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 98 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
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 197 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, order_options: []) ⇒ 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, order_options: []) # 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, ) success(order: order) rescue StandardError => e failure(nil, e.) end end |
#create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, order_options = []) ⇒ 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 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 30 def create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, = []) # 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 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 order. = if .present? raise StandardError, order.errors..to_sentence unless order.save order.update_with_updater! order end |
#insert_saved_guests_per_line_items_leg(line_items) ⇒ Object
186 187 188 189 190 191 192 193 194 195 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 186 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 |
#open_dated_leg_dates(trip, _leg, date) ⇒ Object
165 166 167 168 169 170 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 165 def open_dated_leg_dates(trip, _leg, date) validity_days = trip.product.open_dated_validity_days from_date = date.in_time_zone.beginning_of_day to_date = from_date + validity_days.days [from_date, to_date] 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.
110 111 112 113 114 115 116 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 110 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
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 146 def resolve_leg_dates(trip, leg, date) return open_dated_leg_dates(trip, leg, date) if trip.open_dated? 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 |