Metadata-Version: 2.4
Name: stapel-billing
Version: 0.21.1
Summary: Billing and payments Django app for the Stapel framework
License: MIT
Project-URL: Homepage, https://github.com/usestapel/stapel-billing
Project-URL: Repository, https://github.com/usestapel/stapel-billing
Project-URL: Documentation, https://github.com/usestapel/stapel-billing#readme
Project-URL: Changelog, https://github.com/usestapel/stapel-billing/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/usestapel/stapel-billing/issues
Keywords: django,stapel,billing,payments,stripe
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
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.85.1
Provides-Extra: stripe
Requires-Dist: stripe<12,>=8.0; extra == "stripe"
Provides-Extra: celery
Requires-Dist: celery>=5.3; extra == "celery"
Provides-Extra: all
Requires-Dist: stapel-billing[celery,stripe]; extra == "all"
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-billing

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

> Payments and billing: per-user credit wallets held as expiry-aware credit lots over an immutable ledger, reservations (hold/capture/release) for work priced only after it runs, partial charges that record what a wallet could not cover as a collectable debt, plan bundles granted with or without a payment provider, one-off credit packages and subscription plans, Stripe-backed checkout, customer portal and refund clawback, idempotent webhooks and a service-to-service debit endpoint.

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-billing
```

## At a glance

| Fact | Value |
|---|---|
| Version | `0.21.1` |
| Python | `>=3.11` (3.11, 3.12, 3.13, 3.14) |
| HTTP operations | 11 |
| Config axes | 1 |
| Usage surface | 69 |
| Extension points | 6 |
| Error codes | 56 |
| Fleet dependencies | [`stapel-auth`](https://github.com/usestapel/stapel-auth) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) |

## Documentation

**Errors:** [English](https://github.com/usestapel/stapel-billing/blob/main/docs/errors.en.md) · [Español](https://github.com/usestapel/stapel-billing/blob/main/docs/errors.es.md) · [Русский](https://github.com/usestapel/stapel-billing/blob/main/docs/errors.ru.md) · [OpenAPI](https://github.com/usestapel/stapel-billing/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-billing/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-billing/blob/main/docs/llms.txt)

## Quick start

```python
# settings.py
INSTALLED_APPS = [
    ...
    'stapel_billing',
]

from stapel_billing.tasks import get_billing_beat_schedule

CELERY_BEAT_SCHEDULE = {
    **get_billing_beat_schedule(),   # credit expiry, hold sweep, reconcile
}
```

Register the beat schedule (or run the three callables in `stapel_billing.tasks`
from your own cron): credits granted with an expiry only expire because
something runs. System check `stapel_billing.W105` says so if nothing does.

## Wallets are lots

A wallet is a set of `CreditLot` rows — each one knows where its credits came
from (`purchase`, `subscription`, `grant`, `adjustment`, `hold_release`) and
when they die (`expires_at`, `NULL` = never). Spend walks the lots
**expiring-soonest first**, so a subscription bundle is used before the
non-expiring credits a customer paid cash for. `Wallet.balance` is a
maintained cache of that total, recomputed from the lots inside the same row
lock as every mutation.

```python
from stapel_billing import credit, debit, hold, capture, release
from stapel_billing.models import LotSource, TransactionType

credit(user=user, credits=3000, type=TransactionType.SUBSCRIPTION_BONUS,
       source=LotSource.SUBSCRIPTION, expires_at=sub.current_period_end)

held = hold(user=user, credits=15, type=TransactionType.AI_CHARGE,
            idempotency_key=f"mic:{recording_id}")
try:
    used = do_the_work()
except Exception:
    release(hold_id=held.id)      # credits go back with their original expiry
else:
    capture(hold_id=held.id, actual_credits=used)
```

## Granting credits by hand

Credits move without a payment more often than a payments library likes to
admit: staff testing the product, goodwill after an outage, an invoiced
agreement. There is one audited path for it, reachable from a terminal —

```
manage.py billing_grant_credits --account <id|e-mail> --credits 100 \
    --reason "staff testing" --actor ops@example.com \
    --idempotency-key grant-2026-09-16-01
```

— and the same service (`services.grant_credits`) behind the admin's **Grant
credits** action, so a grant made in a browser and a grant made over ssh are
the same ledger row: type `adjustment`, never expiring, carrying the reason
**and** the actor. The command refuses an unknown or ambiguous account, a
non-positive amount and an empty reason or actor with a non-zero exit; with an
idempotency key a repeat grants once and says so.

## Your own staff should not have to pay to test

```python
STAPEL_BILLING = {"INTERNAL_ACCOUNT_POLICY": "meter_only"}
```

An internal account is then **metered but not charged**: `debit`, `can_afford`,
`hold` and `capture` all still run, the ledger row is still written with its
type, description and metadata — and `credits_delta` is `0`, with
`metadata.waived_credits` recording what it would have cost. No credits move,
no `CreditDebt` is opened, and "what did our own testing consume this month"
is still a query. Off by default; who counts as internal is
`INTERNAL_ACCOUNT_RESOLVER` (default `is_staff or is_superuser`).

## Bus events

### Emits
| `payment.completed` | [schema](schemas/emits/payment.completed.json) | A payment transaction completed successfully. |
| `payment.failed` | [schema](schemas/emits/payment.failed.json) | A payment attempt was declined. Nothing was granted, so nothing is clawed back — the fact exists to be told to the payer. |
| `subscription.changed` | [schema](schemas/emits/subscription.changed.json) | User subscription plan or status changed. Carries `cancel_at_period_end`, which no consumer can infer from `status`. |

### Consumes
| `user.deleted` | [schema](schemas/consumes/user.deleted.json) |
| `user.deletion_initiated` | [schema](schemas/consumes/user.deletion_initiated.json) |

This module also subscribes to **its own** `payment.completed`, `payment.failed`
and `subscription.changed` — see `stapel_billing/notifications.py`. That is not
a loop: the emit records that money moved, and the subscriber turns it into the
letter the payer is owed. It lives here rather than in each host because the
gap is identical in every host that installs this library, and the template it
asks for lives in stapel-notifications (≥ 0.20.0) because copy, channels and
languages are that module's job and not this one's.

## License

MIT — see [LICENSE](https://github.com/usestapel/stapel-billing/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>
