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.