Class: SpreeCmCommissioner::Seats::LockState
- Inherits:
-
Object
- Object
- SpreeCmCommissioner::Seats::LockState
- 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
-
#selections ⇒ Object
readonly
Returns the value of attribute selections.
Instance Method Summary collapse
-
#dates_by_variant ⇒ Object
Selected dates grouped by variant.
-
#fully_locked_block_ids ⇒ Object
Blocks locked on EVERY selected date (:locked only, excluding :partial).
-
#initialize(selections:) ⇒ LockState
constructor
selections: array of [variant_id, Date] pairs, parsed from the calendar's "variant_id:date" keys.
-
#inventory_items ⇒ Object
Inventory items for the exact (variant, date) pairs selected -- not the cross product, which would count dates the operator never chose for that variant.
-
#locked_block_ids ⇒ Object
Blocks carrying at least one lock somewhere in the selection AND not occupied anywhere in it -- i.e.
- #locked_count_for(block) ⇒ Object
- #state_for(block) ⇒ Object
- #total_count_for(block) ⇒ Object
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
#selections ⇒ Object (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_variant ⇒ Object
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_ids ⇒ Object
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_items ⇒ Object
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_ids ⇒ Object
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 |