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



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

Raises:

  • (StandardError)


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

Raises:

  • (StandardError)


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, 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?

  raise StandardError, order.errors.full_messages.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