Class: SpreeCmCommissioner::Seats::LockState

Inherits:
Object
  • Object
show all
Defined in:
app/services/spree_cm_commissioner/seats/lock_state.rb

Overview

Aggregate seat state across a multi-date selection: for each seat, one honest answer over the whole selection -- including "locked on some dates but not others", which a single-date view cannot say.

state = Seats::LockState.new(selections: [[512, Date.new(2026, 8, 8)], ...])
state.state_for(block)  # => :locked | :partial | :unavailable | :free

Lives in the gem, not in a presenter, because two callers need the identical answer: the drawer that draws the seat map, and Seats::ApplyLocks which diffs the operator's submission against it. When those two disagreed the operator saw one thing and the server did another.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(selections:) ⇒ LockState

selections: array of [variant_id, Date] pairs, parsed from the calendar's "variant_id:date" keys.



14
15
16
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 14

def initialize(selections:)
  @selections = Array(selections)
end

Instance Attribute Details

#selectionsObject (readonly)

Returns the value of attribute selections.



18
19
20
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 18

def selections
  @selections
end

Instance Method Details

#dates_by_variantObject

Selected dates grouped by variant. Also the shape ApplyLocks needs to call the lock services once per variant.



72
73
74
75
76
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 72

def dates_by_variant
  @dates_by_variant ||= selections.each_with_object({}) do |(variant_id, date), acc|
    (acc[variant_id] ||= []) << date.to_date
  end
end

#fully_locked_block_idsObject

Blocks locked on EVERY selected date (:locked only, excluding :partial). Ticked seats already in this set need no work, so the caller can skip them -- a partially-locked seat is deliberately absent so that ticking it still fills in its missing dates.



63
64
65
66
67
68
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 63

def fully_locked_block_ids
  block_counts.filter_map do |block_id, counts|
    block_id if counts[:occupied].zero? && counts[:locked].positive? &&
      counts[:locked] >= relevant_item_count_by_block_id(block_id)
  end
end

#inventory_itemsObject

Inventory items for the exact (variant, date) pairs selected -- not the cross product, which would count dates the operator never chose for that variant.

One indexed query plus an in-memory pair filter. Previously this OR-chained one relation per selection, which is 62 OR'd clauses for a 31-day, 2-variant selection; this is also the shape BlockDateIteration#inventory_index already uses, so the two paths now ask the same question the same way.



85
86
87
88
89
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 85

def inventory_items
  return @inventory_items if defined?(@inventory_items)

  @inventory_items = load_inventory_items
end

#locked_block_idsObject

Blocks carrying at least one lock somewhere in the selection AND not occupied anywhere in it -- i.e. :locked OR :partial, but never :unavailable.

This is what makes a single "Update" button possible: the submitted checkboxes are the DESIRED state, and the difference against this set tells us what to unlock. Deriving unlocks from here rather than from "every unchecked seat" matters -- the latter would ask UnlockBlocks to release hundreds of seats that were never locked, each coming back as a spurious skip.

The occupied check is NOT optional. A seat locked on date A and separately sold on date B (the normal outcome of a partial-success lock, see Seats::LockBlocks) is :unavailable, and its checkbox is rendered disabled -- the operator has no way to tick or untick it. Without excluding it here, it would still land in locked_block_ids, be absent from every submitted desired, and get diffed into to_unlock on ANY unrelated Update submit -- silently releasing a valid lock on date A that the operator never touched and could not have touched.



56
57
58
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 56

def locked_block_ids
  block_counts.select { |_id, counts| counts[:occupied].zero? && counts[:locked].positive? }.keys
end

#locked_count_for(block) ⇒ Object



34
35
36
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 34

def locked_count_for(block)
  block_counts.dig(block.id, :locked).to_i
end

#state_for(block) ⇒ Object



20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 20

def state_for(block)
  counts = block_counts[block.id]
  return :free if counts.nil?
  # Sold or mid-checkout on any selected date wins: the operator cannot lock it, and showing it as
  # free would invite a lock that the service then skips.
  return :unavailable if counts[:occupied].positive?

  locked = counts[:locked]
  return :free if locked.zero?
  return :locked if locked >= relevant_item_count(block)

  :partial
end

#total_count_for(block) ⇒ Object



38
39
40
# File 'app/services/spree_cm_commissioner/seats/lock_state.rb', line 38

def total_count_for(block)
  relevant_item_count(block)
end