deployecommerce/module-order-status-audit-log

Records every Magento order status change: what it was, what it became, who changed it, from where, when, and from which IP address.

Maintainers

Package info

github.com/DeployEcommerce/module-order-status-audit-log

Homepage

Type:magento2-module

pkg:composer/deployecommerce/module-order-status-audit-log

Transparency log

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-08-07 11:30 UTC

This package is auto-updated.

Last update: 2026-08-07 11:30:50 UTC


README

Records every order status change: what it was, what it became, who changed it, from where, when, and from which IP address.

"Why is this order marked complete?" is a question Magento cannot answer. The status history shows that a status changed, sometimes, and never says who did it or how. This module answers it.

composer require deployecommerce/module-order-status-audit-log
bin/magento module:enable DeployEcommerce_OrderStatusAudit
bin/magento setup:upgrade

What an entry looks like

In the order's Comments History:

[STATUS-AUDIT] from=processing to=dispatched by=admin:jbloggs#7 area=adminhtml
ip=203.0.113.9 at=2026-08-07T14:22:31+00:00 trigger=sales_order_shipment_save
state=processing->complete cause=magento

One line, fixed field order, parseable with explode(' ') then explode('=', $part, 2). Values are stripped of whitespace, = and |, so nothing in a username or a header can forge an extra field.

Field
from / to The status either side. [new] when the order had just been placed
by admin:jbloggs#7, integration:Warehouse#3, customer:#42, guest, [cron:cron:run], [cli:sales:clean:quotes], [system]
area adminhtml, frontend, webapi_rest, crontab, …
ip The request address, or [system] under cron and the CLI
at ISO 8601, UTC
trigger The entry point: a controller action, POST:/V1/order/12/ship, or a console command
state Only when the order's state also moved
cause magento when Magento rewrote the status itself; placement for an order's first status

cause=magento

Magento moves orders between statuses on its own — processing becomes complete once nothing is left to ship, and either becomes closed once nothing is left to refund. No caller asks for it.

Without distinguishing those, an audit trail tells you an admin set the order to complete when in fact they created a shipment and Magento drew the conclusion. That is the difference between an audit trail and a list of events.

Where entries are written

Two places, independently switchable, both on by default.

Order comments. Where admins already look. They are written inside the order's own database transaction, so an entry exists if and only if the change it describes was committed — and rolls back with a failed save. They are not visible to customers.

A table of its own, deployecommerce_order_status_audit. This is the copy that lasts:

  • No foreign key on order_id, so entries survive order deletion. Order comments cascade away with the order; these do not.
  • The actor is resolved to a name when the change happens, not looked up later, so entries survive the admin user being deleted too.
  • Indexed on order, increment ID, actor, status and date, so "every change by this admin in March" is a query rather than a full scan of a text column.

Turning the table off leaves you with a trail that lasts exactly as long as the orders do. That is a reasonable choice for operational debugging and a poor one for compliance.

Configuration

Stores → Configuration → Advanced → Order Status Audit Log

Setting Default
Enabled Yes
Write To Order Comments Yes
Write To Audit Table Yes The copy that survives order deletion
Record Order Placement Yes One extra entry per order. Without it the trail cannot show what an order started as
Statuses To Ignore Changes into or out of these are skipped, for a status some integration flips constantly

Default scope only. Audit policy is a property of the installation; scoping it per store view would leave you unable to say which policy applied when an entry was written.

What is and is not recorded

Detection is handled by deployecommerce/module-order-status-transition, which covers everything that goes through a normal order save: the admin, mass actions, the API, shipments, invoices, refunds, payment webhooks, cron, the CLI, and the statuses Magento assigns itself.

It cannot see writes that bypass the model layer — raw SQL, and bulk import tools that write straight to the sales_order table. No event-based approach can. See that module's README.

Two related caveats worth knowing:

  • Behind a CDN the recorded IP is only as trustworthy as Magento's trusted-proxy configuration. With none configured, Magento takes the first X-Forwarded-For value, which the client controls.
  • Anything with database access can write rows that look like audit entries, including order import tools. The comment sink in particular shares a table with ordinary order comments.

Record both, and treat them as a strong account of what happened rather than as evidence.

Adding your own destination

A sink is a destination. It can filter, so it does not have to care about every change — here, one that alerts a channel when a parcel comes back:

use DeployEcommerce\OrderStatusAudit\Api\TransitionSinkInterface;
use DeployEcommerce\OrderStatusTransition\Api\Data\ActorInterface;
use DeployEcommerce\OrderStatusTransition\Api\Data\TransitionInterface;
use Magento\Sales\Model\Order;

class ReturnAlertSink implements TransitionSinkInterface
{
    public function record(Order $order, TransitionInterface $transition, ActorInterface $actor): void
    {
        if ($transition->getTo() !== 'return_to_sender') {
            return;
        }

        $this->notifier->send(sprintf(
            'Order %s came back — %s set it via %s',
            $transition->getIncrementId(),
            $actor->getLabel(),
            $actor->getTrigger()
        ));
    }
}
<type name="DeployEcommerce\OrderStatusAudit\Model\Sink\CompositeSink">
    <arguments>
        <argument name="sinks" xsi:type="array">
            <item name="return_alert" sortOrder="30" xsi:type="object">My\Module\Model\ReturnAlertSink</item>
        </argument>
    </arguments>
</type>

Sinks run inside the order's save transaction. They may attach data to the order, which Magento persists as part of the same save; they must never call save() on it. A sink that throws is logged and skipped — losing one destination must not cost you the others, and an audit log must never break a checkout.

Development

composer install
composer test

Tests are written with Pest and need no Magento installation. Magento's own packages come from the public Mage-OS mirror, so no Adobe credentials are required.

Licence

MIT. See LICENSE.md.