The HTTP surface over
liberusoftware/ecommerce-loyalty:
a person's earned standing with a merchant, and the append-only ledger that
standing is replayed from.
It presents the domain. It reimplements none of it, holds no business rule, and reads none of the domain's Eloquent models — everything goes through the actions, queries and value objects that package publishes.
There is no balance column behind this API. A balance is a replay over an
ordered ledger of lots, and the replay can fail. So a balance is published with
reconcilable on its face, and when that is false points is null — never
0. "You have no points" and "we could not work out how many points you have"
are different sentences, and rendering the second as the first is a customer told
their points are gone.
The merchant is never accepted and the subject depends on the ability. The
merchant comes from the credential, always. The subject comes from the credential
under loyalty:read and loyalty:redeem, and from the request body under
loyalty:operate and loyalty:privacy. Which applies is decided by the ability
an endpoint requires and never by anything a caller sends.
There is no idempotency key. Every write already has a natural key the
database enforces: an award on its cause, an enrolment on the subject and
programme, a redemption on its cause. A key the client holds is a key the client
can change, so one layered on top would be strictly weaker than the constraint
already there. Send your own order reference as cause_ref and retry freely.
Mounted at api/loyalty by default.
| Ability | ||
|---|---|---|
GET /programmes/{programme} |
loyalty:read |
Currency, timezone, earn rate, point value |
GET /programmes/{programme}/rewards |
loyalty:read |
What is on offer today, cheapest first |
GET /programmes/{programme}/balance |
loyalty:read |
The caller's own, replayed |
GET /programmes/{programme}/statement |
loyalty:read |
The caller's own ledger, with the lots each debit drew from |
GET /tiers/{tier}/membership |
loyalty:read |
Is the caller currently in this tier |
POST /programmes/{programme}/redemptions |
loyalty:redeem |
Spend the caller's own points |
POST /programmes/{programme}/enrolments |
loyalty:operate |
Join a named subject |
POST /programmes/{programme}/awards |
loyalty:operate |
Put a lot in the ledger |
POST /programmes/{programme}/tier-evaluations |
loyalty:operate |
Judge a named subject against the tiers |
POST /programmes/{programme}/expiry-runs |
loyalty:operate |
Retire lapsed lots |
GET /programmes/{programme}/liability |
loyalty:operate |
What the merchant owes |
POST /subject-records |
loyalty:privacy |
Export one subject's whole record |
POST /erasures |
loyalty:privacy |
Erase the subject, keep the arithmetic |
The OpenAPI 3.1 document is at resources/openapi/openapi.json, and the suite
asserts parity with the router in both directions — including that every
operation documents the ability the controller actually enforces.
Every error body is {"error": {"code", "message", "resubmittable"}}.
resubmittable is the whole classification, and there are only two ways to get
it: 422, the input was wrong, and 503, a seam this deployment has not
bound was needed and nothing was written. Everything else — 404 and 409 — reached
a decision that will not change.
Nothing here is transient, so there is no 429, no 423 and no retry header anywhere. A courtesy retry prompt on a permanent refusal is a lie a surface tells on its domain's behalf, and the suite asserts no message here contains one.
A programme, a tier, a reward or a membership belonging to another merchant answers 404, byte for byte as one nobody ever minted. There is no 403 for a reference that is not yours: a 403 confirms it exists.
Needs the VCS repository entry for the domain package in the host's root
composer.json — Composer honours repositories only from there.
composer require liberusoftware/ecommerce-loyalty-api
Composer boots nothing. Enable ecommerce-loyalty and ecommerce-loyalty-api in
MODULES_ENABLED.
Read docs/adoption.md before issuing a token: which
abilities go where, what each unbound seam means, and the two things a caller
must not collapse.
docs/domain.md— what this surface decided, and whydocs/adoption.md— what a host has to dodocs/runbook.md— what to do when it misbehaves
MIT.