Module: Models::Kittable

Extended by:
ActiveSupport::Concern
Includes:
Memery
Included in:
Item
Defined in:
app/concerns/models/kittable.rb

Overview

Kit behavior for Item: associations to kit components and parent kits,
kit content strings for display/specs, and validations/callbacks that keep
kits and their components consistent (store presence, box quantities,
weight/dimension consolidation).

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#kit_componentsActiveRecord::Relation<Item>

Component items that make up this kit.

Returns:

  • (ActiveRecord::Relation<Item>)


28
# File 'app/concerns/models/kittable.rb', line 28

has_many :kit_components, through: :kit_target_item_relations, source: :target_item

#kit_components_for_specsActiveRecord::Relation<Item>

Component items included in spec rendering for this kit.

Returns:

  • (ActiveRecord::Relation<Item>)


32
# File 'app/concerns/models/kittable.rb', line 32

has_many :kit_components_for_specs, through: :kit_target_item_relations_for_specs, source: :target_item

#kit_parentsActiveRecord::Relation<Item>

Kits that include this item as a component.

Returns:

  • (ActiveRecord::Relation<Item>)


40
# File 'app/concerns/models/kittable.rb', line 40

has_many :kit_parents, through: :kit_source_item_relations, source: :source_item

#kit_source_item_relationsActiveRecord::Relation<ItemRelation>

Relations from kits that include this item as a component.

Returns:



36
# File 'app/concerns/models/kittable.rb', line 36

has_many :kit_source_item_relations, -> { kit_components }, class_name: 'ItemRelation', foreign_key: 'target_item_id', inverse_of: :target_item, dependent: :destroy

#kit_target_item_relationsActiveRecord::Relation<ItemRelation>

Relations from this kit to its component items.

Returns:



20
# File 'app/concerns/models/kittable.rb', line 20

has_many :kit_target_item_relations, -> { kit_components }, class_name: 'ItemRelation', foreign_key: 'source_item_id', inverse_of: :source_item, dependent: :destroy

#kit_target_item_relations_for_specsActiveRecord::Relation<ItemRelation>

Relations from this kit to component items marked for spec display.

Returns:



24
# File 'app/concerns/models/kittable.rb', line 24

has_many :kit_target_item_relations_for_specs, -> { kit_components_for_specs }, class_name: 'ItemRelation', foreign_key: 'source_item_id', inverse_of: :source_item, dependent: :destroy

Class Method Details

.kitsActiveRecord::Relation<Models::Kittable>

A relation of Models::Kittables that are kits. Active Record Scope

Returns:

See Also:



42
# File 'app/concerns/models/kittable.rb', line 42

scope :kits, -> { where(is_kit: true) }

.non_kitsActiveRecord::Relation<Models::Kittable>

A relation of Models::Kittables that are non kits. Active Record Scope

Returns:

See Also:



43
# File 'app/concerns/models/kittable.rb', line 43

scope :non_kits, -> { where(is_kit: false) }

Instance Method Details

#box_contentsArray<String>

Per-box human-readable contents strings (up to 3 boxes).

Returns:

  • (Array<String>)

    one entry per box, e.g. "1 x SKU-A and 2 x SKU-B";
    empty strings for boxes with no components



116
117
118
# File 'app/concerns/models/kittable.rb', line 116

def box_contents
  Array.new(3) { |i| kit_target_item_relations.filter_map { |ir| ir.box_quantities[i] > 0 ? "#{ir.box_quantities[i]} x #{ir.target_item.sku}" : nil }.to_sentence(last_word_connector: ' and ') }
end

#consolidate_linked_kitsvoid

This method returns an undefined value.

Recalculates and persists weight/dimension rollups on every parent kit.

Runs as an after_update callback when this component's shipping
dimensions changed; delegates each parent kit to Item::KitConsolidator.



90
91
92
# File 'app/concerns/models/kittable.rb', line 90

def consolidate_linked_kits
  kit_parents.each { |kp| Item::KitConsolidator.new(kp).consolidate_weights_and_dimensions.commit }
end

#get_kit_item_idsArray<Integer>

Ids of the kit component items (excluding self).

Returns:

  • (Array<Integer>)


181
182
183
# File 'app/concerns/models/kittable.rb', line 181

def get_kit_item_ids
  get_kit_items.ids
end

#get_kit_items(spec_only: nil) ⇒ ActiveRecord::Relation<Item>

The kit component items, or an empty relation for non-kit items.

Parameters:

  • spec_only (Boolean, nil) (defaults to: nil)

    if true, only components marked for spec display

Returns:

  • (ActiveRecord::Relation<Item>)

    kit components



165
166
167
168
169
# File 'app/concerns/models/kittable.rb', line 165

def get_kit_items(spec_only: nil)
  return Item.none unless is_kit?

  spec_only ? kit_components_for_specs : kit_components
end

#get_multi_box_item_contents_item_id_hash_arrayArray<Hash{String => Integer}>

Per-box component quantities keyed by component item id.

Returns:

  • (Array<Hash{String => Integer}>)

    one hash per box, mapping
    target item id (as string) to quantity in that box



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'app/concerns/models/kittable.rb', line 124

def get_multi_box_item_contents_item_id_hash_array
  contents_item_id_hash_array = []
  boxes.each_with_index do |_box, i|
    contents_hash = {}
    kit_target_item_relations.each do |ir|
      item_id = ir.target_item.id.to_s
      if ir.box_quantities[i] > 0
        contents_hash[item_id] ||= 0
        contents_hash[item_id] += ir.box_quantities[i]
      end
    end
    contents_item_id_hash_array << contents_hash
  end
  contents_item_id_hash_array
end

#get_self_and_kit_item_idsArray<Integer>

Ids of self plus all kit components.

Returns:

  • (Array<Integer>)


174
175
176
# File 'app/concerns/models/kittable.rb', line 174

def get_self_and_kit_item_ids
  get_self_and_kit_items.map(&:id)
end

#get_self_and_kit_items(spec_only: nil) ⇒ Array<Item>

Pulls the item and its kit components. If you specify spec only then only those target items marked include_in_spec will be pulled
This allows greater control of how specs are rendered

Always returns an Array. For non-kit items, returns [self] without a DB query.
For kit items, returns self + kit components ordered by category priority then SKU.
Result is memoized per spec_only variant to avoid N+1 queries when called repeatedly
(e.g. once per ProductSpecification in Item::SpecificationsMasher).

Parameters:

  • spec_only (Boolean, nil) (defaults to: nil)

    If true, only includes kit components marked for spec display

Returns:

  • (Array<Item>)

    Self and kit components (if any)



150
151
152
153
154
155
156
157
158
# File 'app/concerns/models/kittable.rb', line 150

def get_self_and_kit_items(spec_only: nil)
  if is_kit?
    item_ids = [id]
    item_ids += spec_only ? kit_components_for_spec_ids : kit_component_ids
    Item.where(id: item_ids).joins(:product_category).order(Item[:id].not_eq(id), ProductCategory[:priority], Item[:sku]).to_a
  else
    [self]
  end
end

#is_part_of_a_kit?Boolean

Whether this item is a component of at least one kit.

Returns:

  • (Boolean)


56
57
58
# File 'app/concerns/models/kittable.rb', line 56

def is_part_of_a_kit?
  kit_parents.present?
end

#is_part_of_an_active_kit?Boolean

Whether this item is a component of at least one active kit.

Returns:

  • (Boolean)


63
64
65
# File 'app/concerns/models/kittable.rb', line 63

def is_part_of_an_active_kit?
  kit_parents.active.present?
end

#kit_contents(short = false, with_brackets = false) ⇒ String?

Human-readable description of this kit's contents.

Parameters:

  • short (Boolean) (defaults to: false)

    omit the leading "The SKU is" phrase

  • with_brackets (Boolean) (defaults to: false)

    wrap the result in parentheses

Returns:

  • (String, nil)

    e.g. "The KIT-1 is composed of 2 x SKU-A and 1 x SKU-B";
    nil when the item has no kit components



100
101
102
103
104
105
106
107
108
109
110
# File 'app/concerns/models/kittable.rb', line 100

def kit_contents(short = false, with_brackets = false)
  contents = nil
  if kit_target_item_relations.any?
    contents = ''
    contents += "The #{sku} is " unless short
    contents += 'composed of '
    contents += kit_target_item_relations.map { |ir| "#{ir.quantity} x #{ir.target_item.sku}" }.to_sentence(last_word_connector: ' and ')
    contents = "(#{contents})" if with_brackets
  end
  contents
end

#shipping_dimensions_changed?Boolean

Whether any weight or shipping dimension attribute changed on this save.

Returns:

  • (Boolean)


70
71
72
# File 'app/concerns/models/kittable.rb', line 70

def shipping_dimensions_changed?
  base_weight_changed? or shipping_weight_changed? or shipping_length_changed? or shipping_width_changed? or shipping_height_changed?
end

#store_item_for(store_id, location) ⇒ StoreItem

Finds or creates this item's StoreItem for the given store and location.

Parameters:

  • store_id (Integer)

    the store's primary key

  • location (String)

    store location code (e.g. AVAILABLE)

Returns:

  • (StoreItem)

    the found or newly created store item (zeroed-out on create)



79
80
81
# File 'app/concerns/models/kittable.rb', line 79

def store_item_for(store_id, location)
  store_items.where(store_id:, location:).first_or_create(qty_available: 0, is_discontinued: false, unit_cogs: 0, qty_committed: 0)
end