spree_cm_commissioner 2.12.5 → 2.12.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0228f6ca7b7b92d005af33e616b862a1ca576c9320b70510c7f235fbb058848
4
- data.tar.gz: 66e3e4f595da2175c3fd0b06ca0c5712bb340b3fb8b9e1e1f71cd1495559dda9
3
+ metadata.gz: ed84dcf2382e8d3720366acd62551189adfa4edba9533d8ca5e94b0704942836
4
+ data.tar.gz: aa37bf6b43593f7f7ff91d71d1839b1ceeeb8c9271d5db371451f94958399366
5
5
  SHA512:
6
- metadata.gz: 3a21bc63b5229c2ab198fa5d1d342b8375ed069d9a4cbee964243310a840a99acf9ab63b426defeca77b8f1f5511fcfa4fa8e5703c72fe580c06d53834452961
7
- data.tar.gz: 71f6bd10a265ab1dd8c6ed0b30972105f3d2cfa332a2da3b6dfa347c8899eb1e97bb9962a6ff1c831b5cc3c6e3209409c435154e6cdc4f92a162191990bd2263
6
+ metadata.gz: a5b89052a82aedeed1144cf9a5892e3d8ec2428c3a4cd26cc5806010c1bbcc3ad353a768e6854c3fdaf69a2ae2fdb5f6f049616b7cac1b2a9d3cd532344708d3
7
+ data.tar.gz: 7842ce22211f5e072e308efda5f4f5a0b95625729d18388ec6c3c22406fd76b4e5577daca6d5a9f717fd28990639d3897b3a0d58c1d18bebd2c1959f152b7b80
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.5)
37
+ spree_cm_commissioner (2.12.6)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -20,8 +20,6 @@ module Spree
20
20
  # Off `collection`, which ResourceController already scoped with accessible_by(current_ability)
21
21
  # — going through model_class directly would discard that.
22
22
  @private_sales = collection.order(created_at: :desc).page(params[:page]).per(25)
23
-
24
- load_audience_names
25
23
  end
26
24
 
27
25
  # PATCH /admin/private_sales/:id/revoke
@@ -38,19 +36,6 @@ module Spree
38
36
 
39
37
  private
40
38
 
41
- # Names for every product, user and role named by the sales ON THIS PAGE, in three queries.
42
- # The ids live in jsonb rather than join tables, so there is no association for `includes` to
43
- # preload — asking each row for its own would be 3 queries per row.
44
- def load_audience_names
45
- sales = @private_sales.to_a
46
-
47
- @product_names = Spree::Product.where(id: sales.flat_map(&:product_ids)).pluck(:id, :name).to_h
48
- @user_emails = Spree::User.where(id: sales.flat_map(&:user_ids)).pluck(:id, :email).to_h
49
- @role_names = Spree::Role.where(id: sales.flat_map(&:role_ids))
50
- .pluck(:id, :presentation, :name)
51
- .to_h { |id, presentation, name| [id, presentation.presence || name] }
52
- end
53
-
54
39
  # Straight to edit rather than back to the list: a sale is not usable until stock is withheld
55
40
  # for it, and edit is where that happens. Landing on the index would hide the next step.
56
41
  # non_vendor: a private sale grants access across the store, and this repo's roles include
@@ -15,7 +15,13 @@ module SpreeCmCommissioner
15
15
  capacity = inventory_item.max_capacity.to_i
16
16
  return nil if capacity.zero?
17
17
 
18
- (inventory_item.quantity_available.to_f / capacity * 100).round
18
+ percentage = inventory_item.quantity_available.to_f / capacity * 100
19
+ return percentage.round if percentage <= 0 || percentage >= 1
20
+
21
+ # A few seats left out of a large capacity is a fraction of a percent but is NOT sold out;
22
+ # rounding it whole would print 0% and read as exactly that. Two decimals, floored at 0.01 so
23
+ # capacities past 10k still show something non-zero.
24
+ [percentage.round(2), 0.01].max
19
25
  end
20
26
 
21
27
  # High/medium/low as risk of running out of public stock, not as a grade — a bare number in a
@@ -123,11 +123,20 @@ module SpreeCmCommissioner
123
123
  # table-unqualified `available_on`/`discontinue_on` strings below become ambiguous between
124
124
  # `spree_products` and `spree_variants` and the whole query raises `PG::AmbiguousColumn`. A
125
125
  # plain status check needs no join at all, so it can't collide with these.
126
+ #
127
+ # `event_id` alone isn't enough to tell "belongs to this event" from "organizer unlisted it
128
+ # from its section but hasn't moved it off the event" — those look identical on the product
129
+ # row. `children_classifications` (Classification rows for the event's section children,
130
+ # taxon_decorator.rb) carries the actual unlist toggle: `visible: false` is how an organizer
131
+ # hides a product from a section without touching `event_id`, same flag
132
+ # `Products::FindDecorator` already checks for storefront listings, so an unlisted product
133
+ # is excluded here too rather than still getting a live stock-status entry in Firestore.
126
134
  def active_products(taxon)
127
135
  now = Time.zone.now
128
136
  taxon.event_products.ecommerce.where(status: 'active')
129
137
  .where('available_on IS NULL OR available_on <= ?', now)
130
138
  .where('discontinue_on IS NULL OR discontinue_on > ?', now)
139
+ .where(id: taxon.children_classifications.where(visible: true).select(:product_id))
131
140
  end
132
141
 
133
142
  # Sums, per product id, of the live Redis-sourced quantities for every active inventory item
@@ -92,23 +92,38 @@ module SpreeCmCommissioner
92
92
  end
93
93
 
94
94
  # Live count of guests no longer waiting: granted entry (allow_to_enter_room_at set) or left
95
- # the queue (left_at set). Both matter because #stamp_doc's position is permanent once
96
- # assigned — e.g. if the highest stamped position is 4200 but this only counts 3714, every
97
- # guest stamped after 3714 shows ~486 phantom people "ahead of them" that never resolve.
98
- # Summed as two single-field counts rather than one OR query so each stays index-free (no
99
- # composite index needed), and #waiting_query / WaitingGuestsCaller#eligible_guests_in never
100
- # need to know about `left_at`. Recomputed fresh every run rather than incremented, so a
101
- # crashed run can't leave it permanently understated.
95
+ # the queue without ever being granted (left_at set, allow_to_enter_room_at still nil). Both
96
+ # matter because #stamp_doc's position is permanent once assigned — e.g. if the highest
97
+ # stamped position is 4200 but this only counts 3714, every guest stamped after 3714 shows
98
+ # ~486 phantom people "ahead of them" that never resolve.
99
+ #
100
+ # allow_to_enter_room_at and left_at are not mutually exclusive: rejoinQueueAfterExpiry()
101
+ # sets left_at on a doc that was already granted (session renewal failed past the expiry
102
+ # ceiling after entry), so a doc can carry both fields non-nil at once. #allowed_count is
103
+ # unconditional on left_at so it always claims that doc; #left_without_entry_count excludes
104
+ # anything #allowed_count already claimed (allow_to_enter_room_at == nil) so the two counts
105
+ # partition every exited doc into exactly one bucket — summing plain `left_at != nil` here
106
+ # instead would double-count that doc, same as summing plain `allow_to_enter_room_at == nil`
107
+ # without excluding it here would drop the doc from both counts entirely.
108
+ #
109
+ # Two counts rather than one OR query so each stays index-light — #allowed_count needs no
110
+ # composite index, #left_without_entry_count needs only a two-field one (allow_to_enter_room_at
111
+ # ==, left_at !=) — and #waiting_query / WaitingGuestsCaller#eligible_guests_in never need to
112
+ # know about `left_at`. Recomputed fresh every run rather than incremented, so a crashed run
113
+ # can't leave it permanently understated.
102
114
  def exited_count(path)
103
- allowed_count(path) + left_count(path)
115
+ allowed_count(path) + left_without_entry_count(path)
104
116
  end
105
117
 
106
118
  def allowed_count(path)
107
119
  firestore.col(path).where('allow_to_enter_room_at', '!=', nil).aggregate_query.add_count.get.first.get.to_i
108
120
  end
109
121
 
110
- def left_count(path)
111
- firestore.col(path).where('left_at', '!=', nil).aggregate_query.add_count.get.first.get.to_i
122
+ def left_without_entry_count(path)
123
+ firestore.col(path)
124
+ .where('allow_to_enter_room_at', '==', nil)
125
+ .where('left_at', '!=', nil)
126
+ .aggregate_query.add_count.get.first.get.to_i
112
127
  end
113
128
 
114
129
  # merge: true so total_exited_queue coexists with the fields WaitingGuestsCaller publishes to
@@ -31,6 +31,7 @@
31
31
  <th><%= Spree.t(:variant) %></th>
32
32
  <th class="text-center"><%= t('.max_capacity') %></th>
33
33
  <th class="text-center"><%= t('.quantity_available') %></th>
34
+ <th class="text-center"><%= t('.quantity_on_hold') %></th>
34
35
  <th class="text-center"><%= t('.quantity_locked') %></th>
35
36
  <th class="text-center"><%= t('.quantity_locked_on_hold') %></th>
36
37
  <th class="text-center"><%= t('.lock_release') %></th>
@@ -43,7 +44,7 @@
43
44
  <% next if product.nil? %>
44
45
 
45
46
  <tr class="bg-light">
46
- <td colspan="6"><strong><%= product.name %></strong></td>
47
+ <td colspan="7"><strong><%= product.name %></strong></td>
47
48
  <td class="text-right">
48
49
  <%# Own column so these line up under the same x-position for every product, rather than
49
50
  sharing space with the name — icon-only, matching the with-tip pattern used for row
@@ -69,12 +70,13 @@
69
70
  <% if item.nil? %>
70
71
  <%# No undated row means this product's stock is per date — hundreds of rows a single
71
72
  input cannot express. Those are managed on the product's own screen. %>
72
- <td colspan="6" class="text-center text-muted">
73
+ <td colspan="7" class="text-center text-muted">
73
74
  <%= link_to t('.per_date_stock'), admin_product_inventory_locks_path(product) %>
74
75
  </td>
75
76
  <% else %>
76
77
  <td class="text-center text-muted"><%= item.max_capacity %></td>
77
78
  <td class="text-center"><%= render partial: 'spree/admin/shared/availability_percentage', locals: { inventory_item: item } %></td>
79
+ <td class="text-center"><%= item.quantity_on_hold %></td>
78
80
  <td class="text-center"><strong><%= item.quantity_locked %></strong></td>
79
81
  <td class="text-center"><%= item.quantity_locked_on_hold %></td>
80
82
  <td class="text-center">
@@ -84,7 +86,7 @@
84
86
  <div class="input-group input-group-sm">
85
87
  <%= number_field_tag :quantity, nil, class: 'form-control text-center p-0', id: nil %>
86
88
  <div class="input-group-append">
87
- <%= button_tag(class: 'btn btn-outline-success pl-2 pr-1') do %>
89
+ <%= button_tag(class: 'btn btn-outline-success pl-2 pr-1', data: { confirm: t('.lock_release_confirm') }) do %>
88
90
  <%= svg_icon(name: 'arrow-left-right.svg', classes: 'icon', width: 14, height: 14) %>
89
91
  <% end %>
90
92
  </div>
@@ -14,10 +14,10 @@
14
14
  <thead class="text-muted">
15
15
  <tr>
16
16
  <th><%= Spree.t(:name) %></th>
17
- <th><%= t('.products') %></th>
18
- <th><%= t('.audience') %></th>
19
17
  <th><%= t('.expires_at') %></th>
20
18
  <th><%= Spree.t(:status) %></th>
19
+ <th><%= Spree.t(:created_at) %></th>
20
+ <th><%= t('.updated_at') %></th>
21
21
  <th class="text-right"></th>
22
22
  </tr>
23
23
  </thead>
@@ -27,18 +27,6 @@
27
27
  answer, and a vanished row cannot give it. %>
28
28
  <tr class="<%= 'text-muted' unless private_sale.usable? %>">
29
29
  <td><%= private_sale.name %></td>
30
- <%# Names come from the page-wide lookups built in the controller — asking each row for
31
- its own products/users/roles is 3 queries per row. %>
32
- <td><%= private_sale.product_ids.filter_map { |id| @product_names[id] }.join(', ').presence || '—' %></td>
33
- <td>
34
- <% if private_sale.open_to_anyone? %>
35
- <%= t('.anyone') %>
36
- <% else %>
37
- <%= [private_sale.user_ids.filter_map { |id| @user_emails[id] }.presence&.join(', '),
38
- private_sale.role_ids.filter_map { |id| @role_names[id] }.presence&.join(', ')]
39
- .compact.join(' · ') %>
40
- <% end %>
41
- </td>
42
30
  <td><%= private_sale.expires_at.to_date %></td>
43
31
  <td>
44
32
  <% if private_sale.revoked? %>
@@ -51,6 +39,8 @@
51
39
  <span class="badge badge-success"><%= t('.active') %></span>
52
40
  <% end %>
53
41
  </td>
42
+ <td><%= l(private_sale.created_at) %></td>
43
+ <td><%= l(private_sale.updated_at) %></td>
54
44
  <td class="text-right">
55
45
  <% if private_sale.usable? %>
56
46
  <div class="d-inline-block align-middle" style="width: 280px;">
@@ -559,10 +559,8 @@ en:
559
559
  index:
560
560
  title: "Private Sales"
561
561
  new: "New Private Sale"
562
- products: "Products"
563
- audience: "Who can buy"
564
562
  expires_at: "Expires"
565
- anyone: "Anyone with the link"
563
+ updated_at: "Updated"
566
564
  active: "Active"
567
565
  revoked: "Revoked"
568
566
  expired: "Expired"
@@ -593,9 +591,11 @@ en:
593
591
  no_products: "Pick at least one product above and save, then set how much to hold back."
594
592
  max_capacity: "Max capacity"
595
593
  quantity_available: "Quantity available"
594
+ quantity_on_hold: "Quantity on hold"
596
595
  quantity_locked: "Quantity locked"
597
596
  quantity_locked_on_hold: "Quantity locked on hold"
598
597
  lock_release: "Lock (+) / Release (−)"
598
+ lock_release_confirm: "Apply this change to locked stock? This takes effect immediately."
599
599
  manage_product_stock: "Product stock"
600
600
  lock_history: "Lock history"
601
601
  per_date_stock: "This product's stock is per date — manage it on the product"
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.12.5'.freeze
2
+ VERSION = '2.12.6'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.5
4
+ version: 2.12.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-01 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree