Light API 0.4.0 is a public beta: a minor release such as 0.5.0 may still break. What that means for your module
Light
Developer guideRecipes

Order and customer pages

Order actions, timeline lines, totals rows and cards on the order and customer pages.

The Light order and customer pages take actions, timeline lines, totals rows and cards from other modules. Returns uses all of them; this recipe follows it.

When to use it

  • Your module adds a step to handling an order (create a return, print a label), a fact to its history, or a line to its totals.
  • Add an action to More actions unless it is the next step for most orders, and a card only when the timeline can't say it (Built for Light, rule 2).

Steps

1. Order actions

An ActionPool item (pool order_actions) in etc/adminhtml/di.xml adds a button to the order page. condition names a class that implements OrderActionConditionInterface, so the button shows only when it applies:

app/code/Mrx/Returns/etc/adminhtml/di.xml
<item name="create_return" xsi:type="array">
    <item name="label" xsi:type="string" translate="true">Create return</item>
    <item name="route" xsi:type="string">light/returns/new</item>
    <item name="params" xsi:type="array">
        <item name="order_id" xsi:type="string">entity_id</item>
    </item>
app/code/Mrx/Orders/Api/OrderActionConditionInterface.php
public function isAvailable(OrderInterface $order): bool;

The keys: label; route with params (a route parameter mapped to an order field: entity_id, increment_id, customer_id or store_id), or id and attributes for a JS action; icon, tone, resource, module, slot (more or secondary), sort_order (core uses 5 to 90) and condition. A condition that throws is logged and drops its action. A route in the route map gets mrx_stock=1, so an action that opens a stock screen keeps the Light shell around it.

Prop

Type

2. Timelines

A class that implements the Orders or Customers TimelineProviderInterface adds lines to the order's or customer's history. Register it in the Timeline argument providers of Orders (pool order_timeline) or Customers (pool customer_timeline). Returns adds its lines to the customer timeline:

app/code/Mrx/Returns/etc/adminhtml/di.xml
<item name="returns" xsi:type="object">Mrx\Returns\Model\Customer\ReturnsTimeline\Proxy</item>
app/code/Mrx/Orders/Api/TimelineProviderInterface.php
public function getEvents(OrderInterface $order): array;

A link on a line takes external for a URL outside the admin; unsafe URLs show as text. Light merges your lines with core's before it sorts them. When the line must also show in the stock order history and outlive your module, write it with Mrx\Orders\Model\Service\History::addEvent() instead. A Customers provider gets the customer as its argument: don't inject CustomerPageContextInterface into it or into the services it builds, which would be a circular dependency. A customer mail about the order gets a View email button on its own line, which opens the copy Light kept of it; a provider line can't open that viewer, so for a mail of yours write a notified history row instead (Copies of sent mails).

3. Totals rows

A class that implements TotalsProviderInterface adds rows to the order totals, registered in the TotalsPool argument providers (pool order_totals). When the rows and the grand total differ by more than 0.005, Light adds "Other charges" (other_charges) or "Other adjustments" (other_adjustments) itself, so the totals always add up. The codes subtotal, discount, shipping, tax, grand_total, other_charges and other_adjustments are core's: a row that reuses one, or whose amount is not a finite number, drops every row of its provider, so take a code with your module's prefix. Light uses fpt itself: while Magento_Weee is on, a fixed product tax gets its own row after the subtotal, and the subtotal no longer holds it.

app/code/Mrx/Orders/Api/TotalsProviderInterface.php
public function getRows(OrderInterface $order): array;

4. A card with its own controller

Order and customer pages have no form pipeline. The order page has four containers, mrx.order.main.top (for what needs action), mrx.order.main.before_timeline, mrx.order.aside.top and mrx.order.aside.bottom; the customer page has mrx.customer.main.bottom and mrx.customer.aside.bottom. A card there gets the page's order or customer from OrderPageContextInterface or CustomerPageContextInterface, posts to its own controller, and sets aclResource, so an admin without the resource never sees a link to a page they can't open:

app/code/Acme/LightProof/view/adminhtml/layout/light_orders_view.xml
<referenceContainer name="mrx.order.aside.bottom">
app/code/Mrx/Orders/Api/OrderPageContextInterface.php
public function getOrder(): ?OrderInterface;

5. Carriers

A carrier is an item in the pool carriers (an argument of Mrx\Orders\Model\Carrier\Carriers, global area). Light recognises the carrier from a tracking code, builds the tracking link, and offers the carrier in the Ship items form. Light for the Netherlands (Mrx_CountryNl) adds PostNL this way:

packages/module-country-nl/etc/di.xml
<item name="postnl" xsi:type="array">
    <item name="label" xsi:type="string">PostNL</item>
    <item name="patterns" xsi:type="array">
        <item name="domestic" xsi:type="string">^3S[A-Z0-9]{8,20}$</item>
        <item name="international" xsi:type="string">^[A-Z]{2}[0-9]{9}NL$</item>
    </item>
    <item name="tracking_url" xsi:type="string">https://jouw.postnl.nl/track-and-trace/{code}-{country}[-{postcode}]</item>
    <item name="sort_order" xsi:type="number">10</item>
    <item name="select_order" xsi:type="number">10</item>
</item>
  • The item's key (postnl) is the carrier's code, a string.
  • patterns are regular expressions for the tracking code. Detection tries the carriers by sort_order, and the first match wins.
  • tracking_url fills {code}, {country} and {postcode}, and drops a [bracketed] part whose value is empty.
  • The Ship items form offers the carriers that have a select_order, in that order. A carrier without one is detected but not offered.
  • disabled and module work as in every pool; a null item removes a carrier.

6. Test payments and refunds

A payment module tells Orders that a payment was made in its test mode, and Orders warns for any provider. Store the mode in the payment's additional information under Mrx\Orders\Api\PaymentMode::INFO_KEY, with PaymentMode::TEST or PaymentMode::LIVE. Mrx_PaymentsMollie does it when Mollie reports the payment:

packages/module-payments-mollie/Observer/RecordMollieMode.php
$order->getPayment()?->setAdditionalInformation(PaymentMode::INFO_KEY, $mode === ApiKey::MODE_TEST ? PaymentMode::TEST : PaymentMode::LIVE);

While the order is open, the order page shows "Test payment: no money received" in the container mrx.order.main.top, naming the provider by the label of its payment_providers item (Payment providers). The orders list gets a "Test payment" column after "Payment", hidden until an open order was paid in test mode; it comes from Mrx\Orders\Model\IndexTable\TestPayments, which Orders registers in the pool table_extensions for the list orders. A provider adds neither. The count that decides whether the column shows is cached for up to an hour; an order save that changes the count (a test order placed or changing state, a test mark set or removed) drops that cache, so save the order through Magento after you set the mode, as Mollie's and Pay.'s own handling does. A mode written straight to the database shows on the order page at once and in the list within the hour.

A refund from the order page fails in Magento's RefundInvoice with "Could not save a Creditmemo", whatever went wrong. A payment module that refuses a refund sets one sentence on Mrx\Orders\Api\RefundReasonInterface before it throws, and the refund then fails with that sentence:

packages/module-payments-mollie/Plugin/MollieRefund.php
$this->refundReason->set((string)$message);
throw new LocalizedException($message);

What the admin sees

An order page with the proof module's card in the side column

A customer page with the pilot's Account request card in the side column

  • Simple mode. The action in the order header (or under More actions), the timeline lines with their links, the totals rows, and the card in the side column.
  • Advanced mode. The stock order view, without any of these.

ACL

Each action, timeline provider and card checks its own resource or aclResource; the order page itself needs the Orders resource.

Check it

  • bin/magento mrx:light:doctor --module=<your module>: unknown_container and unknown_key.
  • ?mrx_slots=1 outlines the containers.
  • tests/playwright/light-proof.spec.ts test 6 checks an order card.

Pitfalls

  • A card that loads its content after the page, or refreshes it after an action, wraps the request in loader.during(card, request, {preset: 'card'}) from Mrx_Light/js/loader, and its buttons in loader.button(), instead of a "Loading" text (Design system, Loading states).
  • A condition or provider class that needs another module's classes is registered as \Proxy, with #[RequiresModule].
  • A timeline provider that throws is logged and left out; the rest of the timeline still renders. A provider or condition that throws goes to var/log/exception.log; an invalid row or an unsafe URL goes to var/log/system.log as a warning.

Last updated on

On this page