Metadata-Version: 2.4
Name: stapel-orders
Version: 0.1.0
Summary: Cart, checkout and orders for the Stapel framework
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-orders
Project-URL: Repository, https://github.com/usestapel/stapel-orders
Project-URL: Documentation, https://github.com/usestapel/stapel-orders#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-orders/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-orders/issues
Keywords: django,stapel,orders,cart,checkout,ecommerce
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: stapel-core<1.0,>=0.95.0
Provides-Extra: all
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-django; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: stapel-tools<1,>=0.76.0; extra == "test"
Requires-Dist: jsonschema>=4; extra == "test"
Dynamic: license-file

<!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->

# stapel-orders

[![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-orders/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-orders/actions/workflows/ci.yml?query=branch%3Amain)
[![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-orders?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-orders)
[![pypi](https://img.shields.io/pypi/v/stapel-orders?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-orders/)
[![downloads](https://static.pepy.tech/badge/stapel-orders/month)](https://pepy.tech/project/stapel-orders)
[![python](https://img.shields.io/pypi/pyversions/stapel-orders?logo=python&logoColor=white)](https://pypi.org/project/stapel-orders/)
[![license](https://img.shields.io/github/license/usestapel/stapel-orders)](https://github.com/usestapel/stapel-orders/blob/main/LICENSE)
[![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-orders/blob/main/docs/llms.txt)

> Cart, checkout and orders over opaque purchasables. A host registers what can be bought (purchasable_type -> a keyed-batch resolver Function answering title, price, currency, availability); the cart snapshots each line when it is added, so reading a cart never calls the product owner. Checkout re-prices every line, refuses with the refreshed cart when anything drifted, and places an order idempotently on a client key, emptying the cart and announcing orders.order.placed with its lines. Orders follow a staff-driven lifecycle with buyer cancel; payment is a provider seam (none, mock) whose outcome arrives as the payment.completed fact.

Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.

## Install

```bash
pip install stapel-orders
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.1.0` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 10 |
| Config axes | 2 |
| Usage surface | 11 |
| Extension points | 7 |
| Error codes | 54 |
| Documented flows | 4 |
| Fleet dependencies | [`stapel-auth`](https://github.com/usestapel/stapel-auth) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) · [`stapel-currencies`](https://github.com/usestapel/stapel-currencies) (optional) · [`stapel-listings`](https://github.com/usestapel/stapel-listings) (optional) |

## Documentation

[OpenAPI](https://github.com/usestapel/stapel-orders/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-orders/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-orders/blob/main/docs/llms.txt)

## What this is

Cart, checkout and orders for a Stapel shop — over **opaque purchasables**.
The module ships knowing nothing about what is sold: a host registers each
purchasable type with a comm Function that resolves a keyed batch of keys to
title, price, currency and availability. Orders never imports a product
model, so a listing, a spare part from a custom app, or anything else is
bought through one cart.

## Quick start

```python
INSTALLED_APPS = [
    # ...
    "stapel_orders",
]

STAPEL_ORDERS = {
    "PURCHASABLE_TYPES": {
        "listing": {"resolve": "listings.purchasables"},  # stapel-shop preset default
    },
}

# urls.py
path("orders/api/", include("stapel_orders.urls"))   # -> /orders/api/v1/...

# settings.py — the abandoned-cart purge (Celery optional)
from stapel_orders.beat import get_orders_beat_schedule
CELERY_BEAT_SCHEDULE = {**get_orders_beat_schedule()}
```

A custom product type is one resolver and one registration:

```python
from stapel_core.comm import function

@function("parts.purchasables")
def purchasables(payload):
    parts = Part.objects.filter(pk__in=payload["keys"])
    return {str(p.pk): {"title": p.name, "price": str(p.price), "currency": "USD",
                        "available": p.stock > 0, "max_quantity": p.stock,
                        "seller_id": None, "image": None, "url": f"/parts/{p.pk}"}
            for p in parts}

# AppConfig.ready()
from stapel_orders.registry import register_purchasable_type
register_purchasable_type("part", {"resolve": "parts.purchasables"})
```

## Concepts

- **Purchasable** — an opaque pair `(purchasable_type, purchasable_key)`.
  The resolver answers on add and at checkout only; a cart line is a
  snapshot, so `GET cart/` makes zero comm calls.
- **Cart** — one per user. A stapel-auth guest is a user too, and
  `user.merged` folds the guest's cart into the account on sign-in (same
  item summed, capped at `MAX_LINE_QUANTITY`). One currency per cart; a line
  in another currency is refused, never converted.
- **Checkout** — one idempotent call (`idempotency_key`). Every line is
  re-priced; a changed price, a vanished item or short stock refreshes the
  cart and refuses with it in `params.cart`, so the shopper is never charged
  a number they did not see. Guests are asked to sign in unless
  `CHECKOUT_REQUIRES_ACCOUNT` is off.
- **Order** — `new → confirmed → shipped → completed`, cancel from `new` by
  the buyer (`OWNER_CANCELLABLE_STATUSES`), staff for the rest, `needs_review`
  when a payment does not match. History is anchor-paginated.
- **Money** — `Decimal` inside, strings on the wire. Line and cart limits
  are a proof that a total cannot overflow its column
  (`tests/test_money_bounds.py`, system check `E005`).
- **Payment** — a provider seam: `none` (staff confirm by hand) or `mock`
  (prototype posture only). The outcome always arrives as
  `payment.completed`, the one path that marks an order paid.
- **GDPR** — erasure deletes carts and pseudonymizes the person on orders;
  amounts stay.

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-orders/blob/main/LICENSE).

---

<sub>This page is assembled by `stapel-readme` from `docs/readme.md` plus the contract artifacts in `docs/`. Edit the prose in `docs/readme.md`; the badges, facts and links above and below it are generated — do not hand-edit `README.md`.</sub>
