> For the complete documentation index, see [llms.txt](https://docs.b2b-sellers.com/b2b-platform/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.b2b-sellers.com/b2b-platform/developer-guide/how-to/cart-recalculation-extension-points.md).

# Cart recalculation extension points

Available from B2Bsellers Suite 4.1.5.

Several B2Bsellers features price a cart outside the customer's own session: order approvals, offers, and sales representative fast orders all build a *virtual cart* and recalculate it. Order approvals go one step further and store a snapshot of the cart, then rebuild it from that snapshot whenever the approval is refreshed.

If your plugin keeps its own data on cart line items, or relies on entity extensions on the customer address, that data does not automatically survive the round trip. The snapshot stores a fixed set of fields, and a rebuilt address is a plain entity without your extensions. Anything your cart collectors or processors depend on has to be put back before recalculation runs.

These events are where you put it back.

### Quick overview

| Event                                   | Dispatched                                                 | Use it to                                                       |
| --------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------- |
| `OrderApprovalLineItemStructBuildEvent` | Once per line item while the snapshot is created           | Copy your payload data into the stored snapshot                 |
| `OrderApprovalCartLineItemBuildEvent`   | Once per line item while the cart is rebuilt               | Put your payload data back on the line item                     |
| `OrderApprovalCartRebuiltEvent`         | Once after the cart has been rebuilt                       | Do one lookup for the whole cart instead of one per item        |
| `OrderApprovalAddressMappedEvent`       | When a customer address is stored in the approval snapshot | Carry address data that is not in the columns into the snapshot |
| `VirtualCartContextBuiltEvent`          | After the virtual context is built, before recalculation   | Re-attach entity extensions the recalculation needs             |

The first four live in `B2bOrderApproval\Components\OrderApproval\Event`. `VirtualCartContextBuiltEvent` lives in `B2bSellersCore\Components\Checkout\Cart\Event` and applies to every virtual cart, not only order approvals.

All five extend `Symfony\Contracts\EventDispatcher\Event` and expose their data through getters.

### Preserving a value on the line item payload

The common case: your plugin writes a key to the line item payload, and your cart processor needs it to calculate a price. Use the two per-item events as a pair — one copies the value out, the other puts it back.

```php
public function onStructBuild(OrderApprovalLineItemStructBuildEvent $event): void
{
    $customFields = $event->getStructItem()->customFields ?? [];

    $customFields[self::PAYLOAD_KEY] = $event->getCartLineItem()->getPayloadValue(self::PAYLOAD_KEY);

    $event->getStructItem()->customFields = $customFields;
}

public function onCartLineItemBuild(OrderApprovalCartLineItemBuildEvent $event): void
{
    $customFields = $event->getStructItem()->customFields ?? [];

    if (isset($customFields[self::PAYLOAD_KEY])) {
        $event->getLineItem()->setPayloadValue(self::PAYLOAD_KEY, $customFields[self::PAYLOAD_KEY]);
    }
}
```

The struct item is handed to you as a live object, so writing to its properties changes what is persisted into the `order_approval.line_items` column. The same is true of the line item: it is the exact instance that is added to the cart.

{% hint style="info" %}
**Write to the top level payload, not to `payload['customFields']`.**

Shopware's `CartSerializationCleaner` filters `payload['customFields']` against the `custom_field.allow_cart_expose` allow list before a cart is stored, and the order is later persisted from that stored payload. A top level key such as `unitType` passes through untouched; a value nested inside `customFields` may not.

Note the asymmetry in the example above: `customFields` on the **struct** is fine, because the snapshot is our own JSON column and is never passed through the cleaner. It is the **cart payload** that has to be top level.
{% endhint %}

`OrderApprovalCartLineItemBuildEvent` is dispatched after every payload value the suite writes itself — `position`, `originalUnitPrice`, `originalQuantity`, `customFields`, `comment`, `options` — and after the `B2bLineItemInformation` and `LineItemBudget` extensions have been attached. Your subscriber always writes last and can override any of them.

### Doing one lookup for the whole cart

{% hint style="warning" %}
**Do not perform I/O in the per-item events.**

They fire once per line item, and the order approval refresh task rebuilds many approvals in a single run. One database query or ERP call per line item multiplies quickly: a 50-item approval becomes 50 round trips, and the task repeats that for every open and approved approval in every sales channel.

Resolve external data in a cart collector, which receives the whole cart and is the layer Shopware provides for exactly this. If you need a lookup at rebuild time, use `OrderApprovalCartRebuiltEvent` and do it once.
{% endhint %}

`OrderApprovalCartRebuiltEvent` is dispatched once, after the cart has been rebuilt and every per-item event has run. It fires even when the snapshot contained nothing to rebuild.

```php
public function onCartRebuilt(OrderApprovalCartRebuiltEvent $event): void
{
    $numbers = [];

    foreach ($event->getCart()->getLineItems() as $lineItem) {
        $numbers[] = $lineItem->getPayloadValue('productNumber');
    }

    $prices = $this->erpClient->fetchPrices($numbers); // one call, not one per item

    foreach ($event->getCart()->getLineItems() as $lineItem) {
        $lineItem->setPayloadValue('erpPrice', $prices[$lineItem->getPayloadValue('productNumber')] ?? null);
    }
}
```

### Preserving data on the address

An order approval stores its own copy of the billing and shipping address, with a newly generated id. Entity extensions on the customer address are not part of that copy, and the originating `customer_address.id` cannot be recovered afterwards — so anything you need later has to be stored at the moment the snapshot is written.

`OrderApprovalAddressMappedEvent` gives you the source address and the payload that is about to be written. Both the billing and the shipping path go through it, so one subscriber covers every way an approval is created, including creation by a sales representative and conversion from an offer.

```php
public function onAddressMapped(OrderApprovalAddressMappedEvent $event): void
{
    $extension = $event->getCustomerAddress()->getExtension('erpAddress');

    if ($extension === null) {
        return;
    }

    $payload = $event->getPayload();

    $payload['customFields']['erpAddressId'] = $extension->get('id');

    $event->setPayload($payload);
}
```

{% hint style="info" %}
The payload is a DAL write payload for `order_approval_address`. Only keys the entity definition knows may be added, so put your own data under `customFields`.
{% endhint %}

Custom fields and extensions stored on the snapshot are carried back onto the `CustomerAddressEntity` when the cart is rebuilt, so you can also register an entity extension on `order_approval_address` and read it directly instead of going through custom fields.

### Adjusting the context before recalculation

`VirtualCartContextBuiltEvent` is the last point at which the sales channel context can be changed so that cart collectors and processors see it. It applies to every virtual cart — order approvals, offers, and sales representative fast orders.

```php
public function onVirtualCartContextBuilt(VirtualCartContextBuiltEvent $event): void
{
    $address = $event->getSalesChannelContext()->getCustomer()?->getActiveShippingAddress();

    $address?->addExtension('erpAddress', new ArrayStruct(['id' => $this->resolveErpAddressId($address)]));
}
```

{% hint style="info" %}
`getSalesChannelContext()` returns the **virtual** context built for this calculation, not the caller's context. Changes affect only this calculation, which is what you want. The options are informational — the context has already been built by the time the event fires, so changing them has no effect.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.b2b-sellers.com/b2b-platform/developer-guide/how-to/cart-recalculation-extension-points.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
