Skip to content

Repository files navigation

Ecommerce Returns API

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.


Installing

composer require liberusoftware/ecommerce-returns-api

Neither 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=staff

Two 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.


The two groups

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.


Eligibility is an input, and a caller does not supply it

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.


What crosses a boundary, and what the host has to write

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.


Security, in one place

  • 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.write is not part of returns: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.

Documents

Licence

MIT. See LICENSE.md.

About

This optional API presentation package exposes approved HTTP operations for the Returns domain module. It presents exactly one independent module, delegates all authoritative behavior to that module's public actions/queries/policies, and contains no other module's API logic.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages