manicanldes-backend/docs/checkout-api.md

50 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 1D: carts, coupons and orders
All paths below start with /api/v1. Customer routes require an active bearer session and operate only on that user's organization and data. Customers do not need staff inventory permissions. Accounts currently use the existing provisioning flow.
| Method and path | Access |
| -------------------------------------------- | --------------------------------------------------------------------------- |
| GET /cart | Own cart |
| PUT /cart/lines/:variantId | Own cart; body: quantity, version |
| DELETE /cart/lines/:variantId | Own cart; body: version |
| POST /checkout | Own cart; body: cartVersion, addressId, idempotencyKey, optional couponCode |
| GET /orders, GET /orders/:id | Own orders |
| POST /orders/:id/cancel | Own order |
| GET /admin/orders, GET /admin/orders/:id | orders.read |
| POST /admin/orders/:id/cancel | orders.manage |
| GET/POST /coupons, PATCH /coupons/:id/status | coupons.manage |
Lists accept limit (1100, default 25) and offset (010000). Staff order lists return summaries; private address snapshots are available only in authorized detail responses. New permissions are granted to existing system roles by migration; custom roles keep their explicit permissions.
## Cart and checkout contract
GET /cart returns version and lines with current product information. PUT sets an absolute quantity of 1100. A cart contains at most 20 variants in one currency. Every edit requires the last version and increments it. Stale writes fail with CART_CHANGED. A cart does not hold stock.
POST /checkout requires a UUID idempotencyKey and the expected cartVersion. It rechecks product availability, calculates current server prices, validates a private saved address and any coupon, creates immutable snapshots and reserves stock for 15 minutes. It then clears the cart and increments its version. These writes and the audit record share one transaction; failures preserve the cart and roll back orders and holds.
A matching retry returns the original order with its current status even after cart clearing. Reusing the same key with changed input returns IDEMPOTENCY_CONFLICT. Keys are scoped to the user and organization. Use a new key for a new checkout intent.
Orders snapshot SKU, product/variant names, unit prices, quantities, line totals, currency, coupon rules, subtotal, discount, merchandise total and address. Later catalog/address edits do not alter them. Decimal output strings can omit trailing zeroes. Calculations use integer minor units and percentage discounts round half up to the nearest minor unit. SQL enforces line arithmetic and reconciles order subtotals at transaction commit.
## Pricing and payment boundary
Orders start as PENDING_PAYMENT. Without a matching active pricing policy, pricingStatus is UNFINALIZED and taxTotal, shippingTotal and payableTotal are null. Phase 1E adds optional finalized pricing snapshots; see [pricing configuration](operations-api.md). paymentAvailable remains false in all cases. Merchandise total is subtotal minus discount; it is not a final amount to charge. Null charges must never be rendered as free shipping or zero tax.
Tax and shipping rules have not been supplied. This phase makes no assumption about tax treatment or delivery charges and does not create payments. Configure approved pricing policy versions before using finalized totals. Real payment integration is deferred at the user request; follow the Phase 1E blueprint before enabling it. A fully discounted order still requires that workflow.
## Coupon rules
POST /coupons accepts code, currency, minimumSubtotal, maxUses, perUserLimit, startsAt and endsAt. FIXED coupons also require a positive amount string with two fractional digits. PERCENT coupons require percentBps from 1 to 10000; 1000 means 10%. Codes are normalized to uppercase. Dates require explicit timezone offsets. Ends must follow starts.
Only one coupon can apply to an order. It must be active, within its date window, in the cart currency, above the minimum subtotal and within total and per-user usage limits. Fixed discounts are capped at subtotal. Rules are immutable; create a new code to change them. PATCH status accepts only active.
Usage is held by unexpired pending orders. Cancellation and expiry free that capacity. Future paid-order redemption must remain counted when payments are introduced. Ineligible customer responses remain generic; logs carry distinct diagnostic reasons without exposing private data.
## Inventory and order lifecycle
Checkout selects available stock across warehouses in stable stock-ID order, with a maximum of 200 candidate stock items per checkout. Stock row locks coordinate with standalone reservations and adjustments. An account may have at most ten unexpired pending orders.
Reservations reduce availability, not on-hand stock. Order holds cannot be committed/released through standalone inventory APIs. Cancellation releases all holds atomically and is idempotent; it does not rebuild the cart. After 15 minutes, pending orders display EXPIRED and their holds stop consuming availability without a cleanup job. Expired orders are not payable. The later payment phase must recheck expiry and define stock commitment before fulfillment.
The current organization lock serializes checkout with catalog, address, coupon and cart writes. This favors correctness within the existing architecture; benchmark contention on native PostgreSQL before scaling traffic. Native tests cover duplicate requests, coupon competition and checkout versus standalone inventory reservation.