bikeindex/bike_index

View on GitHub
app/models/b_param.rb

Summary

Maintainability
C
1 day
Test Coverage
B
84%
# b_param stands for Bike param
class BParam < ApplicationRecord
  mount_uploader :image, ImageUploader
  store_in_background :image, CarrierWaveStoreWorker

  # serialize :params
  serialize :bike_errors

  belongs_to :created_bike, class_name: "Bike"
  belongs_to :creator, class_name: "User"
  belongs_to :organization

  scope :with_bike, -> { where.not(created_bike_id: nil) }
  scope :without_bike, -> { where(created_bike_id: nil) }
  scope :without_creator, -> { where(creator_id: nil) }
  scope :partial_registrations, -> { where(origin: "embed_partial") }
  scope :bike_params, -> { where("(params -> 'bike') IS NOT NULL") }
  scope :unprocessed_image, -> { where(image_processed: false).where.not(image: nil) }

  before_create :generate_id_token
  before_save :clean_params

  def self.v2_params(hash)
    h = hash["bike"].present? ? hash : {"bike" => hash.with_indifferent_access}
    # Only assign if the key hasn't been assigned - since it's boolean, can't use conditional assignment
    h["bike"]["serial_number"] = h["bike"].delete "serial" if h["bike"].key?("serial")
    h["bike"]["send_email"] = !(h["bike"].delete "no_notify") unless h["bike"].key?("send_email")
    if h["bike"].key?("owner_email_is_phone_number")
      h["bike"]["is_phone"] = InputNormalizer.boolean(h["bike"].delete("owner_email_is_phone_number"))
    end
    org = Organization.friendly_find(h["bike"].delete("organization_slug"))
    h["bike"]["creation_organization_id"] = org.id if org.present?
    # Move un-nested params outside of bike
    %w[test id components].each { |k| h[k] = h["bike"].delete(k) if h["bike"].key?(k) }
    stolen_attrs = h["bike"].delete "stolen_record"
    if stolen_attrs.present? && stolen_attrs.delete_if { |k, v| v.blank? } && stolen_attrs.keys.any?
      h["stolen_record"] = stolen_attrs
      h["stolen_record"]["street"] = h["stolen_record"].delete("address") if h["stolen_record"]["address"].present?
    end
    h
  end

  def self.find_or_new_from_token(toke = nil, user_id: nil, organization_id: nil, bike_sticker: nil)
    b = where(creator_id: user_id, id_token: toke).first if toke.present? && user_id.present?
    b ||= with_organization_or_no_creator(toke)
    b ||= BParam.new(creator_id: user_id, params: {revised_new: true}.as_json)
    b.creator_id ||= user_id
    if bike_sticker.present?
      b.origin = "sticker"
      b.params["bike"] = b.bike.merge("bike_sticker" => bike_sticker.pretty_code)
      organization_id = bike_sticker.organization_id if bike_sticker.organization_id.present?
    end
    # If the org_id is present, add it to the params. Only save it if the b_param is created_at
    if organization_id.present? && b.creation_organization_id != organization_id
      b.params = b.params.merge("bike" => b.bike.merge("creation_organization_id" => organization_id))
      b.update_attribute :params, b.params if b.id.present?
    end
    # Assign the correct user if user is part of the org (for embed submissions)
    if b.creation_organization_id.present? && b.creator_id != user_id
      if Membership.where(user_id: user_id, organization_id: b.creation_organization_id).present?
        b.update_attribute :creator_id, user_id
      end
    end
    b
  end

  # Because organization embed bikes might not match the creator
  def self.with_organization_or_no_creator(toke)
    without_bike.where("created_at >= ?", Time.current - 1.month).where(id_token: toke)
      .detect { |b| b.creator_id.blank? || b.creation_organization_id.present? || b.params["creation_organization_id"].present? }
  end

  # Attrs that need to be skipped on bike assignment
  def self.skipped_bike_attrs
    # Previously, assigned stolen & abandoned booleans - now that we don't, we need to drop them - in preexisting bparams
    %w[abandoned accuracy address address_city address_country address_state address_state
      address_zipcode bike_code bike_sticker cycle_type_name cycle_type_slug
      front_gear_type_slug handlebar_type_slug is_bulk is_new is_pos no_duplicate
      rear_gear_type_slug revised_new stolen]
  end

  def self.registration_info_attrs
    # Also uses address_hash to get legacy address attributes
    %w[accuracy bike_code bike_sticker city country organization_affiliation phone state
      street student_id user_name zipcode]
  end

  def self.email_search(str)
    return all unless str.present?
    where("email ilike ?", "%#{str.strip}%")
  end

  # There are URLs out there with stolen=true, and will be forever - so lean in
  # Keywords are - :status, :stolen
  def self.bike_attrs_from_url_params(url_params = {})
    status = url_params[:status]
    if status.present?
      status = "status_#{status}" unless status.start_with?("status_")
      status = "status_impounded" if status == "status_found" # Rename, so we can give pretty URLs to users
      return {status: status} if Bike.statuses.include?(status)
    end
    return {status: "status_stolen"} if InputNormalizer.boolean(url_params[:stolen])
    {}
  end

  def self.top_level_propulsion_type(passed_params)
    throttle = InputNormalizer.boolean(passed_params["propulsion_type_throttle"])
    pedal_assist = InputNormalizer.boolean(passed_params["propulsion_type_pedal_assist"])
    if pedal_assist
      throttle ? "pedal-assist-and-throttle" : "pedal-assist"
    elsif throttle
      "throttle"
    elsif InputNormalizer.boolean(passed_params["propulsion_type_motorized"])
      "motorized"
    end&.to_sym
  end

  # Crazy new shit
  def manufacturer_id=(val)
    params["bike"]["manufacturer_id"] = val
  end

  def creation_organization_id=(val)
    params["bike"]["creation_organization_id"] = val
  end

  def owner_email=(val)
    params["bike"]["owner_email"] = val
  end

  def primary_frame_color_id=(val)
    params["bike"]["primary_frame_color_id"] = val
  end

  def secondary_frame_color_id=(val)
    params["bike"]["secondary_frame_color_id"] = val
  end

  def tertiary_frame_color_id=(val)
    params["bike"]["tertiary_frame_color_id"] = val
  end

  def status=(val)
    params["bike"]["status"] = val
  end

  def with_bike?
    created_bike_id.present?
  end

  # Get it unscoped, because unregistered_bike notifications
  def created_bike
    @created_bike ||= created_bike_id.present? ? Bike.unscoped.find_by_id(created_bike_id) : nil
  end

  def bike
    (params && params["bike"] || {}).with_indifferent_access
  end

  def stolen_attrs
    s_attrs = params["stolen_record"] || {}
    nested_params = params.dig("bike", "stolen_records_attributes")
    if nested_params&.values&.first.is_a?(Hash)
      s_attrs = nested_params.values.reject(&:blank?).last
    end
    # Set the date_stolen if it was passed, if something else didn't already set date_stolen
    date_stolen = params.dig("bike", "date_stolen")
    s_attrs["date_stolen"] ||= date_stolen if date_stolen.present?
    s_attrs.except("phone_no_show", "show_address")
  end

  def impound_attrs
    s_attrs = params["impound_record"] || {}
    nested_params = params.dig("bike", "impound_records_attributes")
    if nested_params&.values&.first.is_a?(Hash)
      s_attrs = nested_params.values.reject(&:blank?).last
    end
    s_attrs
  end

  def registration_info_attrs
    ria = params["bike"]&.slice(*self.class.registration_info_attrs) || {}
    # Include legacy address attributes
    (ria.key?("street") ? ria : ria.merge(address_hash)).reject { |_k, v| v.blank? }.to_h
  end

  def status
    if Bike.statuses.include?(bike["status"])
      # Don't override status with status_with_owner
      return bike["status"] unless bike["status"] == "status_with_owner"
    end
    return "unregistered_parking_notification" if parking_notification_params.present?
    return "status_impounded" if impound_attrs.present?
    return "status_stolen" if stolen_attrs.present? || InputNormalizer.boolean(bike["stolen"])
    "status_with_owner"
  end

  def status_stolen?
    status == "status_stolen"
  end

  def status_abandoned?
    status == "status_abandoned"
  end

  def status_impounded?
    status == "status_impounded"
  end

  def unregistered_parking_notification?
    status == "unregistered_parking_notification"
  end

  def primary_frame_color_id
    bike["primary_frame_color_id"]
  end

  def secondary_frame_color_id
    bike["secondary_frame_color_id"]
  end

  def tertiary_frame_color_id
    bike["tertiary_frame_color_id"]
  end

  def manufacturer_id
    bike["manufacturer_id"]
  end

  def is_pos
    bike["is_pos"] || false
  end

  def is_new
    bike["is_new"] || false
  end

  def bulk_import
    BulkImport.find_by_id(params["bulk_import_id"])
  end

  def pos_kind
    return "lightspeed_pos" if is_pos
    bulk_import&.ascend? ? "ascend_pos" : "no_pos"
  end

  def is_bulk
    bike["is_bulk"] || false
  end

  def no_duplicate?
    bike["no_duplicate"] || false
  end

  def bike_sticker_code
    bike["bike_sticker"].presence || bike["bike_code"].presence
  end

  def phone
    Phonifyer.phonify(params.dig("stolen_record", "phone") || bike["phone"])
  end

  def user_name
    bike["user_name"]
  end

  def creation_organization
    Organization.friendly_find(creation_organization_id)
  end

  def manufacturer
    bike["manufacturer_id"] && Manufacturer.friendly_find(bike["manufacturer_id"])
  end

  def partial_registration?
    origin == "embed_partial"
  end

  def primary_frame_color
    primary_frame_color_id.present? && Color.find(primary_frame_color_id)&.name
  end

  def revised_new?
    params && params["revised_new"]
  end

  def creation_organization_id
    bike && bike["creation_organization_id"] || params && params["creation_organization_id"]
  end

  def owner_email
    bike && bike["owner_email"]
  end

  def skip_email?
    return true if status_impounded? || unregistered_parking_notification?
    send_email = params.dig("bike", "send_email").to_s
    send_email.present? && !InputNormalizer.boolean(send_email)
  end

  def organization_affiliation
    bike["organization_affiliation"]
  end

  def student_id
    bike["student_id"]
  end

  def external_image_urls
    bike["external_image_urls"] || []
  end

  # Deal with the legacy address concerns
  def address(field)
    key = field.gsub(/address_?/, "") # remove 'address' from the key if it's present
    if key.blank? || key == "street" # If looking for street or address, try both street and address
      bike["street"] || bike["address"]
    else
      bike[key] || bike["address_#{key}"]
    end
  end

  def address_hash
    %w[street city zipcode state country].map { |k| [k, address(k)] }.to_h
  end

  # For revised form. If there aren't errors and there is an email, then we don't need to show
  def display_email?
    true unless owner_email.present? && bike_errors.blank?
  end

  # Right now this is a partial update. It's improved from where it was, but it still uses the BikeCreator
  # code for protection. Ideally, we would use the revised merge code to ensure we aren't letting users
  # write illegal things to the bikes
  # args are not named so we can pass in the params
  def clean_params(updated_params = {})
    self.params ||= {bike: {}} # ensure valid json object
    process_image_if_required
    self.params = params.with_indifferent_access.deep_merge(updated_params.with_indifferent_access)
    massage_if_v2
    set_foreign_keys
    self.organization_id = creation_organization_id
    self.email = owner_email
    self
  end

  def massage_if_v2
    self.params = self.class.v2_params(params) if %w[api_v2 api_v3].include?(origin)
    true
  end

  def set_foreign_keys
    return true unless params.present? && bike.present?
    set_wheel_size_key
    set_manufacturer_key
    set_color_keys
    set_cycle_type_key
    set_rear_gear_type_slug if bike["rear_gear_type_slug"].present?
    set_front_gear_type_slug if bike["front_gear_type_slug"].present?
    set_handlebar_type_key
    set_frame_material_key # Even if the value isn't present, since we need to remove the key
  end

  def set_handlebar_type_key
    key = bike["handlebar_type"] || bike["handlebar_type_slug"]
    ht = HandlebarType.friendly_find(key)
    params["bike"]["handlebar_type"] = ht&.slug
    params["bike"].delete("handlebar_type_slug")
  end

  def set_cycle_type_key
    if (key = (bike["cycle_type"] || bike["cycle_type_slug"] || bike["cycle_type_name"]).presence)
      ct = CycleType.friendly_find(key)
      params["bike"]["cycle_type"] = ct&.slug
      params["bike"].delete("cycle_type_slug")
      params["bike"].delete("cycle_type_name")
    end
  end

  def set_wheel_size_key
    if bike.key?("rear_wheel_bsd")
      key = "_wheel_bsd"
    elsif bike["rear_wheel_size"].present?
      key = "_wheel_size"
    else
      return nil
    end
    rbsd = params["bike"].delete("rear#{key}")
    fbsd = params["bike"].delete("front#{key}")
    params["bike"]["rear_wheel_size_id"] = WheelSize.id_for_bsd(rbsd)
    params["bike"]["front_wheel_size_id"] = WheelSize.id_for_bsd(fbsd)
  end

  def set_frame_material_key
    frame_material = FrameMaterial.friendly_find(bike["frame_material_slug"])
    params["bike"]["frame_material"] = frame_material.slug if frame_material.present?
    params["bike"].delete("frame_material_slug")
  end

  def set_manufacturer_key
    return false unless bike.present?
    m = params["bike"].delete("manufacturer")
    m = params["bike"].delete("manufacturer_id") unless m.present?
    return nil unless m.present?
    b_manufacturer = Manufacturer.friendly_find(m)
    unless b_manufacturer.present?
      b_manufacturer = Manufacturer.other
      params["bike"]["manufacturer_other"] = m
    end
    params["bike"]["manufacturer_id"] = b_manufacturer.id
  end

  def set_rear_gear_type_slug
    gear = RearGearType.where(slug: params["bike"].delete("rear_gear_type_slug")).first
    params["bike"]["rear_gear_type_id"] = gear && gear.id
  end

  def set_front_gear_type_slug
    gear = FrontGearType.where(slug: params["bike"].delete("front_gear_type_slug")).first
    params["bike"]["front_gear_type_id"] = gear && gear.id
  end

  def set_color_keys
    %w[
      primary_frame_color
      secondary_frame_color
      tertiary_frame_color
    ].each { |key| set_color_key(key) }
  end

  def set_paint_key(paint_entry)
    return nil unless paint_entry.present?

    paint = Paint.friendly_find(paint_entry)

    if paint.present?
      params["bike"]["paint_id"] = paint.id
    else
      paint = Paint.new(name: paint_entry)
      paint.manufacturer_id = bike["manufacturer_id"] if is_pos
      paint.save
      params["bike"]["paint_id"] = paint.id
      params["bike"]["paint_name"] = paint.name
    end

    unless bike["primary_frame_color_id"].present?
      params["bike"]["primary_frame_color_id"] = if paint.color_id.present?
        paint.color.id
      else
        Color.black.id
      end
    end
  end

  def mnfg_name
    Manufacturer.calculated_mnfg_name(manufacturer, bike["manufacturer_other"])
  end

  def generate_id_token
    self.id_token ||= SecurityTokenizer.new_token
  end

  def parking_notification_params
    return nil unless params["parking_notification"].present?
    attrs = params["parking_notification"].with_indifferent_access
      .slice(:latitude, :longitude, :kind, :internal_notes, :message, :accuracy,
        :use_entered_address, :street, :city, :zipcode, :state_id, :country_id)
    attrs.merge(organization_id: creation_organization_id,
      user_id: creator_id,
      bike_id: created_bike_id,
      use_entered_address: InputNormalizer.boolean(attrs[:use_entered_address]))
  end

  def partial_notification_pre_tracking?
    (created_at || Time.current) < EmailPartialRegistrationWorker::NOTIFICATION_STARTED
  end

  def partial_notification_resends
    return partial_notifications if partial_notification_pre_tracking?
    partial_notifications.offset(1)
  end

  def partial_notifications
    Notification.partial_registration.where(notifiable: self).order(:id)
  end

  # Below here is revised setup

  def safe_bike_attrs(new_attrs)
    # existing bike attrs, overridden with passed attributes
    safe_attrs = bike.merge("status" => status).merge(new_attrs.as_json)
      .select { |_k, v| InputNormalizer.present_or_false?(v) }
      .except(*BParam.skipped_bike_attrs)
      .merge("b_param_id" => id,
        "b_param_id_token" => id_token,
        "creator_id" => creator_id,
        "updator_id" => creator_id)
      .merge(address_hash)
    # propulsion_type_slug safe assigns, verifying against cycle_type (in BikeAttributable)
    propulsion_type = self.class.top_level_propulsion_type(params) ||
      safe_attrs["propulsion_type_slug"] || safe_attrs["propulsion_type"]
    # propulsion_type_slug needs to be the last key in the hash
    safe_attrs.except("propulsion_type", "propulsion_type_slug")
      .merge("propulsion_type_slug" => propulsion_type)
  end

  private

  def process_image_if_required
    return true if image_processed || image.blank?
    ImageAssociatorWorker.perform_in(5.seconds)
    ImageAssociatorWorker.perform_in(1.minutes)
  end

  def set_color_key(key = nil)
    # If the ID is present, remove the non-id param
    if params.dig("bike", "#{key}_id").present?
      params["bike"].delete(key)
      return
    end

    # Set the paint from color param, if in primary_frame_color
    if key == "primary_frame_color"
      paint = params.dig("bike", "color") || params.dig("bike", key)
      color = Color.friendly_find(paint.strip) if paint.present?
      if color.present?
        params["bike"]["#{key}_id"] = color.id
      else
        set_paint_key(paint)
      end
      params["bike"].delete("color")
    end
    # Set the frame_color
    color = params.dig("bike", key).presence && Color.friendly_find(params.dig("bike", key))
    params["bike"]["#{key}_id"] = color.id if color.present?
    params["bike"].delete(key)
  end
end