Skip to main content

Mage2Plenty v3.3 - Correct VAT Discount Export, Safer Order Claims & Import Hardening

· 5 min read
Soft Commerce Team
Mage2Plenty Development Team

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_amount gross. 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() to claim([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​

ModuleVersionBumpKey Changes
module-plenty-order-profile2.4.4patchDiscount gross-up discriminator, claim semantics, store-scoped Promotion tax lookup
module-plenty-order2.1.0minorNew UpdateSalesOrderStatusInterface::claim() with negative predicate
module-plenty-item-profile3.2.0minorNone option in price attribute dropdown, null-name guard, collect-service fixes
module-plenty-profile2.2.1patchStoreConfig $item → $storeMap typo fix
module-plenty-category-profile2.0.4patchProfile ID propagation in CategoryCollect
module-plenty-stock-profile2.0.8patchProfile 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​

  1. 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 flipping tax/calculation/discount_tax to 1 can keep the setting or revert it — behaviour will be correct either way.
  2. Verify admin re-export. If you had operators avoiding the Export / Mass Export buttons on complete orders due to the false "claimed by another process" message, try again — it should succeed now.
  3. Third-party integrators: If you maintain a custom implementation of UpdateSalesOrderStatusInterface, add the new claim(int, string, string[]) method. Existing callers of executeConditional() are unchanged.

Resources​


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