Skip to main content

Mage2Plenty v3.4 - Item Mapping Integrity, Stock Visibility & Storage Hygiene

· 10 min read
Soft Commerce Team
Mage2Plenty Development Team

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:

  1. BatchRequest::execute advanced REQUEST_INDEX once 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.
  2. SaveProductMappingPostProcessor wrote plenty_item_id and plenty_variation_id straight back to catalog_product_entity keyed only by product_id, without verifying that the returned number (Plenty variation number) matched the actual Magento SKU. Once an incorrect ID was written, nothing downstream re-validated it — OrderItemVariationIdResolver treats 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::processResponse matches each response entry by base.externalId / base.number to find its source command, falling back to positional alignment only when both identifiers are absent.
  • ItemBatchExecutor::processResponse builds the same SKU/externalId lookup from variations[0] and records the returned NUMBER in the response entry so downstream processors can verify it.
  • SaveProductMappingPostProcessor is 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.sku is loaded once for every candidate product_id.
    • Second pass builds the save request only after verifying cpe.sku == response.number for each candidate. Any row that fails the invariant is skipped and logged rather than written.
  • BatchRequest::execute now advances $index per 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:

  1. catalog_product_entity.plenty_variation_id matches a plenty_variation_entity row whose number = cpe.sku
  2. catalog_product_entity.plenty_item_id matches the same row's parent item_id
  3. plenty_item_entity.product_id cross-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.number matches a Magento SKU (the common cleanup case).
  • --all deletes every locally-tracked Plenty item (use with care).
  • --collect runs a full PlentyONE scroll collect first so the delete set is computed against fresh PlentyONE state, not stale local cache.
  • --dry-run, --force, --limit for safe operation; a confirmation prompt shows count + sample preview before any API call.
  • After a successful API delete, the matching plenty_item_entity rows 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: $qtyOnHand is 0.0 (row absent → method returns 0).
  • PlentyONE side: $qtyRequest is 0.
  • They match → short-circuit → inventory_source_item row 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_entity is empty mid-cycle.
  • SKU sanity check requires the SKU to still exist in catalog_product_entity before deletion.
  • Per-deletion INFO logging captures reservation_id / sku / source_code / qty to 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:

SettingDefaultNotes
Enable Storage CleanupOffOpt-in to roll out per store
Retention (Days)90Minimum 30 enforced at runtime
Cron Schedule0 3 * * *Standard cron expression
Delete Batch Size5000Bounds per-statement lock time
Max Batches Per Run100Caps 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_queue rows for parent/standalone products stayed in pending forever when the export completed without errors, because executors only emit per-SKU messages on failure. SaveQueueStatusPostProcessor now 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 hardcoded setPageSize(20) in ItemExportService::buildDefaultSearchCriteria(). The schedule now reads getProcessBatchSize() and chunks pending product IDs through array_chunk, so the configured batch size actually controls the export pipeline. Pagination is the caller's responsibility.

Release Summary

ModuleVersionBumpKey Changes
module-plenty-item-profile3.3.0minorResponse alignment by SKU/externalId, integrity check, purge-orphans CLI, queue draining + batch size
module-plenty-stock-profile2.1.0minorZero-qty source-item creation, orphaned reservation cleanup, stale-entity pruning
module-plenty-order-profile2.5.0minorOpt-in plenty_order_entity retention cleanup
module-plenty-client2.1.4patchREQUEST_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

  1. 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-integrity

    A non-zero exit + log entries in var/log/plenty-item-integrity.log indicate legacy mapping drift that needs cleanup. The fix prevents new drift — it does not retroactively repair existing rows.

  2. Optional: enable the scheduled integrity check. Plenty > Item Config > Data Integrity runs the same checks on a cron schedule (default 0 3 */2 * *) and emails on mismatch.

  3. Optional: enable order storage cleanup. Plenty > Order Config > Storage Cleanup if plenty_order_entity growth is a concern. 90-day retention default, can be tuned to your archival policy (minimum 30 days enforced).

  4. 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.