Class: SpreeCmCommissioner::Events::PublishStockStatusesToFirestore

Inherits:
Object
  • Object
show all
Includes:
Spree::ServiceModule::Base, FirestoreConnection
Defined in:
app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb

Overview

Publishes live stock status for every active ecommerce product under a live event taxon to Firestore — batched per event: quantities are read from Redis (the real-time source of truth for available/on-hold/locked pools, see docs/concepts/inventory-system.md — the InventoryItem DB columns lag behind until InventoryItems::BulkAdjustQuantities catches up asynchronously), at most one Firestore read, and at most one Firestore write per call, regardless of how many products changed. Driven both by SpreeCmCommissioner::PublishStockStatusesToFirestoreJob (real-time, enqueued off order-complete) and SpreeCmCommissioner::StockStatusConsolidationJob (a low-frequency safety-net sweep) — see plans/2026-08-24-realtime-stock-status/plan.md.

Firestore layout unchanged from the old design: events/event_id with a nested products map field. message is pre-translated per locale (config/locales/*.yml#stock_status), name is denormalized so a screen with no product list of its own (the waiting-room card) can still render a label, and the write is a genuinely nested hash (never a "products.#id" dot-path string key — see the old design's notes in contexts/superseded-01-event-driven-publish-trigger.md for why that distinction matters).

Constant Summary collapse

SOLD_OUT =

The complete set of stock-status buckets this service can publish — listed in the order bucket_stock_status checks them (most-depleted first), so the full state space is visible here without reading every branch. Each has a translation at config/locales/*.yml#stock_status except IN_STOCK, which intentionally has none — see localized_message.

:sold_out
PENDING_HOLD =
:pending_hold
LOW_STOCK =
:low_stock
IN_STOCK =
:in_stock
STATUSES =
[SOLD_OUT, PENDING_HOLD, LOW_STOCK, IN_STOCK].freeze

Instance Method Summary collapse

Methods included from FirestoreConnection

#firestore, #firestore_available?, #service_account

Instance Method Details

#call(taxon:) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb', line 32

def call(taxon:)
  return success(:skipped) unless firestore_available?

  products = active_products(taxon).to_a
  return success(:skipped) if products.empty?

  sums = aggregate_quantities(products)
  existing_products = existing_products_data(taxon)
  now = Time.zone.now

  changed = {}
  products.each do |product|
    quantities = sums[product.id] || [0, 0, 0, 0]
    status = bucket_stock_status(*quantities.first(3))

    next unless changed?(existing_products[product.id.to_s], status, quantities[0], quantities[1])

    changed[product.id.to_s] = build_entry(product, status, quantities, existing_products, now)
  end

  return success(:unchanged) if changed.empty?

  event_document(taxon).set({ products: changed, server_time: now }, merge: true)

  success(changed.keys)
rescue StandardError => e
  # A Firestore outage (or any error for one event) must not take the caller's other live
  # events down with it — both the real-time job and the consolidation sweep call this
  # per-taxon and expect one event's failure to be isolated from the rest.
  Rails.logger.error("[Events::PublishStockStatusesToFirestore] taxon=#{taxon.id} #{e.class}: #{e.message}")
  failure(nil, e.message)
end