17 min readOct 28, 2026by jakub

Changelog

Current version: 1.2.2

Each version lists the packages that changed in it. Packages are versioned separately, so a version can carry only some of them.

1.2.2 — 2026-10-26

qoliber/product-notifications

Upgrade notes

  • Run bin/magento setup:upgrade: a data patch rehashes stored bundle subscriptions (see Fixed below) and cancels duplicates a shopper already has.
  • Update qoliber/product-notifications-msi to 1.2.2 as well (Composer enforces it): it switches the moved legacy stock detection off on MSI stores, and an older one would let both record.
  • In production mode, run bin/magento setup:di:compile (the bundle configurator block's constructor changed) and deploy static content (bin/magento setup:static-content:deploy): the Luma bundle configurator's script and the admin subscriptions grid's filter script changed.

Changed for integrators

  • Plugin\CatalogInventory\StockItemPlugin moved from Magento\CatalogInventory\Model\Stock\Item (afterSave) to the resource model Magento\CatalogInventory\Model\ResourceModel\Stock\Item (aroundSave), and has an enabled constructor argument. A di.xml that disabled the plugin qoliber_product_notifications_stock_item on Stock\Item no longer does anything: set enabled to false instead, as qoliber/product-notifications-msi does.

Fixed

  • Without MSI, a restock saved through StockItemRepository recorded no back-in-stock or low-stock event, so nobody was alerted: admin product saves, the PUT /V1/products/{sku}/stockItems/{itemId} REST endpoint, StockRegistry::updateStockItemBySku() and stock returned by a credit memo or a cancelled order (GitLab issue #2). The detection hooked Stock\Item::save(), which the repository skips. It now hooks the stock item resource model's save(), where every path ends, so each save is seen once, and compares the persisted row before and after the save instead of the item's orig data, which an item built by the caller does not carry.
  • Bundle configurator prices bypassed Magento's price calculation. A bundle's special price (a percentage) was applied to the bundle's own price but not to its selections, so a 100.00 bundle at 80% with a 15.00 selection showed 95.00 while the cart charged 92.00; dynamic bundles ignored it entirely. Prices now come from Magento's pricing framework, as on Magento's own bundle page, so the bundle's discount reaches its selections and the total matches the cart.
  • The configurator's total multiplied the price of one bundle by the quantity, so tier prices never applied: two 115.00 bundles with a 50% tier discount from two showed 230.00 while the cart charged 115.00. The total now follows the quantity as the cart prices it: the bundle's tier discount for that many bundles (the better of it and the special price) and, in a dynamic bundle, each child's own tier price for the quantity the cart holds. The bundle data carries qty_discounts and, per selection, qty_prices.
  • Currency switching changed the symbol without converting: with USD → EUR at 0.8 a 115.00 USD bundle showed €115.00 instead of €92.00. The bundle and grouped configurators now show every price in the shopper's currency, and the Hyvä labels are formatted in it (formatPrice() on both blocks).
  • Ticking the same bundle selections in another order (A then B, later B then A, in a checkbox or multi-select option) created a second subscription, so the shopper got every alert twice. Selections are now hashed in a fixed order (option, then SKU). The upgrade rehashes existing bundle subscriptions; where a shopper already has two for the same configuration and alert, the live one is kept (one still within its expiry date, then active, pending confirmation, fired; the older one on a tie). The other is cancelled, or expired if its expiry date has passed.
  • Cancelling from My Account → Product Notifications showed a page of raw JSON ({"success":true}): the page posts a plain form, but the controller answered in JSON. The shopper is now sent back to the list with "Subscription cancelled." (or "Could not cancel subscription."). XHR requests still get the JSON answer.
  • Fixed-price bundles: the notify configurator's total left out the bundle's own price, so a 100.00 bundle with a 15.00 selection showed 15.00 instead of 115.00 (on Luma; Hyvä needs qoliber/product-notifications-hyva 1.2.2). The bundle data now carries the bundle's price (base_price, 0 for a dynamic bundle), and the total is that price plus the picked selections, times the bundle quantity.
  • "View subscribers in grid" found nothing for a SKU containing / & + =: Magento's grid URL filter decodes with decodeURI and left those characters encoded (PN/E2E&1+2=3 became PN%2FE2E%261%2B2%3D3). The subscriptions grid now reads its URL filter with URLSearchParams, and the link encodes the SKU as a standard query.

qoliber/product-notifications-hyva

Fixed

  • Fixed-price bundles: the configurator's total includes the bundle's own price (115.00, not 15.00, for a 100.00 bundle with a 15.00 selection).
  • Bundle and grouped configurator prices are discounted and converted as Magento prices them (a bundle at 80% shows 92.00, the cart's price, not 95.00; €92.00 at 0.8 EUR to the dollar, not €115.00), and their labels are formatted in the shopper's currency instead of the symbol glued to a US-formatted number.
  • The bundle total follows the quantity as the cart prices it (tier discounts, and child tier prices in a dynamic bundle): two 115.00 bundles at 50% off from two show 115.00, not 230.00.

Notes

  • Requires qoliber/product-notifications 1.2.2, whose bundle data carries the bundle price and whose configurator blocks price and format in the shopper's currency.

qoliber/product-notifications-msi

Changed

  • The core's legacy stock detection moved to the stock item resource model (core 1.2.2 fixes restocks saved through StockItemRepository on stores without MSI). This package switches it off with its new enabled argument instead of disabling the plugin on Stock\Item, so MSI stores still record each restock once, through the source-item plugin.

Notes

  • Requires qoliber/product-notifications 1.2.2.

qoliber/product-notifications-graph-ql

Changed

  • Version alignment with the 1.2.2 suite release; no functional changes in this package.

qoliber/product-notifications-swatches

Changed

  • Version alignment with the 1.2.2 suite release; no functional changes in this package.

qoliber/product-notifications-swatches-hyva

Changed

  • Version alignment with the 1.2.2 suite release; no functional changes in this package.

1.2.1 — 2026-10-26

qoliber/product-notifications

Upgrade notes

  • Run bin/magento setup:di:compile in production mode after updating, then bin/magento cache:flush: the account page's controller has a new constructor argument, and code compiled for 1.2.0 fails on it.

Fixed

  • My Account → Product Notifications listed nothing with the full-page cache on: the page was cacheable, so Magento cleared the customer session before the list was read, and every customer saw "You have no active product notifications". The page is no longer cached.
  • Opening that page signed out (a bookmark, an emailed link) failed with a fatal error instead of sending the shopper to the login page and back.
  • Reports → Top Firing Products and Top Requested Products failed on every load with a MySQL syntax error: their row limit was sent to the database as a quoted string. Both reports load again.
  • On a fixed-price bundle the product page's notify configurator failed with an error (it read a bundle price constant Magento does not have), so its options and the back-in-stock form did not render. Percent-priced selections are priced from the bundle's price again.
  • Subscriptions → Force resend did nothing for a subscription whose back-in-stock alert had already gone out: the resend was retired as "subscription_fired" without a message. It is sent again; a cancelled, unconfirmed or expired subscription is still never mailed.
  • "View subscribers in grid" on the product edit page opened the whole Subscriptions grid; it opens it filtered to the product now.
  • The subscription funnel printed its conversion rates as "88%%".
  • The account page and the guest manage page showed their heading twice (the page title and the template's own heading); so did the report pages, and the Product Notifications section on the product edit page repeated its title.

qoliber/product-notifications-hyva

Fixed

  • The account page and the guest manage page showed their heading twice (the page title and the template's own heading).
  • On Hyvä 1.3 (Tailwind 3) the theme build purged every utility class that only this package's templates use (classes the theme's own templates also use survived), so the notify forms, configurators, cart price-drop block and subscription lists rendered partly unstyled. The package now ships view/frontend/tailwind/tailwind.config.js, whose template and layout content paths mergeTailwindConfig() adds to the theme build. Tailwind 4 (Hyvä 1.4+) ignores the file; its build is unchanged. On Hyvä 1.3, rebuild the theme's styles after updating.

qoliber/product-notifications-msi

Changed

  • Version alignment with the 1.2.1 suite release; no functional changes in this package.

qoliber/product-notifications-graph-ql

Changed

  • Version alignment with the 1.2.1 suite release; no functional changes in this package.

qoliber/product-notifications-swatches

Changed

  • Version alignment with the 1.2.1 suite release; no functional changes in this package.

qoliber/product-notifications-swatches-hyva

Changed

  • Version alignment with the 1.2.1 suite release; no functional changes in this package.

1.2.0 — 2026-10-25

qoliber/product-notifications

Upgrade notes

  • Rebuild generated code before the store serves traffic: bin/magento setup:upgrade, then in production mode bin/magento setup:di:compile, then bin/magento cache:flush (this also clears the compiled_plugins cache of Mage-OS's compiled interceptors). 1.2.0 changes constructors in the core, GraphQL and MSI packages, so compiled DI left over from 1.1 fails on the first product page.

Breaking changes for integrators

A store running the shipped templates and classes is not affected. Custom code is, where it:

  • implements Api\DispatchRepositoryInterface or Api\AttributionRepositoryInterface itself: both gained methods (see "API additions" under Changed).
  • calls Block\Manage\SubscriptionList::getNewMagicLinkToken() from a theme override of manage/list.phtml (Luma or Hyvä). The method is removed: listing no longer rotates the link, so the page keeps getMagicLinkToken(). Copy the 1.2 template again.
  • cancels through product-notifications/subscribe/cancel with a GET or without a form key: the magic-link and signed-in cancels are form-keyed POSTs now. Only the one-click email unsubscribe (unsubscribe_token) still accepts GET.
  • disables or replaces the qoliber_product_notifications_attribute_order observer: it moved from sales_order_place_after to checkout_submit_all_after.
  • overrides the Hyvä package's templates (see that package's changelog).

Security

  • A guest submission for an address that already holds a subscription no longer rewrites it: the owning customer, target, threshold and emailed tokens stay as they are. Signed-in customers always subscribe their account address.
  • Subscribe targets are checked against the catalog: the product must be enabled in the store's website, and configurable/grouped/bundle children must belong to the parent. Every catalog refusal shares one message. Bundle/grouped selections are capped at 50.
  • Email addresses are validated before any write.
  • New per-address and per-network subscribe rate limit, shared by the storefront form and the GraphQL mutation (security/rate_limit_*, defaults 5 / 20 per 60 minutes). Each counter is updated under a lock, so concurrent requests cannot spend one budget twice; a counter that stays locked for 2 s refuses the request.
  • The guest CAPTCHA setting now works on its own: the field renders on Luma (and Hyvä, see that package) from captcha/refresh, and is enforced without also configuring Magento's customer CAPTCHA for the form. A wrong answer answers 422.
  • Magic-link management: listing no longer rotates the link (refreshes, mail prefetchers and link scanners used to kill it); cancelling rotates it. Links are scoped to the website they were issued on. Cancelling from the manage page is a form-keyed POST, and only the one-click email unsubscribe still accepts GET.

Fixed

  • The master enable switch is enforced at store scope: subscribing is refused, no new events are recorded, no new dispatches are fanned out and the price scanner idles; dispatches already queued still drain.
  • Confirmation links expire after subscription/confirm_ttl_hours; abandoned pending_confirm rows are expired by the daily cron, which now drains its whole backlog.
  • Dispatch processing is claim-based: the publisher claims a row before publishing and consumers claim it before sending, so duplicate messages send once and a fast consumer's outcome is never overwritten. A failed publish is retried after 60 s, and the retry sweeper recovers claims abandoned by crashed workers (queued > 60 min, sending > 30 min). New dispatch status sending.
  • Only active subscriptions are mailed: a pending_confirm row (cancel + re-subscribe) no longer receives an alert around double opt-in, and a fired one-shot alert is not repeated by a second queued dispatch.
  • Delivery is serialised per subscription: two dispatches for one subscription (separate stock events) taken by two consumers no longer both send. The second waits up to 10 s for the subscription's lock, then goes back to the publisher for 60 s; when it runs it finds a one-shot alert fired, or a recurring one in its cooldown.
  • An active subscription past its expires_at is not mailed, even before the daily expiry cron has marked it expired (dispatch dead with subscription_expired).
  • A confirmation link clicked after the expiry cron expired its row is refused like an unknown token. It used to answer as already confirmed while the row stayed expired.
  • Confirmation deadline, magic-link lifetime and inactivity window are read for the subscription's store view. They are configurable per store view but were read from the default scope.
  • Price-drop and low-stock cooldowns are read for the subscription's store view, at fanout and before sending. A store's longer cooldown used to fall back to the default (7 days), so its subscribers could be alerted again early.
  • The inactivity window is an inactivity window again: an alert sent to a recurring (price-drop, low-stock) subscription and the signed-in owner subscribing again restart it. expires_at used to stay where activation put it, so an active subscription could expire the day after an alert.
  • The first confirmation email is not sent once the confirmation deadline has passed (a delayed queue message, before the expiry cron runs); the retry path already refused.
  • The product page's display mode (inline / modal) is read for the store view.
  • Price drops from product saves are recorded per website from that website's own price before and after the save. With a website price scope, lowering the price in one website's store view used to record the drop for every website the product is in, and alert subscribers whose price had not changed; a new default price now also skips the websites that set their own.
  • Back-in-stock alerts re-check salability on the subscriber's website at fanout and again before sending; a product that sold out again sends nothing and the subscription stays active.
  • Confirmation emails that fail to send are retried through the dispatch pipeline (backoff, dispatch/max_attempts, visible in the queue grid as "Confirmation email").
  • dispatch/max_attempts and dispatch/quiet_hours now take effect.
  • Catalog-rule (and special-price) price drops are detected again, against a stored baseline of the last effective price: Magento folds rule prices into the indexed final price, so the old comparison could never fire. The scan cursor is a flag now; the old one was written to a scope it never read back.
  • Every alert email links through the click tracker (click_url), with links, product data and images built for the subscription's store view.
  • Order attribution runs on checkout_submit_all_after (the order has an id; it used to be attributed to order 0), once per order line, to the newest click for a product the line contains.
  • Storefront stock reads (product page, configurable variants, bundle and grouped configurators) go through the new Api\SalabilityResolverInterface, batched.
  • Manage page: no Registry, not full-page cached, and a reload in one process no longer fails; cancelling the row the link belongs to no longer gets undone.

Changed

  • Flap window, low-stock threshold and price-drop minimum are set at default scope only. They apply when a SKU's stock event is recorded, before any store is involved, so values set on a website or store view were never read; the admin no longer offers them there.
  • Reporting: attributed conversions and revenue drop to their true values. One order used to credit every open click for the address at the full grand total; it now credits each matching order line once, at row total minus discount (excluding tax and shipping). Expect lower numbers in Reports after upgrading.
  • DispatchConsumer no longer rethrows handled send failures (Magento's consumer rejects a throwing message without requeueing; retries are DB-driven).
  • API additions: DispatchRepositoryInterface::transitionStatus() / findStale(), AttributionRepositoryInterface::hasAttributionForOrder(), Api\SalabilityResolverInterface. TargetValidator::validate() takes an optional store id for the catalog checks.

Added

  • Table qoliber_product_notifications_price_baseline.

qoliber/product-notifications-hyva

Breaking changes for integrators

  • A theme override of this package's templates copied from 1.1 keeps the 1.1 behaviour, which does not work under CSP Alpine, and its manage/list.phtml calls the removed getNewMagicLinkToken(). Copy the 1.2 templates again. product/options_selector.phtml is removed (see Removed).

Security

  • The guest CAPTCHA renders in all three product-page forms when security/captcha_enabled is on (CSP-safe Alpine; image from captcha/refresh). A wrong answer keeps the form open with a fresh image.
  • Manage page: cancelling is a form-keyed POST; the link survives a reload.

Fixed

  • Under the CSP build of Alpine (a strict storefront CSP on Hyva/default-csp) a guest's typed email never reached the notify form, so every guest subscribe failed with "Email is required": x-model reads but cannot write there. The email, CAPTCHA and bundle qty inputs bind :value plus an @input method instead.

  • The cart price-drop block was plain Alpine (inline x-data arguments, ternaries, x-model) and did nothing under CSP Alpine. It is now a registered, CSP-safe component. It also honours the master switch, and is offered to signed-in customers only while the guest CAPTCHA is on (one CAPTCHA answer cannot cover one request per cart item).

  • The dropdown-cascade configurable template (used when Magento_Swatches is disabled) called Hyva/default-csp-only methods and threw on Hyva/default. It is theme-neutral now.

  • A bundle back-in-stock subscription submitted only the out-of-stock picks, so the alert could go out while a selection that was in stock at subscribe time had sold out. The whole configuration is submitted now, as on Luma, and every selection is re-checked before sending.

  • Composer could not install the package: it required hyva-themes/magento2-default-theme-module, which does not exist. The requirement is removed; hyva-themes/magento2-theme-module stays.

Removed

  • product/options_selector.phtml: unused on Hyvä (the layout removes its block on every Hyvä page) and not CSP-safe.

Notes

  • Requires qoliber/product-notifications 1.2 (guest CAPTCHA, store-scoped master switch).

qoliber/product-notifications-msi

Fixed

  • Storefront and fanout salability now follow the website's MSI stock: a preference for Qoliber\ProductNotifications\Api\SalabilityResolverInterface resolves the website's stock and answers with AreProductsSalable / salable qty.
  • The per-stock snapshot taken around a stock reindex reads only SKUs with an active back-in-stock or low-stock subscription, instead of the whole stock index twice.

Notes

  • Requires qoliber/product-notifications 1.2 (SalabilityResolverInterface).

qoliber/product-notifications-graph-ql

Security

  • subscribeProductNotification is rate limited with the same budget as the storefront form, and is refused while the module is disabled for the store.

Added

  • cancelGuestProductNotification(magic_link_token, uid) returns { success, magic_link_token }. Cancelling rotates the guest's token, and this is the only way a headless client gets the replacement to cancel the next subscription.

Deprecated

  • cancelProductNotificationByMagicLink: it returns only a Boolean, so after one cancel the client holds a spent token. It keeps working; use cancelGuestProductNotification.

Changed

  • guestProductNotifications no longer rotates the magic-link token; new_magic_link_token echoes the (still valid) token it was called with, and the list is scoped to the website the link was issued on.

Fixed

  • Unit tests run without setup:di:compile (test stub for the generated ContextExtensionInterface).

Notes

  • Requires qoliber/product-notifications 1.2 (shared subscribe rate limiter).

qoliber/product-notifications-swatches

Changed

  • Version alignment with the 1.2.0 suite release; no functional changes in this package.

qoliber/product-notifications-swatches-hyva

Fixed

  • On the plain Hyva/default theme (and themes inheriting from it) every configurable rendered no swatch options at all, so variants could not be picked or bought: the renderer bound methods that only Hyva/default-csp's components resolve without arguments. The renderer and swatch item are now theme-neutral and CSP-safe: every directive binds a zero-argument pn* method that resolves the attribute and option from the element and calls only the primitives both themes share. Out-of-stock swatch marking works on every Hyvä theme, under a strict CSP (no inline script without a hash, no eval) included.
  • The notify-aware swatch item uses its own block instead of overriding the theme's product.swatch.item.
  • Composer could not install the package: it required hyva-themes/magento2-default-theme-module, which does not exist. The requirement is removed; hyva-themes/magento2-theme-module stays.

Notes

  • Requires hyva-themes/magento2-theme-module 1.3.11 or later (the HyvaCsp view model) and qoliber/product-notifications / qoliber/product-notifications-hyva 1.2.

1.1.0 — 2026-08-07

qoliber/product-notifications

Added

  • Magento_Swatches compatibility is now available through the new companion packages qoliber/product-notifications-swatches (Luma / native SwatchRenderer) and qoliber/product-notifications-swatches-hyva (Hyvä).

Changed

  • Version alignment with the 1.1.0 suite release; no functional changes in this package.

qoliber/product-notifications-hyva

Changed

  • Version alignment with the 1.1.0 suite release; no file changes in this package.
  • Behavioral note: on installs where Magento_Swatches is enabled, configurable option rendering is owned by qoliber/product-notifications-swatches-hyva (this module's dropdown-cascade template targets a block that core Magento_Swatches removes, so it only applies where swatches are absent or disabled).

qoliber/product-notifications-msi

Changed

  • Version alignment with the 1.1.0 suite release (Magento_Swatches compatibility companions); no functional changes in this package.

qoliber/product-notifications-graph-ql

Changed

  • Version alignment with the 1.1.0 suite release (Magento_Swatches compatibility companions); no functional changes in this package.

qoliber/product-notifications-swatches

Added

  • Initial release: Magento_Swatches compatibility for Qoliber Product Notifications.
  • Luma / native SwatchRenderer integration — swatch selections (including out-of-stock combinations) drive the notify-me panel via qoliber-pn:variant-changed.
  • The swatch widget's product maps are widened from salable-only to the full child set so out-of-stock combinations resolve to a product id.
  • Out-of-stock swatch options stay clickable and are visually marked (diagonal strike, notify-me hint) instead of receiving the vendor .disabled state; dropdown attributes inside the swatch widget get the same treatment.
  • The base module's Knockout cascade selector is retired on configurable PDPs (the swatch renderer owns option rendering once Magento_Swatches is active). Product-listing swatches are intentionally untouched.

qoliber/product-notifications-swatches-hyva

Added

  • Initial release: Hyvä swatches compatibility for Qoliber Product Notifications.
  • Restores Hyvä's native swatch UI on configurable products while keeping the notify-me flow: out-of-stock swatch options stay selectable (keyboard-accessible, theme strike-through styling preserved) and emit qoliber-pn:variant-changed with { sku, stockState }.
  • Dropdown (non-swatch) attributes keep out-of-stock options enabled with an "out of stock, notify me" suffix; add-to-cart is suppressed while an out-of-stock pick is active.
  • Block\Renderer substitutes the notify-aware template (the core swatch renderer ignores layout template attributes), and the layout replaces the theme-resurrected configurable options block with an own block.
  • Registers with Hyvä CompatModuleFallback and the Qoliber Tailwind scan; ships a Tailwind 3 config, keeps the automatic @source on Tailwind 4.

1.0.0 — 2026-05-27

qoliber/product-notifications

Added

  • Service contracts: SubscribeServiceInterface, SenderInterface, StockChannelResolverInterface, RecorderInterface, Subscription\MatchSkuWriterInterface, plus typed repositories for Subscription, SubscriptionMatchSku, Dispatch, StockEvent, Attribution.
  • REST endpoints (anonymous): confirm-by-token, cancel-by-magic-link, unsubscribe-by-token, list-by-magic-link.
  • Cron pipeline: FanoutScheduler, Publisher, RetrySweeper, ExpireSubscriptions, CatalogRulePriceDropScanner.
  • Message queue with typed DispatchMessageInterface payload (replaces raw-string topic).
  • Admin UI: subscriptions grid, dispatch queue grid, reports (funnel, top-firing, top-requested), per-product subscribers column, ACL-fine-grained mass actions (cancel / export / resend).
  • Triggers: legacy stock-item plugin (CatalogInventory) for non-MSI installs, plus MSI per-stock indexer and source-item-save detection (in the optional Qoliber_ProductNotificationsMsi submodule). All paths share one transition rule set via Model\StockEvent\StockTransitionNotifier.
  • Email templates and price-drop attribution observer hooked to sales_order_place_after.
  • Subscription model with _eventObject = 'subscription' so observers consume qoliber_product_notifications_subscription_save_after cleanly.

Security

  • Every adminhtml controller declares ADMIN_RESOURCE; mass actions use sub-resources (subscriptions_cancel, subscriptions_export, subscriptions_resend).
  • All public storefront controllers implement HttpGetActionInterface / HttpPostActionInterface (no legacy Action parents).
  • Subscription::setTarget / setConfig use JSON_THROW_ON_ERROR.

qoliber/product-notifications-hyva

Added

  • Hyvä-themed templates for the product page subscribe form, customer-account list, manage-by-magic-link page, and an opt-in cart price-drop block (hyva_checkout config group).
  • module.xml sequences Hyva_Theme and Hyva_DefaultTheme.

qoliber/product-notifications-msi

Added

  • MSI-aware StockChannelResolver (overrides the core resolver via DI preference).
  • StockSnapshotInterface and StockSnapshot for capturing per-stock-id index state before / after the Magento\InventoryIndexer\Indexer\Stock\StockIndexer run.
  • Plugin on the per-stock indexer that records per-stock back_in_stock and low_stock transitions.
  • Source-item save detection (SourceItemsSavePlugin on Magento\InventoryApi\Api\SourceItemsSaveInterface): catches the path real restocks actually take - admin Advanced Inventory, the source-item grid, the REST API and CSV import - which reindex via the SourceItem indexer and therefore never reach StockIndexer::executeRow. Detection is per MSI stock: for every stock bound to a website sales channel it samples salability (via AreProductsSalableInterface / GetProductSalableQtyInterface, which answer correctly for the default stock too) before the save and again after MSI's reindex (high sortOrder), and records each transition against that stock as SCOPE_MSI_STOCK + stock id. This keeps a restock in one website's stock from notifying another website's subscribers. Consistent with the indexer plugin's scope, so the on-save and scheduled-reindex paths dedup cleanly. Covered by a unit cross-stock-isolation test and an end-to-end SourceItemsRestockDetectionTest.
  • SalesChannelStockProvider: resolves the set of stock ids bound to website sales channels.
  • Disables the legacy Magento\CatalogInventory\Model\Stock\Item plugin from the core module to avoid double-firing.

qoliber/product-notifications-graph-ql

Added

  • GraphQL schema (schema.graphqls) for Product Notifications.
  • Resolvers: subscribeProductNotification, confirmProductNotification, cancelProductNotification, cancelProductNotificationByMagicLink, productNotificationStatus, customerProductNotifications, guestProductNotifications.
  • ProductNotificationGuestList.new_magic_link_token rotates on each call; guest token is single-use replacement.