Preview a coupon against a basket total
READ-ONLY preview: resolves the presented code, checks it against the basket at the current instant (validity window, remaining redemptions, minBasketCents floor) and returns the discount it WOULD grant, in cents. Writes nothing and consumes no redemption — POST /v1/checkout/finalize is what actually redeems a code. Use it to answer ‘does this code work on this basket, and for how much?’ before committing a sale. basketCents is the basket total in integer cents. A code that does not exist (or was deactivated) gives 404; a code that exists but is expired, exhausted or below the basket floor gives 422 with the reason in error.message — treat 422 as ‘the customer presented a real but unusable code’, not as a malformed request. SCOPE TRAP (tracked as solya-pos#950): the router gate is pos.checkout.operate, which is what x-required-permissions reports, but that scope is NOT sufficient. The use-case additionally asserts the kernel permission discount:apply, and a Keycloak principal derives its kernel permissions solely from its scopes — pos.checkout.operate maps to [sale:create, drawer:open, report:view], which does not include it. A token holding exactly pos.checkout.operate therefore passes the router and then 403s INSIDE the use-case. To call this today the principal needs a scope that does derive discount:apply — pos.promotion.manage or pos.loyalty.manage. Treat a 403 here as a missing grant, not a bad code.
Authorizations
Authorization: Bearer <token>. Accepts EITHER a Keycloak access token (scopes-in-token) OR an opaque POS session token; both resolve to the same pos.* scope vocabulary the route guards enforce.
Body
Response
The eligible coupon and the discount it would grant for this basket.
The eligible coupon and the discount it would grant for this basket.

