Mage2Plenty v3.3 - Correct VAT Discount Export, Safer Order Claims & Import Hardening
We're pleased to announce Mage2Plenty v3.3, a targeted stability release that restores correct VAT handling on discounted order lines, removes a false "already claimed" error when re-exporting completed orders, and fixes several edge cases in the item import pipeline surfaced by production use since v3.2.
What's New in v3.3
This release follows up on customer feedback from v3.2.x production rollouts. The headline change is a correction to how cart-rule and coupon discounts are exported to PlentyONE for stores that charge VAT — an area where the v2.0 rewrite of the order export pipeline introduced a subtle double-taxation regression.
Order Export - Discount Tax Handling
The Problem
After upgrading from the v2.x order pipeline to v3.x, stores using Magento shopping cart rules on VAT-charged lines saw Plenty invoice totals diverging from the amount the customer actually paid. For a £77 line with a 10% cart rule and 20% VAT, Magento showed £74.30 including shipping, but Plenty recorded £72.76 — a £1.54 shortfall per line equal to re-applying VAT on the discount.
Root cause: Item::buildItemAmountsRequest was unconditionally multiplying the per-unit discount by (1 + taxPercent/100) on the assumption that base_discount_amount was stored net. For EU stores where Catalog Prices = Including Tax (the default setup in Germany, Switzerland, Austria, and most Plenty integrations), Magento stores the discount gross, so the grossup double-applied VAT.
The Fix
The grossup now gates on tax/calculation/price_includes_tax instead of tax/calculation/discount_tax:
- Catalog Prices = Including Tax → Magento already stores
base_discount_amountgross. Forwarded to Plenty as is. - Catalog Prices = Excluding Tax → Magento stores it net. Grossed up by
(1 + taxPercent/100)before export.
This produces correct totals in both configurations without requiring any Magento or Plenty setting changes.
Promotion Generator
The cart-level coupon export (Promotion.php) now reads the same tax config at the order's store scope rather than default scope. Previously, stores with per-website tax configuration were silently falling back to the default value, producing incorrect coupon-line VAT rates on multi-website installations.
Order Export - Claim Semantics
The Problem
The v3.2.1 duplicate-export incident fix introduced an atomic compare-and-swap on plenty_order_status to prevent two concurrent flows from creating duplicate PlentyONE orders. That fix was correct in principle but too narrow in scope: it whitelisted only pending and error as claimable source statuses, which caused admin Export and Mass Export buttons to fail on any order that had moved to complete, success, new, pending_collect, skipped, or NULL — with a misleading message:
Skip: Order claimed by another process (M:...)
The Fix
Claim semantics now match their actual invariant: only processing blocks a new claim. Every other state — including terminal ones — is reclaimable, so admin re-exports of completed orders work as expected:
- Added
UpdateSalesOrderStatusInterface::claim()with a negative predicate:WHERE plenty_order_status NOT IN (...) OR plenty_order_status IS NULL - Switched
OrderExportService::claimOrderForProcessing()toclaim([PROCESSING]) - Reworded the thrown exception to "Skip: Order already being processed by another flow" — the message now reflects the actual blocking condition
Race protection against concurrent cron / MQ / admin / CLI flows is unchanged; two flows still cannot both transition to processing because the loser's UPDATE sees zero rows after the winner's transition commits.
Item Import Hardening
Guard Against Null Product Names
ConfigAttribute generator threw a TypeError in explode() when a configurable's child product name bucket contained a null value for a given store — typically when the variation's plenty_variation_entity.name was null and no other generator had populated the store-scoped name. Child product name generation now skips null/empty entries instead of crashing, allowing mass import of configurable items to complete.
Price Attribute Dropdown - "None" Option
The "Magento price attribute" dropdown in the item import profile UI is now prefilled with a None entry, allowing admins to explicitly clear a price mapping. Previously the first attribute in the list was always preselected, making it impossible to leave a mapping unset.
Configuration - Locale Mapping
StoreConfig::buildStoreData() was referencing an undefined $item variable when reading the per-mapping LOCALE key, causing the per-store locale override to be silently ignored and always falling back to Magento's general/locale/code. The typo is fixed and the locale field in the store-mapping JSON now works as documented.
Release Summary
| Module | Version | Bump | Key Changes |
|---|---|---|---|
| module-plenty-order-profile | 2.4.4 | patch | Discount gross-up discriminator, claim semantics, store-scoped Promotion tax lookup |
| module-plenty-order | 2.1.0 | minor | New UpdateSalesOrderStatusInterface::claim() with negative predicate |
| module-plenty-item-profile | 3.2.0 | minor | None option in price attribute dropdown, null-name guard, collect-service fixes |
| module-plenty-profile | 2.2.1 | patch | StoreConfig $item → $storeMap typo fix |
| module-plenty-category-profile | 2.0.4 | patch | Profile ID propagation in CategoryCollect |
| module-plenty-stock-profile | 2.0.8 | patch | Profile ID propagation in StockCollect |
Upgrade Guide
Prerequisites
- Magento 2.4.6+ (2.4.8 recommended)
- PHP 8.1+ (8.3 or 8.4 recommended)
- Mage2Plenty v3.2.x
Quick Upgrade
composer require softcommerce/mage2plenty-os:^3.3
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
Post-Upgrade Steps
- No config changes required. The discount fix is self-adjusting based on
tax/calculation/price_includes_tax. Stores that previously worked around the issue by flippingtax/calculation/discount_taxto1can keep the setting or revert it — behaviour will be correct either way. - Verify admin re-export. If you had operators avoiding the Export / Mass Export buttons on
completeorders due to the false "claimed by another process" message, try again — it should succeed now. - Third-party integrators: If you maintain a custom implementation of
UpdateSalesOrderStatusInterface, add the newclaim(int, string, string[])method. Existing callers ofexecuteConditional()are unchanged.
Resources
Questions about the upgrade? Reach out to us at support@byte8.io.
