Skip to main content

Mage2Plenty v3.6 - Stock Export Profile with Channel-Safe Delta Corrections

· 10 min read
Soft Commerce Team
Mage2Plenty Development Team

We're pleased to announce Mage2Plenty v3.6, which closes the Magento → PlentyONE direction of the stock-sync story. The connector previously imported stock from PlentyONE; it can now also push Magento's physical-stock changes back to PlentyONE in real time, with a new delta correction mode that preserves Amazon, eBay, and other Plenty-side channel deductions instead of overwriting them. The release ships an event-driven producer pipeline, an MQ consumer, a cron drainer, an hourly drift reconciler, a structured CLI surface, and three configurable bootstrap policies for the day-zero alignment question.

What's New in v3.6

Stock export has lived as a partially-wired surface in the codebase for some time. v3.6 finishes the build: a dedicated queue table backed by event producers on every Magento stock-change path, a message queue topic for high-priority async pushes, a cron scheduler for steady-state drain, and a drift reconciler as the safety net. On top of that, a second correction strategy — delta mode — uses Plenty's bookIncomingItems / bookOutgoingItems endpoints to push only the change Magento observed since the last sync, which preserves Plenty-side channel state instead of overwriting it. The release also carries a cross-cutting Magento 2.4.8 compatibility fix applied in three services.

The Stock Export Problem

PlentyONE has its own physical-stock counter. When Magento ships an order, runs a mass MSI update, or processes a stock receipt, that change isn't visible to Plenty until something pushes it. The previously-shipped absolute-push generator (createStockCorrection with reasonId 301) worked for the common case — Plenty has no external channels, so Magento can authoritatively overwrite Plenty's value on every change. But for any client whose Plenty also sells the same SKU on Amazon, eBay, or other channels via Plenty's order-routing, absolute pushes are unsafe: a channel sale processed inside Plenty decrements Plenty's physical, and the next Magento push overwrites that decrement and the channel can oversell.

The fix needed three things at once: a reliable queue to absorb every Magento change, a way to push only the change Magento saw rather than the absolute, and a clean answer to "what happens on day-zero for any SKU that's never been pushed before."

Event-Driven Stock Export Queue

A dedicated plenty_stock_export_queue table with a UNIQUE constraint on sku coalesces rapid back-to-back changes — the next worker run processes the latest state per SKU rather than draining a duplicate-laden backlog. Each row carries status, trigger_reason, attempts, last_pushed_qty (per-warehouse JSON map for delta mode), metadata (per-push audit trail), and the usual processed/next-attempt timestamps.

Five producer plugins enqueue affected SKUs whenever Magento's physical stock changes:

ProducerHooksCatches
EnqueueAfterSourceItemsSavePluginMagento\InventoryApi\Api\SourceItemsSaveInterface::executeAdmin product page, REST API, CSV imports, mass actions
EnqueueAfterSourceItemsDeletePluginMagento\InventoryApi\Api\SourceItemsDeleteInterface::executeMSI source-item deletes
EnqueueAfterLegacyStockItemSavePluginMagento\CatalogInventory\Api\StockItemRepositoryInterface::saveLegacy single-source stock saves
EnqueueAfterDecrementSourceItemQtyPluginMagento\Inventory\Model\SourceItem\Command\DecrementSourceItemQty::executeShipment-driven decrements — the path that bypasses the higher-level SourceItemsSaveInterface and would otherwise leave Plenty stale until the next drift sweep
EnqueueOnCatalogProductSaveAfter (observer)catalog_product_save_afterSafety net for any code path that bypasses both inventory APIs

Order-placement events are deliberately not producers — Plenty manages its own reservation when the order arrives via the order-export profile, so pushing stock on sales_order_* would double-deduct. Configurable, bundle, and grouped product parents are filtered at the producer boundary by STOCK_ELIGIBLE_TYPES = ['simple', 'virtual', 'downloadable'] so the queue stays focused on SKUs that actually have an inventory_source_item row.

Delta Correction Mode

The new Stock Correction Mode field on the export profile selects between two correction strategies:

  • Absolute (createStockCorrection with reasonId 301) — overwrite Plenty's value with Magento's current physical. Simple, idempotent on retry. Right choice when Plenty has no external channels.
  • Delta (bookIncomingItems with reasonId 100 or bookOutgoingItems with reasonId 200) — push only the change Magento observed since the last sync, with signed quantity (positive for incoming, negative for outgoing), W3C-format deliveredAt, and a per-warehouse baseline tracked on the queue row. Plenty applies the delta on top of whatever it currently has, so channel deductions are never touched by us.

Walk-through with concrete numbers — Magento starts at 10, channels then sell 3 (Plenty real becomes 7), Magento ships one (Magento becomes 9):

ModePushPlenty afterChannel state
AbsolutecreateStockCorrection(quantity = 9)9Overwritten — channel can oversell
Delta (baseline = 10)bookOutgoingItems(quantity = -1)6 (= 7 − 1)Preserved

The processor captures Plenty's response into the queue row's metadata.plenty_bookings audit trail: incoming pushes record Plenty's booking id and orderNo (the response shape Plenty returns for bookIncomingItems), outgoing pushes record direction, quantity, warehouse, and timestamp. Operators investigating a Plenty-side anomaly can grep the queue for the specific booking and find it in Plenty's UI.

Bootstrap Policies

Delta mode needs an answer to "what happens on the very first push for a given (SKU, warehouse) pair, before last_pushed_qty has an entry?" The new Bootstrap Policy field offers three:

  • absolute_then_delta (new default, recommended) — on first push, run a one-shot createStockCorrection to align Plenty with Magento's current quantity, then record it as the baseline. From the next push onward, delta-only — channel deductions are preserved. The baseline is stashed only when the API call succeeds, so a failed bootstrap doesn't leave us thinking we're in sync. Resolves the temporal tension between "Magento is authoritative" (operator enabled the profile) and "channel deductions matter" (operator chose delta) by handling each on its right horizon: day-zero alignment, then channel-safety from push #2 onward.
  • skip_and_seed — no API call. Record Magento's current quantity as the baseline; preserve Plenty's existing value untouched. For migrating an established Plenty store with state you want kept as-is.
  • reconcile_to_cache — compute delta against the local plenty_stock_entity cache and push it. Useful in staged rollouts for inspecting computed deltas before any HTTP push; overwrites channel state on day-zero so not channel-safe.

The atomicity property required the absolute processor to start stashing the baseline on every successful push (regardless of mode). That has a useful side-effect: switching from absolute to delta mode later needs no operator onboarding step, because active SKUs already have last_pushed_qty populated by ongoing absolute runs.

Trigger Layers — MQ, Cron, CLI

Three independent layers drain the queue, each with a different latency profile:

Message queue — topic plenty.stock.export.processor, db-backed, async. The admin grid's mass-export action publishes selected entity_ids to this topic for immediate processing, and external integrations can publish to it directly. Payload accepts skus[] or entityIds[] plus profile and priority. The handler chunks (100 standard / 50 high-priority), builds a search criteria filter, and runs the service.

Cron — job plenty_stock_export in group plenty_stock_profile, schedule driven by an admin-tunable <config_path> for steady-state drain. Mirrors the plenty_stock_import pattern.

CLIbin/magento plenty:stock:export with structured flags. Operators can filter on -i <productIds>, -s <skus>, --status pending,error, or pass -p <profileId>. The -i and -s paths now seed the queue when rows don't exist yet (and report how many rows were inserted vs already present), and the no-flag run prints a hint and exits instead of silently draining the entire pending queue.

Drift Reconciler — Hourly Safety Net

Producers can miss events under outage conditions (MQ down, hook fails, deploy race). The drift reconciler runs hourly under the plenty_stock_profile cron group at 15 * * * *, comparing inventory_source_item.quantity against the cached plenty_stock_entity.stock_physical per (Magento source, Plenty warehouse). SKUs whose values diverge by more than 0.0001 get enqueued with trigger_reason = collect_drift.

A new plenty:stock:export:drift command exposes the same sweep on demand with --limit and --profile flags, useful for incident response. In delta mode the reconciler interacts cleanly with the bootstrap policy: an enqueued SKU goes through the standard generator path, the baseline gets atomically aligned, and subsequent runs see no divergence — closing the "drift reconciler hammers forever" failure mode that the previous bootstrap default could produce.

Magento 2.4.8 Compatibility Fix

A cross-cutting fix applied in three modules: gc_collect_cycles() calls inside per-batch cleanup helpers have been disabled. On Magento 2.4.8 in this codebase, forcing cycle collection between paginated getList() calls resets the internal page pointer on the underlying resource collection, causing the do-while batch loop to re-fetch page 1 forever and re-process the same batch (and re-publish its downstream side-effects).

The change is now in:

  • module-plenty-stock-profileStockExportService::cleanupBatchMemory() (originally surfaced here during stock-export testing)
  • module-plenty-customer-profileCustomerExportService and CustomerImportService cleanup helpers
  • module-plenty-item-profileItemImportService cleanup helper

The calls are commented rather than deleted so the intent is visible and the guard can be reinstated if the upstream collection ever fixes its page-pointer reset semantics under cycle GC.

Release Summary

ModuleVersionBumpKey Changes
module-plenty-stock2.1.0 → 2.2.0minorplenty_stock_export_queue table, queue management API, bookOutgoingItems endpoint with reasonId 100 / 200 constants and supplierId / deliveredAt payload fields
module-plenty-stock-profile2.2.0 → 2.3.0minorFive producer plugins, MQ consumer + publisher, cron + schedule processor, drift reconciler, delta correction mode with three bootstrap policies, do-while batch pagination + message aggregation in StockExportService, structured plenty:stock:export CLI with --status / -i / -s, admin grid + mass actions
module-plenty-customer-profile2.1.2 → 2.1.3patchMagento 2.4.8 gc_collect_cycles compatibility fix
module-plenty-item-profile3.4.0 → 3.4.1patchMagento 2.4.8 gc_collect_cycles compatibility fix, drop unused ScheduleConfigInterfaceFactory dep from ItemExportService

Metapackage: softcommerce/mage2plenty-os 3.5.0 → 3.6.0

Upgrade Guide

Prerequisites

  • Magento 2.4.6+ (2.4.8 recommended)
  • PHP 8.1+ (8.3 or 8.4 recommended)
  • Mage2Plenty v3.5.x

Quick Upgrade

composer require softcommerce/mage2plenty-os:^3.6

bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush

Post-Upgrade Steps

  1. Set the correction mode on each export profile. The shipped default is absolute — leave it on absolute for stores with no Plenty-side channel sales. Flip to delta for any store where Plenty sells the same SKUs on Amazon, eBay, or other channels.

  2. Leave bootstrap policy on absolute_then_delta unless you have a specific reason not to. For an established Plenty store you want to keep untouched on day-zero, set it to skip_and_seed before enabling delta mode.

  3. Seed the queue once so the first cron cycle has work:

    bin/magento plenty:stock:export:add --all
    bin/magento plenty:stock:export --status=pending -vv

    Inspect the result before committing to scheduled drains — in delta mode, the first run per SKU triggers the bootstrap policy and is the right place to confirm channel deductions aren't being overwritten.

  4. Enable the cron schedule on the export profile (admin: Profiles → Stock Export → Schedule Configuration → Enable Schedule). Recommended frequencies: every 5 minutes for high-traffic stores with channels, every 15 minutes for standard e-commerce, hourly for low-velocity catalogs.

  5. Start the MQ consumer for async high-priority pushes:

    bin/magento queue:consumers:start plenty.stock.export.processor

    Typically managed by supervisor or systemd. The admin grid's mass-export action publishes to this topic and falls back to the cron drainer if the consumer is down.

  6. No action required for the drift reconciler — it runs hourly under the existing plenty_stock_profile cron group as soon as the cron is unfrozen after upgrade.

Verifying the Setup

-- Queue health
SELECT status, COUNT(*) FROM plenty_stock_export_queue GROUP BY status;

-- Producers firing on Magento changes
SELECT sku, trigger_reason, created_at FROM plenty_stock_export_queue
WHERE created_at > NOW() - INTERVAL 1 HOUR ORDER BY created_at DESC LIMIT 20;

-- Delta-mode baseline populated
SELECT sku, last_pushed_qty FROM plenty_stock_export_queue
WHERE last_pushed_qty IS NOT NULL LIMIT 10;

-- Plenty booking audit trail
SELECT sku, JSON_EXTRACT(metadata, '$.plenty_bookings') FROM plenty_stock_export_queue
WHERE JSON_EXTRACT(metadata, '$.plenty_bookings') IS NOT NULL LIMIT 5;

Resources


Questions about the upgrade? Reach out to us at support@byte8.io.