Mage2Plenty v3.6 - Stock Export Profile with Channel-Safe Delta Corrections
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:
| Producer | Hooks | Catches |
|---|---|---|
EnqueueAfterSourceItemsSavePlugin | Magento\InventoryApi\Api\SourceItemsSaveInterface::execute | Admin product page, REST API, CSV imports, mass actions |
EnqueueAfterSourceItemsDeletePlugin | Magento\InventoryApi\Api\SourceItemsDeleteInterface::execute | MSI source-item deletes |
EnqueueAfterLegacyStockItemSavePlugin | Magento\CatalogInventory\Api\StockItemRepositoryInterface::save | Legacy single-source stock saves |
EnqueueAfterDecrementSourceItemQtyPlugin | Magento\Inventory\Model\SourceItem\Command\DecrementSourceItemQty::execute | Shipment-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_after | Safety 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 (
createStockCorrectionwithreasonId 301) — overwrite Plenty's value with Magento's current physical. Simple, idempotent on retry. Right choice when Plenty has no external channels. - Delta (
bookIncomingItemswithreasonId 100orbookOutgoingItemswithreasonId 200) — push only the change Magento observed since the last sync, with signed quantity (positive for incoming, negative for outgoing), W3C-formatdeliveredAt, 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):
| Mode | Push | Plenty after | Channel state |
|---|---|---|---|
| Absolute | createStockCorrection(quantity = 9) | 9 | Overwritten — 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-shotcreateStockCorrectionto 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 localplenty_stock_entitycache 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.
CLI — bin/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-profile—StockExportService::cleanupBatchMemory()(originally surfaced here during stock-export testing)module-plenty-customer-profile—CustomerExportServiceandCustomerImportServicecleanup helpersmodule-plenty-item-profile—ItemImportServicecleanup 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
| Module | Version | Bump | Key Changes |
|---|---|---|---|
module-plenty-stock | 2.1.0 → 2.2.0 | minor | plenty_stock_export_queue table, queue management API, bookOutgoingItems endpoint with reasonId 100 / 200 constants and supplierId / deliveredAt payload fields |
module-plenty-stock-profile | 2.2.0 → 2.3.0 | minor | Five 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-profile | 2.1.2 → 2.1.3 | patch | Magento 2.4.8 gc_collect_cycles compatibility fix |
module-plenty-item-profile | 3.4.0 → 3.4.1 | patch | Magento 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
-
Set the correction mode on each export profile. The shipped default is
absolute— leave it onabsolutefor stores with no Plenty-side channel sales. Flip todeltafor any store where Plenty sells the same SKUs on Amazon, eBay, or other channels. -
Leave bootstrap policy on
absolute_then_deltaunless you have a specific reason not to. For an established Plenty store you want to keep untouched on day-zero, set it toskip_and_seedbefore enabling delta mode. -
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 -vvInspect 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.
-
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.
-
Start the MQ consumer for async high-priority pushes:
bin/magento queue:consumers:start plenty.stock.export.processorTypically 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.
-
No action required for the drift reconciler — it runs hourly under the existing
plenty_stock_profilecron 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
- Stock Export Profile guide — full configuration reference, workflow walk-throughs, troubleshooting
- Stock Import Profile guide — the inverse direction, populates the cache delta mode reads
- Order Export Profile guide — why stock export deliberately skips
sales_order_*events - Complete Changelog
Questions about the upgrade? Reach out to us at support@byte8.io.
