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 +4 -4
- data/Gemfile.lock +1 -1
- data/app/jobs/spree_cm_commissioner/publish_stock_statuses_to_firestore_job.rb +19 -0
- data/app/jobs/spree_cm_commissioner/stock_status_consolidation_job.rb +27 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +24 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -1
- data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +78 -42
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +1 -1
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +1 -1
- data/config/locales/en.yml +3 -1
- data/config/locales/km.yml +3 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +5 -4
- data/app/jobs/spree_cm_commissioner/stock_status_poll_job.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 10936d8185cc09d4bd8eca8c3c2626cb9bb113d2e94695030be009f9b38edbcc
|
|
4
|
+
data.tar.gz: 8b1493341611af89e52c06017d1abc3ad6adc838a541dd3ec93dd76877985eaf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f945b868565fa7e4f699c07c35f20bb324b4c87d910f96738d4d4049e5b17d7c1455fd3227c4ab699bf85a449349ab4bc0cdd52b1b3d55bd519c48762da39ab6
|
|
7
|
+
data.tar.gz: d6fd25adafc7a2ad5e710043fbd8dc062008292df46d4819598a0a45333fd4dbbccae1428b6bc389ec6c1bcde2ffd9f7ddd1665a5974ff14989148622fabcadd
|
data/Gemfile.lock
CHANGED
|
@@ -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::
|
|
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:
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
|
|
44
|
-
status = bucket_stock_status(
|
|
45
|
-
|
|
46
|
-
next unless changed?(existing_products[product.id.to_s], status,
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
68
|
-
# events down with it — the job
|
|
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
|
-
#
|
|
121
|
-
# that would come from summing each product's inventory
|
|
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
|
|
125
|
-
#
|
|
126
|
-
# the withheld pool."
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
|
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
|
data/config/locales/en.yml
CHANGED
|
@@ -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:
|
|
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
|
|
data/config/locales/km.yml
CHANGED
|
@@ -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
|
|
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
|
|
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/
|
|
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:
|
|
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
|