Mage2Plenty v3.4 - Item Mapping Integrity, Stock Visibility & Storage Hygiene
We're pleased to announce Mage2Plenty v3.4, a stability release built around a coordinated fix for the 2026-05-04 item-mapping alignment issue — including a primary executor-side fix, an upstream batch-request cleanup, and a defense-in-depth integrity check that detects mapping drift early. The release also restores storefront visibility for products imported at zero stock, clears orphaned client reservations from auto-unassigned warehouses, and adds opt-in storage cleanup for plenty_order_entity.
What's New in v3.4
This release is the follow-up to a mapping-alignment issue identified on 2026-05-04, where a batch of newly added Magento variants ended up with another product's plenty_item_id / plenty_variation_id written into catalog_product_entity. Because those columns drive downstream resolution (order export, stock sync), any drift there propagates into subsequent flows until the mapping is corrected. The fix touches three modules — the executor that aligns API responses, the batch request layer underneath it, and a new integrity-check service that monitors for drift.
Item Mapping - The Core Fix
The Problem
When a batch of new variants is exported to PlentyONE via the PIM batch endpoint, the executor receives an array of responses and walks them in step with the original commands. The previous implementation used positional alignment — $commands[$index] — to decide which Magento SKU each response belonged to. As long as request and response order matched perfectly, this worked. But:
BatchRequest::executeadvancedREQUEST_INDEXonce per resource rather than once per pushed item. When a single resource body carried multiple records (which is normal for bulk variant updates), the index drifted by N-1 after every multi-record body.SaveProductMappingPostProcessorwroteplenty_item_idandplenty_variation_idstraight back tocatalog_product_entitykeyed only byproduct_id, without verifying that the returnednumber(Plenty variation number) matched the actual Magento SKU. Once an incorrect ID was written, nothing downstream re-validated it —OrderItemVariationIdResolvertreats the stored column as cached truth and reuses it on every subsequent lookup.
On 2026-05-04 this surfaced when a single bulk operation of new variants ended up with mapping rows pointing at unrelated PlentyONE items, including duplicate plenty_variation_id values across multiple Magento SKUs.
The Fix
The response-alignment logic is now identifier-based, not positional, and the database write is invariant-gated:
PimBatchExecutor::processResponsematches each response entry bybase.externalId/base.numberto find its source command, falling back to positional alignment only when both identifiers are absent.ItemBatchExecutor::processResponsebuilds the same SKU/externalId lookup fromvariations[0]and records the returnedNUMBERin the response entry so downstream processors can verify it.SaveProductMappingPostProcessoris reworked into a two-pass collect/verify flow:- First pass collects candidate entries keyed by
product_id, refusing to overwrite a prior entry for the same product within a run. - Between passes,
cpe.skuis loaded once for every candidateproduct_id. - Second pass builds the save request only after verifying
cpe.sku == response.numberfor each candidate. Any row that fails the invariant is skipped and logged rather than written.
- First pass collects candidate entries keyed by
BatchRequest::executenow advances$indexper pushed item, matching the slot it occupies in$response— fixing the upstream drift that surfaced the alignment issue in the first place.
This is defense in depth: even if a future change reintroduces an alignment bug at the executor layer, the SKU↔number invariant in the post-processor makes it impossible to silently write incorrect values to the mapping columns.
Item Mapping - Detection & Cleanup
A fix that only prevents future drift doesn't help installations that already have incorrect mapping rows from before the upgrade. Two new tools address that.
Scheduled Integrity Check
A new admin group at Plenty > Item Config > Data Integrity (disabled by default) runs ItemMappingIntegrityCheckService on a configurable schedule (default 0 3 */2 * *). The service runs three SQL invariants against plenty_variation_entity as the source of truth:
catalog_product_entity.plenty_variation_idmatches aplenty_variation_entityrow whosenumber = cpe.skucatalog_product_entity.plenty_item_idmatches the same row's parentitem_idplenty_item_entity.product_idcross-references back to the same Magento product
Results are always logged to var/log/plenty-item-integrity.log, and an alert email is dispatched via ProfileNotification when mismatches are found (email is on by default once the group is enabled).
A companion bin/magento plenty:item:check-integrity CLI runs the same checks ad-hoc and returns a non-zero exit when issues exist, so it can be used in post-repair verification or CI/cron wrappers.
plenty:item:purge-orphans CLI
The mapping-drift pattern often leaves orphaned PlentyONE items — Plenty items that exist in plenty_item_entity but no longer have a corresponding Magento SKU, or that share a variation.number with another SKU.
bin/magento plenty:item:purge-orphans deletes these via /rest/batch DELETE /rest/items/{id}:
- Default mode targets items whose
variation.numbermatches a Magento SKU (the common cleanup case). --alldeletes every locally-tracked Plenty item (use with care).--collectruns a full PlentyONE scroll collect first so the delete set is computed against fresh PlentyONE state, not stale local cache.--dry-run,--force,--limitfor safe operation; a confirmation prompt shows count + sample preview before any API call.- After a successful API delete, the matching
plenty_item_entityrows are removed and FK cascade cleans up the children.
Stock Import - The Zero-Quantity Visibility Bug
The Problem
StockImportService::Generator\StockPhysical short-circuited via $qtyRequest == $qtyOnHand to skip writing an unchanged stock value. The check called SourceItemRepository::getQtyPhysical(), which returns 0.0 when the source-item row doesn't exist at all. So when a brand-new product was imported from PlentyONE with zero stock:
- Magento side:
$qtyOnHandis0.0(row absent → method returns 0). - PlentyONE side:
$qtyRequestis0. - They match → short-circuit →
inventory_source_itemrow never gets created.
Without that row, the MSI inventory_stock_X index has no entry for the product, and the product stays invisible in category/search until a positive stock value first arrives. For stores using PlentyONE as the source of truth for stock, this masked any product that started at 0 stock — a long-standing edge case that surfaced as "new products aren't appearing on the storefront."
The Fix
The equality short-circuit now fires only when an inventory_source_item row actually exists (verified via SourceItemRepository::get()). Otherwise we fall through and create the row with quantity=0, status=false — matching the older-version behaviour where source assignment was unconditional during import. The get() call reuses the in-memory cache populated by the preceding getQtyPhysical() / getQtyNet() call, so there's no additional SQL.
Stock Reservations - Orphans From Auto-Unassigned Warehouses
The Problem
PlentyONE automatically deletes warehouse assignments after 6 months of zero stock on a (variation, warehouse) pair. The Magento-side reservation cleanup didn't know about this, so any client_reserved row pointing to a warehouse that PlentyONE had since unassigned remained in inventory_reservation forever — making the product unsalable in Magento (negative salable_qty) despite stock existing on other sources.
The Fix
CleanupClientReservations::execute() now accepts a warehouse_id => source_code mapping and additionally removes client_reserved reservations whose (sku, source_code) pair has no matching plenty_stock_entity row — i.e. the warehouse is no longer assigned to that variation. Safeguards:
- 500/run cap to protect against catastrophic mass-deletion if
plenty_stock_entityis empty mid-cycle. - SKU sanity check requires the SKU to still exist in
catalog_product_entitybefore deletion. - Per-deletion INFO logging captures
reservation_id/sku/source_code/qtyto the PlentyStock virtual logger for post-mortem traceability.
The mapping is supplied by the ReservationCleanup pre-execute processor from StockConfig::getWarehouseToSourceMapping().
Stock Entity - Pruning After Full Collect
Delta-based stock collect (the normal mode) can't detect when PlentyONE has deleted a warehouse assignment, because untouched variations don't reappear in the delta feed. The one-time full-collect cycle (enable_onetime_full_process) is the only point at which every still-valid (variation, warehouse) pair is refreshed — so any plenty_stock_entity row left with an older collected_at after a clean cycle is, by definition, stale.
A new StockEntityStaleCleanup service runs after a successful full-collect cycle and deletes rows whose collected_at is older than the cycle-start cutoff or NULL. It includes a 50% safety threshold: if more than half the table is flagged stale, it refuses to prune and surfaces the condition for manual review — that ratio indicates an incomplete cycle rather than a real cleanup.
Order Storage - Opt-In Cleanup
A new admin group at Plenty > Order Config > Storage Cleanup (disabled by default) prunes plenty_order_entity rows older than a configurable retention window. Settings:
| Setting | Default | Notes |
|---|---|---|
| Enable Storage Cleanup | Off | Opt-in to roll out per store |
| Retention (Days) | 90 | Minimum 30 enforced at runtime |
| Cron Schedule | 0 3 * * * | Standard cron expression |
| Delete Batch Size | 5000 | Bounds per-statement lock time |
| Max Batches Per Run | 100 | Caps total cron run time |
Deletes only target rows with a populated collected_at (so never-collected entries are preserved), and FK cascade purges plenty_order_item, plenty_order_payment, plenty_order_document, plenty_order_property automatically. Disabled by default so the rollout is opt-in per store.
Item Export Queue - Draining & Batch Size
Two long-standing rough edges in Schedule\ItemExport:
plenty_item_export_queuerows for parent/standalone products stayed inpendingforever when the export completed without errors, because executors only emit per-SKU messages on failure.SaveQueueStatusPostProcessornow marks parent/standalone products as complete when no per-SKU messages were emitted.getProcessBatchSize()was ignored by the export schedule — pagination was driven by a hardcodedsetPageSize(20)inItemExportService::buildDefaultSearchCriteria(). The schedule now readsgetProcessBatchSize()and chunks pending product IDs througharray_chunk, so the configured batch size actually controls the export pipeline. Pagination is the caller's responsibility.
Release Summary
| Module | Version | Bump | Key Changes |
|---|---|---|---|
| module-plenty-item-profile | 3.3.0 | minor | Response alignment by SKU/externalId, integrity check, purge-orphans CLI, queue draining + batch size |
| module-plenty-stock-profile | 2.1.0 | minor | Zero-qty source-item creation, orphaned reservation cleanup, stale-entity pruning |
| module-plenty-order-profile | 2.5.0 | minor | Opt-in plenty_order_entity retention cleanup |
| module-plenty-client | 2.1.4 | patch | REQUEST_INDEX advances per pushed item in batch responses |
Upgrade Guide
Prerequisites
- Magento 2.4.6+ (2.4.8 recommended)
- PHP 8.1+ (8.3 or 8.4 recommended)
- Mage2Plenty v3.3.x
Quick Upgrade
composer require softcommerce/mage2plenty-os:^3.4
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
Post-Upgrade Steps
-
Run the integrity check once. Even with the executor fix in place, any rows containing incorrect mapping values from before the upgrade will remain incorrect until repaired:
bin/magento plenty:item:check-integrityA non-zero exit + log entries in
var/log/plenty-item-integrity.logindicate legacy mapping drift that needs cleanup. The fix prevents new drift — it does not retroactively repair existing rows. -
Optional: enable the scheduled integrity check.
Plenty > Item Config > Data Integrityruns the same checks on a cron schedule (default0 3 */2 * *) and emails on mismatch. -
Optional: enable order storage cleanup.
Plenty > Order Config > Storage Cleanupifplenty_order_entitygrowth is a concern. 90-day retention default, can be tuned to your archival policy (minimum 30 days enforced). -
No action required for the stock-visibility and reservation-cleanup fixes — they take effect immediately on the next stock import / reservation cleanup cycle.
Resources
Questions about the upgrade? Reach out to us at support@byte8.io.
