Metadata-Version: 2.5
Name: django-mvp-payments
Version: 0.1.0
Summary: Payment and subscription UI as Cotton components for django-mvp projects, one namespace per payment backend
Project-URL: Homepage, https://github.com/django-mvp/django-mvp-payments
Project-URL: Repository, https://github.com/django-mvp/django-mvp-payments
Project-URL: Issues, https://github.com/django-mvp/django-mvp-payments/issues
Author-email: Samuel Jennings <samuel.scott.jennings@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: components,cotton,django,payments,stripe,subscriptions
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Natural Language :: English
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Office/Business :: Financial
Requires-Python: <4.0,>=3.12
Requires-Dist: django-mvp>=0.30.0
Requires-Dist: django>=5.2
Provides-Extra: stripe-lite
Requires-Dist: stripe<17,>=16; extra == 'stripe-lite'
Description-Content-Type: text/markdown

# django-mvp-payments

[![Tests](https://github.com/django-mvp/django-mvp-payments/actions/workflows/tests.yml/badge.svg)](https://github.com/django-mvp/django-mvp-payments/actions/workflows/tests.yml)
[![Build](https://github.com/django-mvp/django-mvp-payments/actions/workflows/build.yml/badge.svg)](https://github.com/django-mvp/django-mvp-payments/actions/workflows/build.yml)
[![Publish](https://github.com/django-mvp/django-mvp-payments/actions/workflows/publish.yml/badge.svg)](https://github.com/django-mvp/django-mvp-payments/actions/workflows/publish.yml)
[![Coverage](https://codecov.io/gh/django-mvp/django-mvp-payments/branch/main/graph/badge.svg)](https://codecov.io/gh/django-mvp/django-mvp-payments)
[![PyPI](https://img.shields.io/pypi/v/django-mvp-payments)](https://pypi.org/project/django-mvp-payments/)
[![Development status](https://img.shields.io/pypi/status/django-mvp-payments)](https://pypi.org/project/django-mvp-payments/)
[![Python](https://img.shields.io/pypi/pyversions/django-mvp-payments)](https://pypi.org/project/django-mvp-payments/)
[![Django](https://img.shields.io/pypi/djversions/django-mvp-payments)](https://pypi.org/project/django-mvp-payments/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Licence](https://img.shields.io/github/license/django-mvp/django-mvp-payments)](https://github.com/django-mvp/django-mvp-payments/blob/main/LICENSE)

Payment and subscription UI as Cotton components for django-mvp projects, one namespace per payment backend.

It covers pricing, checkout and subscriptions for
[django-mvp](https://github.com/django-mvp/django-mvp) projects.

Django has no shortage of packages that talk to a payment provider. What none of them give you
is the part your users look at. A package hands you a subscription record, a list of prices and
an endpoint that starts a checkout, and leaves the plan grid, the subscribe button, the billing
panel and the "your card expires next month" notice to be built from raw utility classes — in
every project, every time.

This package is that layer, plus the least it takes to keep it fed. It runs no payment logic and
depends on no third-party payment backend. For Stripe it has an app of its own,
`mvp_payments.stripe`, which keeps a small copy of each user's subscription in step with Stripe
through webhooks. Its pages turn up in django-mvp's Account Center, rather than leaving you to
build and route them.

## Status

Version 0.0.1. The `stripe` namespace contributes one entry to the Account Center, Subscription,
under a Billing group. That page shows a signed-in person every subscription they currently hold,
the way to switch plans, and the way through to Stripe's billing portal. The plans page it leads
to is a public page of its own. It draws every recurring price in your Stripe account, with each
product's name, description and features, and says so plainly when Stripe cannot be reached or has
nothing on offer. Nothing here is stable.

## Requirements

- Python 3.12+
- Django 5.2, 6.0 or 6.1
- django-mvp 0.30.0+
- For Stripe, the `stripe-lite` extra and a Stripe account

## Install

```bash
pip install django-mvp-payments
```

Add it to `INSTALLED_APPS`, before `mvp`:

```python
INSTALLED_APPS = [
    ...,
    "mvp_payments",
    "mvp",
]
```

That order is load-bearing, not a style choice: this package ships its own copy of
`mvp/account/overview.html` and extends the name from inside it, which only resolves when this
application is found first.

Then mount its URLs wherever you like:

```python
urlpatterns = [
    ...,
    path("account/billing/", include("mvp_payments.urls")),
]
```

Mount it where a reader would expect to find it. These are Account Center pages, so inside the
Account Center's own prefix and under the label the navigation gives them is the natural place.
Addresses carry no backend name: a page is at `<your prefix>/subscription/`, never
`<your prefix>/stripe/subscription/`, because which library you chose to talk to your
payment provider is not a person's business while they read their own subscription.

To offer the plans page, add a second include at the address you choose, outside the Account Center:

```python
urlpatterns = [
    ...,
    path("plans/", include("mvp_payments.plans_urls")),
]
```

Those two lines are the only wiring. From there, every backend you have installed contributes its own
section of the Account Center's navigation and its own card to the Account Center's overview, and a
backend you have not installed contributes nothing. There is no settings block, no flag to turn on
and no registry to populate — what is in `INSTALLED_APPS` decides what exists.

### The Stripe app

To keep each user's Stripe subscription in your database, install the extra and add the app:

```bash
pip install "django-mvp-payments[stripe-lite]"
```

```python
INSTALLED_APPS = [
    ...,
    "mvp_payments",
    "mvp_payments.stripe",
    "mvp",
]
```

Then set `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` in `MVP_PAYMENTS` and run
`python manage.py migrate`.
[How to install the Stripe app](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/how-to/install-the-stripe-app.md)
has the steps, [how to point Stripe at the webhook](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/how-to/point-stripe-at-the-webhook.md)
the rest of the setup, and the
[Stripe app reference](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/reference/stripe-app.md)
the settings, the records and the webhook.

## Namespaces

Components are grouped by the backend they speak to, and the namespace is that backend's name:

```html
<c-stripe.plans :plans="plans" />
<c-stripe.subscription :subscription="subscription" />
```

Each namespace assumes one backend's records and nothing else. Two namespaces never
share markup or a data shape, because the things they are describing are not the same thing
wearing different names — a subscription in one library is a different record with different
fields from a subscription in the next.

Shipped today: `stripe`, built on the package's own Stripe app. It contributes a user subscriptions page
to the Account Center. That page lists every subscription a person currently holds, reading the Stripe app's
records, and hands the reader to Stripe's own billing portal and plan-change screen through views the
app mounts. It also carries the way to the plans page, a public page of its own that draws every
price on offer in your Stripe account and that a project mounts with `mvp_payments.plans_urls`. Namespaces for other backends are welcome and do not need this one's
agreement about anything: adding one is adding it beside the ones already installed, and changes
nothing about their navigation entries, their card or their pages.

[docs/reference/user-subscriptions-page.md](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/reference/user-subscriptions-page.md) covers the user subscriptions page: what it puts
in the template context, the components it is built from, and how to replace it with your own.

[docs/reference/plans-page.md](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/reference/plans-page.md) covers the plans page: where to mount it, the parts
it is built from and the data each receives, the setting it reads, the `{% stripe_plans %}` tag,
and how to place the plans on your own pages or replace one part.

[docs/namespaces.md](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/namespaces.md) is how you add a namespace: what one declares, the two
questions it answers about itself, and what it may not do.

## JavaScript

No component here needs a payment provider's script. The plans are drawn on the server, and
checkout, the billing portal and plan changes all happen on Stripe's own pages. This package never
emits a script tag for a provider's library, and should a future component need one, loading it is
your project's decision.

The one script of its own, `mvp_payments/stripe/billing_portal.js`, binds the "Manage
subscription" and "Switch plans" controls. Include it with
`{% static %}` on any page that shows those controls, as
[the user subscriptions page reference](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/reference/user-subscriptions-page.md) shows.

## Scope & philosophy

**What this is.** A presentation layer for payment and subscription state, and, for each
supported provider, a small app that keeps that state. Templates, the small amount of JavaScript
some components need, the Python that puts a page at a URL and an entry in a menu, and for
Stripe, `mvp_payments.stripe`.

The pages look like the rest of your django-mvp site, because they are drawn with the same daisyUI
classes and follow the same theme. They show a person where they stand. Anything that changes
what they pay, such as choosing a plan, switching, cancelling or updating a card, is handed to the
provider's own hosted pages, which already do it well.

The Stripe app keeps the least it can: who each user's Stripe customer is, and that customer's
subscription with its status, price and period. Stripe is the source of truth. The app learns of
changes from Stripe's webhooks, checks each one's signature and then asks Stripe for the current
state, rather than trusting what the webhook says. It is installed with
`django-mvp-payments[stripe-lite]`
([ADR 0010](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/adr/0010-the-package-ships-its-own-stripe-app.md)).

The standing directions this package works toward are in [GOALS.md](https://github.com/django-mvp/django-mvp-payments/blob/main/GOALS.md).

**What this deliberately is not.**

- **Not a full Stripe integration.** No copy of Stripe's products, prices, invoices or payment
  methods, no taxes, no metered billing, no one-off payments. If you need those,
  [dj-stripe](https://github.com/dj-stripe/dj-stripe) is built for it.
- **Not a decider.** Nothing here computes a price, a proration or a trial, and nothing decides
  what a subscription grants. The Stripe app can tell your views whether someone is subscribed.
  What that lets them see is your project's call, enforced in your views.
- **Not a place card details pass through.** Cards are typed into the provider's own pages and
  iframes, never into markup written here.
- **Not a way to run two backends at once.** Whose subscription a person is looking at stops
  being answerable, and nothing here is built to arbitrate it.
- **Not a backend abstraction.** There is no common interface that every payment provider gets
  adapted onto. That abstraction is the classic trap in this domain: it fits the first provider,
  bends for the second and is a liability by the third, because subscription models genuinely
  differ. A namespace per backend costs some duplication and buys the freedom to match each one
  exactly.
- **Not a CSS framework.** Components render the daisyUI classes django-mvp already ships. No
  stylesheet, no build step, no theme of its own.

**Tie-breaks.** When two of these pull against each other:

- match the provider rather than generalise
- keep less rather than more, and ask the provider rather than guess
- route and render rather than compute
- render less rather than assume how a page is laid out

## Running the demo

```bash
uv sync
uv run python manage.py migrate
uv run python manage.py seed_demo
uv run python manage.py runserver 8020
```

Sign in as `regular.user`, `staff.user` or `super.user`, password `password`. Out of the box the
pages run on fake Stripe values, so the billing portal says it could not be reached, while
`seed_demo` puts a demo set of plans (Basic, Pro and Team, monthly and yearly) in the demo's file
cache for the plans page. To see the portal and your own plans working, copy `demo/.env.example` to `demo/.env` and fill it in from your own
Stripe sandbox. That file is gitignored. Then run `seed_demo` again so the demo accounts get real
sandbox customers and subscriptions, recorded by the Stripe app as well, where `super.user` can
look at them in the admin at `/admin/`. The file lists the one portal setting "Switch plans" needs
from your sandbox.

To have the demo record subscriptions made in the sandbox, forward Stripe's webhooks to it with the
[Stripe CLI](https://docs.stripe.com/stripe-cli) in a second terminal:

```bash
stripe listen --forward-to localhost:8020/account/billing/stripe/webhook/ \
  --events checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted,customer.subscription.paused,customer.subscription.resumed,customer.deleted,product.created,product.updated,product.deleted,price.created,price.updated,price.deleted
```

It prints a `whsec_...` signing secret. Put it in `demo/.env` as `STRIPE_TEST_WEBHOOK_SECRET` and
restart the demo. Subscribing, switching plans and cancelling in the sandbox then show up in the
demo's records. The demo starts without the command
running, and `python manage.py stripe_sync` brings the records up to date after a gap.

## Contributing

Changes follow [CONSTITUTION.md](https://github.com/django-mvp/django-mvp-payments/blob/main/CONSTITUTION.md), the
[testing standards](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/contributing/standards/testing.md), the
[code documentation standards](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/contributing/standards/code-documentation.md) and the
[Django standards](https://github.com/django-mvp/django-mvp-payments/blob/main/docs/contributing/standards/django.md).

## Changelog

See [CHANGELOG.md](https://github.com/django-mvp/django-mvp-payments/blob/main/CHANGELOG.md).

## Licence

MIT.
