Class: SpreeCmCommissioner::TransitOrder::Create

Inherits:
Object
  • Object
show all
Includes:
Spree::ServiceModule::Base
Defined in:
app/services/spree_cm_commissioner/transit_order/create.rb

Instance Method Summary collapse

Instance Method Details

#build_guests_for!(line_item, seat_selections) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 178

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



124
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 124

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)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 73

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 } }


104
105
106
107
108
109
110
111
112
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 104

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

Raises:

  • (StandardError)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 203

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, order_options)
    success(order: order)
  rescue StandardError => e
    failure(nil, e.message)
  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
57
58
59
60
61
62
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 30

def create_order!(outbound_date, inbound_date, outbound_legs, inbound_legs, user, order_options = []) # 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.order_options = order_options if order_options.present?

  ActiveRecord::Base.transaction do
    raise StandardError, order.errors.full_messages.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



192
193
194
195
196
197
198
199
200
201
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 192

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



171
172
173
174
175
176
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 171

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.



116
117
118
119
120
121
122
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 116

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



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'app/services/spree_cm_commissioner/transit_order/create.rb', line 152

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