class SpreeCmCommissioner::TripDistanceCalculator

  • distance_km, directions_url, ordered_points, estimated_time_minutes (when optimize true)
    - base_km, detour_pickup_km, detour_dropoff_km: Floats or nil
    - payload: Hash (compatible with previous controller response)
    Outputs via context on success:
    - boundary_km: Numeric (optional, default 0)
    - base_price_usd: Numeric
    - dropoff_oob_confirmed: Boolean (optional)
    - pickup_oob_confirmed: Boolean (optional)
    - dropoffs: Array<{ lat:, lng: }> (optional)
    - pickups: Array<{ lat:, lng: }> (optional)
    - configured_destination: { lat:, lng: }
    - origin: { lat:, lng: }
    Inputs via context:
    Computes trip distance details and pricing payload.

def assign_context(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, payload)

def assign_context(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, payload)
  context.payload = payload
  context.distance_km = details_ctx.distance_km
  context.directions_url = details_ctx.directions_url
  context.ordered_points = details_ctx.ordered_points
  context.estimated_time_minutes = details_ctx.estimated_time_minutes
  context.base_km = base_km
  context.detour_pickup_km = detour_pickup_km
  context.detour_dropoff_km = detour_dropoff_km
end

def build_payload(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, data)

def build_payload(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, data)
  {
    distance_km: details_ctx.distance_km,
    directions_url: details_ctx.directions_url,
    ordered_points: details_ctx.ordered_points,
    estimated_time_minutes: details_ctx.estimated_time_minutes,
    base_price_usd: base_price_usd.to_f,
    total_price_usd: data[:total_price_usd],
    extra_pickup_km: data[:extra_pickup_km],
    extra_dropoff_km: data[:extra_dropoff_km],
    extra_pickup_charge_usd: data[:extra_pickup_charge_usd],
    extra_dropoff_charge_usd: data[:extra_dropoff_charge_usd],
    extra_charge_usd: data[:extra_charge_usd],
    base_km: base_km,
    detour_pickup_km: detour_pickup_km,
    detour_dropoff_km: detour_dropoff_km
  }
end

def call

def call
  validate_inputs
  pickups_points, dropoffs_points, final_destination = prepare_points
  base_km, detour_pickup_km, detour_dropoff_km = compute_route_kms(pickups_points, dropoffs_points, final_destination)
  details_ctx = fetch_details(final_destination, pickups_points, dropoffs_points)
  extra_data = compute_extra_data(base_km, detour_pickup_km, detour_dropoff_km)
  payload = build_payload(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, extra_data)
  assign_context(details_ctx, base_km, detour_pickup_km, detour_dropoff_km, payload)
end

def charge_for_extra_km(extra_km)

def charge_for_extra_km(extra_km)
  x = extra_km.to_f
  return 0.0 if x <= 0
  return 2.0 if x <= 5.0
  return 5.0 if x <= 10.0
  return 10.0 if x <= 20.0
  (10.0 + ((x - 20.0) * 0.5)).round(2)
end

def compute_extra_data(base_km, detour_pickup_km, detour_dropoff_km)

def compute_extra_data(base_km, detour_pickup_km, detour_dropoff_km)
  extra_pickup_km = compute_extra_km(detour_pickup_km, base_km, pickup_oob_confirmed)
  extra_dropoff_km = compute_extra_km(detour_dropoff_km, base_km, dropoff_oob_confirmed)
  extra_pickup_charge_usd = charge_for_extra_km(extra_pickup_km)
  extra_dropoff_charge_usd = charge_for_extra_km(extra_dropoff_km)
  extra_charge_usd = (extra_pickup_charge_usd + extra_dropoff_charge_usd).round(2)
  total_price_usd = (base_price_usd.to_f + extra_charge_usd).round(2)
  {
    extra_pickup_km: extra_pickup_km,
    extra_dropoff_km: extra_dropoff_km,
    extra_pickup_charge_usd: extra_pickup_charge_usd,
    extra_dropoff_charge_usd: extra_dropoff_charge_usd,
    total_price_usd: total_price_usd
  }
end

def compute_extra_km(detour_km, base_km, confirmed)

def compute_extra_km(detour_km, base_km, confirmed)
  return 0.0 unless confirmed && detour_km && base_km
  [(detour_km - base_km - boundary_km), 0.0].max
end

def compute_km(origin:, destination:, pickups: nil, dropoffs: nil, optimize: nil)

def compute_km(origin:, destination:, pickups: nil, dropoffs: nil, optimize: nil)
  ctx = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
    origin: origin,
    destination: destination,
    pickups: pickups,
    dropoffs: dropoffs,
    optimize: optimize
  )
  return nil unless ctx.success?
  ctx.distance_km
end

def compute_route_kms(pickups_points, dropoffs_points, final_destination)

def compute_route_kms(pickups_points, dropoffs_points, final_destination)
  base_km = compute_km(origin: origin, destination: configured_destination)
  detour_pickup_km =
    if pickups_points.any?
      compute_km(
        origin: origin,
        destination: configured_destination,
        pickups: pickups_points,
        optimize: false
      )
    end
  detour_dropoff_km =
    if dropoffs_points.any?
      compute_km(
        origin: origin,
        destination: final_destination,
        dropoffs: dropoffs_points,
        optimize: false
      )
    end
  [base_km, detour_pickup_km, detour_dropoff_km]
end

def fetch_details(final_destination, pickups_points, dropoffs_points)

def fetch_details(final_destination, pickups_points, dropoffs_points)
  ctx = SpreeCmCommissioner::GoogleRoutesDistanceCalculator.call(
    origin: origin,
    destination: final_destination,
    pickups: pickups_points,
    dropoffs: dropoffs_points,
    optimize: true
  )
  context.fail!(message: ctx.message || 'Unable to calculate') unless ctx.success?
  ctx
end

def prepare_points

def prepare_points
  pickups_points = Array(pickups).compact
  dropoffs_points = Array(dropoffs).compact
  final_destination = dropoffs_points.last || configured_destination
  [pickups_points, dropoffs_points, final_destination]
end

def validate_inputs

def validate_inputs
  context.fail!(message: 'origin is required') if origin.blank?
  context.fail!(message: 'configured_destination is required') if configured_destination.blank?
end