Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Sales Attribute: Storing Earned Points on Order and Order Item

Sales Attribute: Storing Earned Points on Order and Order Item

~8 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026

loyalty_points_earned (type int) records how many points a specific order, or a single order line item, actually earned - the "result" of PointsCalculator::calculatePoints() from chapter 5, permanently attached to the exact order that triggered it. Unlike the company solution from chapter 22, this chapter needs no extension attribute and no plugin - Magento ships a dedicated tool for Sales entities specifically.

Order and order item aren't EAV either

Up through Magento 1, orders really were EAV entities - one of the old platform's most notorious performance bottlenecks. Magento 2 designed sales_order, sales_order_item, and the other Sales entities as flat tables from the start, for exactly the reasons chapter 18 gave for flat tables in general: high write frequency (every order, every invoice), fixed column structure. And yet the mechanism for adding new fields is still called a "sales attribute" - a historical leftover of the name, not a statement about how it's actually stored.

SalesSetup instead of EavSetup

\Magento\Sales\Setup\SalesSetup extends EavSetup and offers the same addAttribute() method used in chapters 19-21 - but internally it behaves completely differently for order/order item: instead of creating rows in eav_attribute and a value table, it directly adds a real ALTER TABLE ... ADD COLUMN column to the respective table (sales_order/sales_order_item). The call looks identical to a product attribute call, but the result in the database is a flat field, not an EAV value.

app/code/Mironsoft/Loyalty/Setup/Patch/Data/InstallSalesLoyaltyAttributes.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Setup\Patch\Data;

use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Sales\Setup\SalesSetupFactory;

/**
 * Adds loyalty_points_earned as a flat column to sales_order and sales_order_item,
 * via the Sales module's dedicated SalesSetup helper rather than EavSetup.
 */
class InstallSalesLoyaltyAttributes implements DataPatchInterface
{
    /**
     * @param ModuleDataSetupInterface $moduleDataSetup Provides the setup connection for the patch.
     * @param SalesSetupFactory $salesSetupFactory Creates the SalesSetup helper used to add sales attributes.
     */
    public function __construct(
        private readonly ModuleDataSetupInterface $moduleDataSetup,
        private readonly SalesSetupFactory $salesSetupFactory
    ) {
    }

    /**
     * Adds loyalty_points_earned to both the order and the order item entity.
     *
     * @return void
     */
    public function apply(): void
    {
        $this->moduleDataSetup->getConnection()->startSetup();

        /** @var \Magento\Sales\Setup\SalesSetup $salesSetup */
        $salesSetup = $this->salesSetupFactory->create(['setup' => $this->moduleDataSetup]);

        $attributeConfig = [
            'type' => 'int',
            'label' => 'Loyalty Points Earned',
            'input' => 'text',
            'required' => false,
            'default' => '0',
            'visible' => false,
            'system' => false,
        ];

        $salesSetup->addAttribute('order', 'loyalty_points_earned', $attributeConfig);
        $salesSetup->addAttribute('order_item', 'loyalty_points_earned', $attributeConfig);

        $this->moduleDataSetup->getConnection()->endSetup();
    }

    /**
     * @return array<int, string>
     */
    public static function getDependencies(): array
    {
        return [];
    }

    /**
     * @return array<int, string>
     */
    public function getAliases(): array
    {
        return [];
    }
}

Achtung: The entity type codes 'order' and 'order_item' are string literals, not class constants like Product::ENTITY - a typo such as 'orders' doesn't produce an immediate error during the patch run, it fails deep inside SalesSetup with a cryptic "unknown entity type" exception, because there's an internal fixed mapping from code to table name.

Access without an extension attribute

Because loyalty_points_earned is a real column, access already works with plain getData()/setData() - \Magento\Sales\Model\Order and \Magento\Sales\Model\Order\Item use the magic getter/setter mechanism of AbstractModel, with no extension_attributes.xml involved at all: $order->getLoyaltyPointsEarned() works immediately after a single setup:upgrade, unlike the company solution from chapter 22, which needed a plugin precisely because CompanyInterface doesn't allow its own getters/setters for foreign fields.

Real access via REST or GraphQL, though - say, so an external system can see how many points an order earned - does need its own extension_attributes.xml on OrderInterface/OrderItemInterface, because the service contract layer only knows what's reachable via the interface or an extension attribute. Block 10 (chapters 79-87) picks this back up once the module's REST/GraphQL layer is built.

The order grid isn't automatically included

The new column only lands in sales_order/sales_order_item, not in the separate, denormalized sales_order_grid table the admin order grid reads from. For loyalty_points_earned to show up there, the column would additionally need to be added to sales_order_grid via db_schema.xml and populated through a plugin/observer on the grid sync mechanism - an extra step deliberately left out of this chapter.

bin/magento setup:upgrade
bin/magento cache:flush

Tipp: visible => false is a deliberate choice here: loyalty_points_earned is a pure result field an observer (chapter 30) fills in automatically when an order is placed - unlike loyalty_points_multiplier on the product, it needs no admin form field for manual editing.

Four attributes, three fundamentally different techniques: EavSetup for real EAV entities (product, category, customer), column + extension attribute + plugin for a flat entity with no dedicated Sales-style helper (company), and SalesSetup for the flat but specially supported Sales entities. Chapter 24 shifts perspective away from storage and toward the source models that give loyalty_tier and loyalty_tier_override their dropdown options.