manicanldes-backend/docs/checkout-api.md

6.0 KiB
Raw Permalink Blame History

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 but expose pricingStatus UNFINALIZED, paymentAvailable false, and null taxTotal, shippingTotal and payableTotal. 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. Before enabling payment, finalize and snapshot those rules through a new migration and implement the verified payment lifecycle in Phase 1E. 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.