An optional HTTP presentation for the liberusoftware/ecommerce-returns module.
A return is the workflow with goods in it: a shopper asks, a merchant authorises, a parcel turns up, somebody looks at it, and a refund is written down as an amount and a reference rather than taken. This package puts that behind HTTP without adding a single rule of its own.
It is the module where a customer-reachable API is most dangerous, because the record it keeps ends in money. So the design starts from that:
A shopper may advance exactly one of the clocks — the first. Starting a return is the whole of the customer group's writing. Approving, receiving, inspecting, refusing, cancelling and resolving are somewhere else, and recording that money went back is behind a scope the operator grant does not even include.
composer require liberusoftware/ecommerce-returns-apiNeither this package nor the module it presents is on Packagist yet, so a host
needs both VCS repositories in its own composer.json — Composer honours
repositories only from the root manifest. See docs/adoption.md.
The package ships no extra.laravel.providers. Installing it boots nothing.
The host's ModuleManagerServiceProvider registers the provider when the
deployment names ecommerce-returns-api in MODULES_ENABLED — and even then no
route exists until returns-api.groups names one.
MODULES_ENABLED=ecommerce-returns,ecommerce-returns-api
RETURNS_API_GROUPS=staffTwo decisions, deliberately. Enabling the module is not consenting to publish an API; publishing the operator's surface is not consenting to publish a shopper's.
| Group | Operations |
|---|---|
customer |
POST /returns · GET /returns/{number} |
staff |
GET /staff/returns · GET /staff/returns/{number} · POST …/approval · POST …/transitions · POST …/receipts · POST …/inspection · POST …/refunds |
Both are mounted under {prefix}/{version}, both configurable. Neither is
reachable anonymously.
The likeliest composition in the fleet is staff alone: returns are raised by
the host's own pages — an in-process call — and staff work them over the wire.
The shopper routes are then not merely denied, they do not exist.
Whether a unit may come back is delivered − already returned, and both
counters live in whatever owns order lines. The module refuses to look them up
and takes the answer as an input. This package is the same rule one layer
out: it reads the answer off the request, put there by middleware the
deployment writes.
$request->attributes->set('returns.customer_id', $request->user()?->id);
$request->attributes->set('returns.order_id', $order->id);
$request->attributes->set('returns.currency', $order->currency);
$request->attributes->set('returns.team_id', $order->team_id);
$request->attributes->set('returns.store_id', $order->store_id);
$request->attributes->set('returns.eligibility', [
['order_line_id' => 91, 'returnable_quantity' => 2, 'name' => 'Rain Coat', 'sku' => 'RC-001'],
]);A shopper who could state what is returnable could start a return for goods that
were never delivered, and set the merchant's refund arithmetic going against it.
So a body carrying returnable_quantity is refused, not ignored — and a
line the host did not resolve is a line with nothing returnable, refused by the
module in its own words.
The consequence is worth saying plainly: a deployment that publishes the shopper group and resolves nothing publishes a surface that accepts no return. That is the direction a mistake here should fail in.
This package imports no sibling commerce module and requires none. An order line id and a quantity are numbers; that is the entire integration in each direction.
Goods coming back have to raise the returned counter on the order line they came from, and that counter belongs to whoever owns order lines. This module does not reach sideways to do it, and neither does this API. The module publishes an event and the host subscribes:
// app/Providers/AppServiceProvider.php
use Liberu\Ecommerce\Orders\Actions\AccountForLine;
use Liberu\Ecommerce\Orders\Enums\LineAccount;
use Liberu\Ecommerce\Orders\Queries\OrderQuery;
use Liberu\Ecommerce\Returns\Events\ReturnGoodsReceived;
Event::listen(function (ReturnGoodsReceived $event) {
foreach ($event->receipts as $receipt) {
$line = app(OrderQuery::class)->line($receipt->orderLineId);
if ($line !== null) {
app(AccountForLine::class)->handle($line, LineAccount::Returned, $receipt->quantity);
}
}
});The host is the only place entitled to know that both modules exist. The receipts on that event are deltas, not totals: a second parcel dispatches it again with the second quantity, and the counter on the far side is append-only too, so a total posted twice is double the goods.
The same shape applies to stock. An inspection publishes what is saleable again;
the host subscribes to ReturnInspected and hands the dispositions to whatever
owns the ledger. Nothing here writes stock.
- A shopper advances one clock. Everything else is refused by name, and each refusal has a test.
- Recording money is its own scope.
returns:refunds.writeis not part ofreturns:staff.write; an integration issued the whole operator grant still cannot write a figure into an accounting record. - The authorisation number is capability-adjacent. Minted from the platform's random source, never accepted from a client, and every failed lookup answers identically — no such return, somebody else's, another team's. The limiter is the other half of that and is on by default.
- No free text anywhere. A return reason is a closed set of slugs, a transition reason is a slug of at most 64 characters, a refund reference admits no whitespace. The module's one free-text field is written by nothing here.
- Two projections, not one with fields blanked. A shopper does not get the row id, the tenant, the inspection dispositions, or the refund reference.
- Expiry is not publishable. The module grants no ability for it and a
host's schedule discovers it. See
docs/runbook.md.
docs/domain.md— what this transport decides, what it refuses to decide, and where every judgement call landed.docs/adoption.md— wiring it into a host.docs/runbook.md— the sweeps, the reconciliation, and what to do when a number disagrees.resources/openapi/returns.json— an OpenAPI 3.1 fragment, held against the registered routes by a test.
MIT. See LICENSE.md.