spree_cm_commissioner 2.12.0.pre.pre2 → 2.12.0

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: c7223999a2cd0d551f0694c7cd4103195db0ca0473b7ee577a17be13d6686f5f
4
- data.tar.gz: bf3fa4a68bf67915a77bd37b2cc2fdaa7f05bece78be8101b76c786f9d8daad5
3
+ metadata.gz: 10936d8185cc09d4bd8eca8c3c2626cb9bb113d2e94695030be009f9b38edbcc
4
+ data.tar.gz: 8b1493341611af89e52c06017d1abc3ad6adc838a541dd3ec93dd76877985eaf
5
5
  SHA512:
6
- metadata.gz: 7e4b196cc0f1426f4672f511546509ddcbf41981203f8affb32e26df9c2e2f0d08cefefb2530810405fa8d9cdf15f3a680e6ea78fcf0751b0efe7dba25e7ce8d
7
- data.tar.gz: 9de8a0f14b88d42174f53d13cf3ac2cf0fd7cf5aea024a31f8e5c8a76aa86dc0dc40ac2f6d5774d7ecc35ca3a54868e4f48fb5084321bbb0c8066138a304669f
6
+ metadata.gz: f945b868565fa7e4f699c07c35f20bb324b4c87d910f96738d4d4049e5b17d7c1455fd3227c4ab699bf85a449349ab4bc0cdd52b1b3d55bd519c48762da39ab6
7
+ data.tar.gz: d6fd25adafc7a2ad5e710043fbd8dc062008292df46d4819598a0a45333fd4dbbccae1428b6bc389ec6c1bcde2ffd9f7ddd1665a5974ff14989148622fabcadd
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.12.0.pre.pre2)
37
+ spree_cm_commissioner (2.12.0)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -0,0 +1,19 @@
1
+ module SpreeCmCommissioner
2
+ # Fired from SpreeCmCommissioner::InventoryItem#publish_live_stock_status_to_firestore
3
+ # (after_commit), one enqueue per event whose stock actually changed, so publishing is near-instant
4
+ # instead of waiting for the next StockStatusConsolidationJob cron tick.
5
+ #
6
+ # ApplicationUniqueJob (args-keyed via activejob-uniqueness, on_conflict: :log) so a burst of
7
+ # concurrent stock changes for the same popular event collapses into one in-flight publish per
8
+ # event_id, instead of one job per changed InventoryItem row.
9
+ class PublishStockStatusesToFirestoreJob < ApplicationUniqueJob
10
+ queue_as :default
11
+
12
+ def perform(event_id:)
13
+ taxon = Spree::Taxon.find_by(id: event_id)
14
+ return unless taxon
15
+
16
+ SpreeCmCommissioner::Events::PublishStockStatusesToFirestore.call(taxon: taxon)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module SpreeCmCommissioner
2
+ # Safety-net reconciliation sweep, run on a low-frequency cron (cm-market-server/config/schedule.yml,
3
+ # `stock_status_consolidation` entry, dispatched through the existing `InvokeJob` cron pattern — same
4
+ # mechanism already used for `WaitingRoom::StampQueuePositionsJob`/`Integrations::PollingSchedulerJob`).
5
+ #
6
+ # SpreeCmCommissioner::PublishStockStatusesToFirestoreJob (enqueued directly off order-complete, see
7
+ # SpreeCmCommissioner::OrderStateMachine) is the primary, near-instant trigger now. This job exists
8
+ # only to catch anything that trigger misses — a failed job, a Firestore hiccup, or stock changing via
9
+ # a path other than order-complete (e.g. an admin stock adjustment) — by re-scanning every event
10
+ # currently inside its live_stock_starts_at/live_stock_ends_at window (see Spree::Taxon.live_stock_active)
11
+ # regardless of whether anything actually changed. Delegates the actual (batched, per-event) Firestore
12
+ # write to Events::PublishStockStatusesToFirestore, same as the real-time job.
13
+ #
14
+ # ApplicationUniqueJob (not ApplicationJob) so a cycle that runs long — many live events, a slow
15
+ # Firestore round trip — can't overlap with the next cron tick a minute later; `perform` takes no
16
+ # arguments, so this is a single global "is a consolidation cycle already in flight" lock, same pattern
17
+ # as WaitingRoom::StampQueuePositionsJob above.
18
+ class StockStatusConsolidationJob < ApplicationUniqueJob
19
+ queue_as :default
20
+
21
+ def perform
22
+ Spree::Taxon.event.live_stock_active.find_each do |taxon|
23
+ SpreeCmCommissioner::Events::PublishStockStatusesToFirestore.call(taxon: taxon)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -35,6 +35,7 @@ module SpreeCmCommissioner
35
35
  before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
36
36
 
37
37
  after_commit :schedule_product_cache_invalidation
38
+ after_commit :publish_live_stock_status_to_firestore
38
39
 
39
40
  # Only for ecommerce: the app uses product.in_stock? (derived from quantity_available) for product cards.
40
41
  # Other types (e.g. accommodation, bus) have many inventory items (including daily-generated ones),
@@ -55,6 +56,29 @@ module SpreeCmCommissioner
55
56
  SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: variant.product)
56
57
  end
57
58
 
59
+ # The single trigger point for real-time Firestore stock-status publishing — catches every
60
+ # stock-mutation path that eventually saves an InventoryItem row: order complete (unstock),
61
+ # order cancel (restock), hold acquire/release/convert (all synced async via
62
+ # InventoryItems::BulkAdjustQuantities(OnHold), which uses per-record `update!`, so this callback
63
+ # does fire for those, just after that job's own async DB sync lands), and admin stock edits via
64
+ # `adjust_quantity!`. A burst of concurrent changes to the same event collapses into one in-flight
65
+ # publish, since PublishStockStatusesToFirestoreJob is unique per event_id (ApplicationUniqueJob,
66
+ # until_executed).
67
+ #
68
+ # Ecommerce-only and quantity-changed-only, same guard shape as schedule_product_cache_invalidation
69
+ # above, but checking all four quantity columns (not just available/locked) since quantity_on_hold
70
+ # alone can flip the published bucket to/from pending_hold.
71
+ def publish_live_stock_status_to_firestore
72
+ return unless ecommerce?
73
+ return unless saved_change_to_quantity_available? || saved_change_to_quantity_on_hold? ||
74
+ saved_change_to_quantity_locked? || saved_change_to_quantity_locked_on_hold?
75
+
76
+ event_id = variant.product.event_id
77
+ return if event_id.nil?
78
+
79
+ SpreeCmCommissioner::PublishStockStatusesToFirestoreJob.perform_later(event_id: event_id)
80
+ end
81
+
58
82
  # Both pools as they stood before this save. saved_change_to_* is nil for a column that did not
59
83
  # change, so an unchanged pool falls back to its current value rather than to zero.
60
84
  def total_quantity_before_last_save
@@ -101,7 +101,7 @@ module SpreeCmCommissioner
101
101
  base.scope :ad_campaigns, -> { where(taxonomy_id: Spree::Taxonomy.ads.id, depth: 1) }
102
102
 
103
103
  # Event taxons currently inside their live-stock sale window — see
104
- # SpreeCmCommissioner::StockStatusPollJob, which only publishes to Firestore for events
104
+ # SpreeCmCommissioner::StockStatusConsolidationJob, which only publishes to Firestore for events
105
105
  # matched by this scope. `live_stock_starts_at`/`live_stock_ends_at` are only meaningful for
106
106
  # `kind: :event` taxons (same convention as `from_date`/`to_date`) — a null on either side
107
107
  # naturally excludes the row here, since SQL's <=/>= against NULL is never true.
@@ -1,12 +1,13 @@
1
1
  module SpreeCmCommissioner
2
2
  module Events
3
3
  # Publishes live stock status for every active ecommerce product under a live event taxon to
4
- # Firestore — batched per event: one aggregate SQL query, at most one Firestore read, and at
5
- # most one Firestore write per call, regardless of how many products changed. Driven by
6
- # SpreeCmCommissioner::StockStatusPollJob on a schedule (see
7
- # plans/2026-08-24-realtime-stock-status/plan.md), not by individual inventory saves this
8
- # replaced the old after_commit-per-change trigger, specifically to bound Firestore
9
- # write/read volume when many events are live and selling concurrently.
4
+ # Firestore — batched per event: quantities are read from Redis (the real-time source of truth
5
+ # for available/on-hold/locked pools, see docs/concepts/inventory-system.md the InventoryItem
6
+ # DB columns lag behind until InventoryItems::BulkAdjustQuantities catches up asynchronously),
7
+ # at most one Firestore read, and at most one Firestore write per call, regardless of how many
8
+ # products changed. Driven both by SpreeCmCommissioner::PublishStockStatusesToFirestoreJob
9
+ # (real-time, enqueued off order-complete) and SpreeCmCommissioner::StockStatusConsolidationJob
10
+ # (a low-frequency safety-net sweep) — see plans/2026-08-24-realtime-stock-status/plan.md.
10
11
  #
11
12
  # Firestore layout unchanged from the old design: events/{event_id} with a nested `products`
12
13
  # map field. `message` is pre-translated per locale (config/locales/*.yml#stock_status), `name`
@@ -40,22 +41,12 @@ module SpreeCmCommissioner
40
41
 
41
42
  changed = {}
42
43
  products.each do |product|
43
- available, on_hold, locked = sums[product.id] || [0, 0, 0]
44
- status = bucket_stock_status(available, on_hold, locked)
45
-
46
- next unless changed?(existing_products[product.id.to_s], status, available, on_hold)
47
-
48
- previous_status = existing_products.dig(product.id.to_s, 'status')&.to_sym
49
-
50
- changed[product.id.to_s] = {
51
- name: product.name,
52
- status: status.to_s,
53
- previous_status: previous_status&.to_s,
54
- message: localized_message(status),
55
- quantity_available: available,
56
- quantity_on_hold: on_hold,
57
- updated_at: now
58
- }
44
+ quantities = sums[product.id] || [0, 0, 0, 0]
45
+ status = bucket_stock_status(*quantities.first(3))
46
+
47
+ next unless changed?(existing_products[product.id.to_s], status, quantities[0], quantities[1])
48
+
49
+ changed[product.id.to_s] = build_entry(product, status, quantities, existing_products, now)
59
50
  end
60
51
 
61
52
  return success(:unchanged) if changed.empty?
@@ -64,14 +55,35 @@ module SpreeCmCommissioner
64
55
 
65
56
  success(changed.keys)
66
57
  rescue StandardError => e
67
- # A Firestore outage (or any error for one event) must not take the poll job's other live
68
- # events down with it — the job wraps each taxon's call individually for this reason.
58
+ # A Firestore outage (or any error for one event) must not take the caller's other live
59
+ # events down with it — both the real-time job and the consolidation sweep call this
60
+ # per-taxon and expect one event's failure to be isolated from the rest.
69
61
  Rails.logger.error("[Events::PublishStockStatusesToFirestore] taxon=#{taxon.id} #{e.class}: #{e.message}")
70
62
  failure(nil, e.message)
71
63
  end
72
64
 
73
65
  private
74
66
 
67
+ # `quantities` is the [available, on_hold, locked, locked_on_hold] tuple from
68
+ # `aggregate_quantities`. `previous_status` is read back from what's already stored, not
69
+ # recomputed — it's the bucket this product was in on the last publish, if any.
70
+ def build_entry(product, status, quantities, existing_products, now)
71
+ available, on_hold, locked, locked_on_hold = quantities
72
+ previous_status = existing_products.dig(product.id.to_s, 'status')&.to_sym
73
+
74
+ {
75
+ name: product.name,
76
+ status: status.to_s,
77
+ previous_status: previous_status&.to_s,
78
+ message: localized_message(status, available),
79
+ quantity_available: available,
80
+ quantity_on_hold: on_hold,
81
+ quantity_locked: locked,
82
+ quantity_locked_on_hold: locked_on_hold,
83
+ updated_at: now
84
+ }
85
+ end
86
+
75
87
  # Buckets a (quantity_available, quantity_on_hold) pair into one of the four stock-status
76
88
  # symbols. `locked` only ever flips a would-be sold_out into pending_hold — see below — it
77
89
  # never feeds the low_stock/in_stock math, which stays public-pool only.
@@ -82,7 +94,6 @@ module SpreeCmCommissioner
82
94
  # "real stock exists but the public can't have it right now" situation `pending_hold` already
83
95
  # describes for held-by-other-checkouts stock, and its message ("In high demand") is exactly
84
96
  # as non-committal here, without naming the reserve the way a more specific bucket would.
85
- # `locked` itself is never included in the published payload, only used to pick the bucket.
86
97
  def bucket_stock_status(available, on_hold, locked = 0)
87
98
  return PENDING_HOLD if available <= 0 && locked.positive?
88
99
  return SOLD_OUT if available <= 0
@@ -117,27 +128,45 @@ module SpreeCmCommissioner
117
128
  .where('discontinue_on IS NULL OR discontinue_on > ?', now)
118
129
  end
119
130
 
120
- # One aggregate query for every active product's sums, keyed by product id avoids an N+1
121
- # that would come from summing each product's inventory items one at a time.
131
+ # Sums, per product id, of the live Redis-sourced quantities for every active inventory item
132
+ # under these products — avoids an N+1 that would come from summing each product's inventory
133
+ # items one at a time. Reuses RedisStock::CachedInventoryItemsBuilder (already the gem's
134
+ # standard "Redis is live, DB is cache-warm fallback" read path — one MGET across all pools,
135
+ # DB value used + cache seeded via SET NX on a cold key) rather than the InventoryItem DB
136
+ # columns directly, since those lag behind Redis until InventoryItems::BulkAdjustQuantities
137
+ # catches up asynchronously.
122
138
  #
123
139
  # quantity_locked (the operator-allotment pool — see cm_inventory_items migration
124
- # `AddLockedQuantitiesToCmInventoryItems`) is summed too, but ONLY so `bucket_stock_status`
125
- # can tell "genuinely zero everywhere" apart from "public pool empty, but real stock exists in
126
- # the withheld pool." Its exact quantity is deliberately never surfaced beyond that: it's not
127
- # part of the low_stock/in_stock math, and it never appears in the published Firestore payload
128
- # (only quantity_available/quantity_on_hold do). Anything more than presence/absence here would
129
- # start advertising the private allotment to every viewer of this broadcast doc, not just the
130
- # one customer `Stock::AvailabilityChecker`/`AvailabilityValidatorDecorator` are allowed to hint
131
- # to via a targeted add-to-cart refusal.
140
+ # `AddLockedQuantitiesToCmInventoryItems`) is summed for `bucket_stock_status`, which needs it
141
+ # to tell "genuinely zero everywhere" apart from "public pool empty, but real stock exists in
142
+ # the withheld pool." quantity_locked and quantity_locked_on_hold are ALSO published verbatim
143
+ # in the Firestore payload below, for operator/debug visibility deliberate per the user's
144
+ # explicit request, despite `events/{event_id}` being publicly readable (no auth on the
145
+ # Firestore security rule): anyone reading the doc can see the private allotment's size.
132
146
  def aggregate_quantities(products)
147
+ items = active_inventory_items(products).to_a
148
+ return {} if items.empty?
149
+
150
+ cached_by_item_id = SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(items).call.index_by(&:inventory_item_id)
151
+
152
+ items.each_with_object(Hash.new { |h, k| h[k] = [0, 0, 0, 0] }) do |item, sums_by_product|
153
+ cached = cached_by_item_id[item.id]
154
+ sums = sums_by_product[item.resolved_product_id]
155
+ sums[0] += cached.quantity_available
156
+ sums[1] += cached.quantity_on_hold
157
+ sums[2] += cached.quantity_locked
158
+ sums[3] += cached.quantity_locked_on_hold
159
+ end
160
+ end
161
+
162
+ # `resolved_product_id` is an extra-select column, not a real InventoryItem attribute — lets
163
+ # the caller above resolve each item's product id without an N+1 `item.variant.product_id`
164
+ # lookup per item.
165
+ def active_inventory_items(products)
133
166
  SpreeCmCommissioner::InventoryItem.active
134
167
  .joins(variant: :product)
135
168
  .where(spree_products: { id: products.map(&:id) })
136
- .group('spree_products.id')
137
- .pluck('spree_products.id', 'SUM(quantity_available)', 'SUM(quantity_on_hold)', 'SUM(quantity_locked)')
138
- .each_with_object({}) do |(id, available, on_hold, locked), memo|
139
- memo[id] = [available.to_i, on_hold.to_i, locked.to_i]
140
- end
169
+ .select('cm_inventory_items.*, spree_products.id AS resolved_product_id')
141
170
  end
142
171
 
143
172
  # Bucket transitions always count; a same-bucket change only counts while LOW_STOCK (the one
@@ -168,10 +197,17 @@ module SpreeCmCommissioner
168
197
  # Pre-translated, per-locale copy baked in at publish time — see
169
198
  # config/locales/*.yml#stock_status. nil for in_stock: the design has no badge for the common
170
199
  # case, and omitting the field is what tells the client that directly.
171
- def localized_message(status)
200
+ #
201
+ # low_stock interpolates `available` into the translation (`%{count}`, pluralized) so the
202
+ # exact "N left" pill text is built once here rather than duplicated client-side from the
203
+ # bare `quantity_available` field — the client just displays this string verbatim (see
204
+ # `cm_ticket_availability_card.dart`'s `_StatusDisplay._label`).
205
+ def localized_message(status, available)
172
206
  return nil if status == IN_STOCK
173
207
 
174
- I18n.available_locales.index_with { |locale| I18n.t("stock_status.#{status}", locale: locale) }
208
+ I18n.available_locales.index_with do |locale|
209
+ I18n.t("stock_status.#{status}", count: available, locale: locale)
210
+ end
175
211
  end
176
212
  end
177
213
  end
@@ -86,7 +86,7 @@ module SpreeCmCommissioner
86
86
  end
87
87
 
88
88
  def inventory_items(inventory_id_and_quantities)
89
- SpreeCmCommissioner::InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id))
89
+ SpreeCmCommissioner::InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id)).includes(variant: :product)
90
90
  end
91
91
  end
92
92
  end
@@ -92,7 +92,7 @@ module SpreeCmCommissioner
92
92
  end
93
93
 
94
94
  def inventory_items(inventory_id_and_quantities)
95
- SpreeCmCommissioner::InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id))
95
+ SpreeCmCommissioner::InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id)).includes(variant: :product)
96
96
  end
97
97
  end
98
98
  end
@@ -991,7 +991,9 @@ en:
991
991
  # SpreeCmCommissioner::Products::PublishStockStatusToFirestore) — not read via I18n at request
992
992
  # time, so every locale we publish for must have an entry here.
993
993
  stock_status:
994
- low_stock: "Only a few left!"
994
+ low_stock:
995
+ one: "%{count} left"
996
+ other: "%{count} left"
995
997
  pending_hold: "In high demand"
996
998
  sold_out: "Sold out"
997
999
 
@@ -606,7 +606,9 @@ km:
606
606
  hold_cooldown: "ការកក់មុនរបស់អ្នកទើបតែផុតកំណត់។ សូមរង់ចាំពីរបីនាទី មុនពេលកក់ម្តងទៀត។"
607
607
 
608
608
  stock_status:
609
- low_stock: "នៅសល់តិចទៀតប៉ុណ្ណោះ!"
609
+ low_stock:
610
+ one: "នៅសល់ %{count}"
611
+ other: "នៅសល់ %{count}"
610
612
  pending_hold: "តម្រូវការខ្ពស់"
611
613
  sold_out: "លក់អស់"
612
614
 
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.12.0-pre2'.freeze
2
+ VERSION = '2.12.0'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.12.0.pre.pre2
4
+ version: 2.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
@@ -1420,6 +1420,7 @@ files:
1420
1420
  - app/jobs/spree_cm_commissioner/payment_incomplete_notification_job.rb
1421
1421
  - app/jobs/spree_cm_commissioner/payment_references/aba_csv_import_job.rb
1422
1422
  - app/jobs/spree_cm_commissioner/product_event_id_to_children_syncer_job.rb
1423
+ - app/jobs/spree_cm_commissioner/publish_stock_statuses_to_firestore_job.rb
1423
1424
  - app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb
1424
1425
  - app/jobs/spree_cm_commissioner/queue_order_webhooks_requests_job.rb
1425
1426
  - app/jobs/spree_cm_commissioner/reports_assigner_job.rb
@@ -1434,7 +1435,7 @@ files:
1434
1435
  - app/jobs/spree_cm_commissioner/sms_job.rb
1435
1436
  - app/jobs/spree_cm_commissioner/sms_pin_code_job.rb
1436
1437
  - app/jobs/spree_cm_commissioner/state_job.rb
1437
- - app/jobs/spree_cm_commissioner/stock_status_poll_job.rb
1438
+ - app/jobs/spree_cm_commissioner/stock_status_consolidation_job.rb
1438
1439
  - app/jobs/spree_cm_commissioner/subscription_order_executor_job.rb
1439
1440
  - app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb
1440
1441
  - app/jobs/spree_cm_commissioner/telegram_alerts/order_integrity_check_job.rb
@@ -3762,9 +3763,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
3762
3763
  version: '2.7'
3763
3764
  required_rubygems_version: !ruby/object:Gem::Requirement
3764
3765
  requirements:
3765
- - - ">"
3766
+ - - ">="
3766
3767
  - !ruby/object:Gem::Version
3767
- version: 1.3.1
3768
+ version: '0'
3768
3769
  requirements:
3769
3770
  - none
3770
3771
  rubygems_version: 3.4.1
@@ -1,26 +0,0 @@
1
- module SpreeCmCommissioner
2
- # Scheduled every 10s via sidekiq-cron (cm-market-server/config/schedule.yml, `stock_status_poll`
3
- # entry, dispatched through the existing `InvokeJob` cron pattern — same mechanism already used
4
- # for `WaitingRoom::StampQueuePositionsJob`/`Integrations::PollingSchedulerJob`).
5
- #
6
- # Replaced the old after_commit-per-change trigger on InventoryItem — see
7
- # plans/2026-08-24-realtime-stock-status/plan.md for why: job/write volume there scaled with the
8
- # number of stock *changes* across every event at once, not with the number of live events. This
9
- # scopes to events currently inside their live_stock_starts_at/live_stock_ends_at window (see
10
- # Spree::Taxon.live_stock_active) and delegates the actual (batched, per-event) Firestore write
11
- # to Events::PublishStockStatusesToFirestore.
12
- #
13
- # ApplicationUniqueJob (not ApplicationJob) so a cycle that runs long — many live events, a slow
14
- # Firestore round trip — can't overlap with the next cron tick 10s later; `perform` takes no
15
- # arguments, so this is a single global "is a poll cycle already in flight" lock, same pattern as
16
- # WaitingRoom::StampQueuePositionsJob above.
17
- class StockStatusPollJob < ApplicationUniqueJob
18
- queue_as :default
19
-
20
- def perform
21
- Spree::Taxon.event.live_stock_active.find_each do |taxon|
22
- SpreeCmCommissioner::Events::PublishStockStatusesToFirestore.call(taxon: taxon)
23
- end
24
- end
25
- end
26
- end