Buckle up, this is a big one. And it will break backwards compatibility.

Adjustments (promotions, taxes) are calculated by multiplication, and need to be rounded for precision. The rounding of taxes is determined by law. Sales tax is rounded on the order level, and stored on the order item unrounded. VAT is rounded per-unit or per-line, as the application chooses. It's just important to be consistent. Accounting software is split on what they prefer.

Commerce 1.x uses per-unit rounding, as did Amazon at the time of research.
Spree and Shopify did per-line rounding at the time of research.
Per-line has the benefit of being much more precise when a large quantity of products is being ordered (a thousand, for example).

When 2.x was being made, the adjustment / promotion / tax APIs were still very young (== non-existent), and we were worried of potential problems with implementing per-line, so we stuck with per-unit rounding. The actual data model was implement in an ambiguous way, with a single $order_items->adjustments field.

Since then, the following problems were identified:
- Avalara and TaxJar both use per-line calculation, making it painful to integrate them.
- The interaction between taxes and discounts (together with other considerations such as returns) requires order-level discounts to be applied
per-order-item. However, a per-order discount can't be always split into equal parts, making the per-order-item amounts not add up
properly. This can be solved by allocation (applying the reminder to the order items) if the calculation is per-line. For per-unit, there is no
solution. See #2980195: Fixed order offers should reduce each order item.

Because of these problems (but primarily because of the tax/discount one), we have decided to switch Commerce to per-line rounding.
1) The $order_item->adjustments field stays the same, except the adjustments now refer to the total_price, not the unit_price.
2) $order gets a legacy_adjustments base field and a usesLegacyAdjustments() getter, defaulting to FALSE, but initialized to TRUE. Based on this field, $order->collectAdjustments() can decide whether to still multiply order item adjustments, preserving old behavior for orders that have already been placed.
3) All code that deals with adjustments (promotions, taxes) gets updated.

We'll need to create a matching Commerce Avatax release, notify/patch other tax cloud modules, notify Commerce Migrate, create a change request.

Comments

bojanz created an issue. See original summary.

  • bojanz committed 070dde5 on 8.x-2.x
    Issue #2980713 by bojanz: Switch order item adjustments from per-unit to...
bojanz’s picture

Status: Active » Fixed

This was not easy :)

Commerce Migrate issue: #2981522: Update for Commerce 2.8 order_item adjustments.
Commerce Avatax issue: #2981517: Update for Commerce 2.8.

bojanz’s picture

Issue tags: +ny-bogo

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.