Add finance app with Stripe Checkout subscriptions (#21) (#23)
Unit Tests / test (push) Successful in 10s

## Summary
- Closes #21 — new Django `finance` app with Stripe as payment provider
- Subscription price defaults to **$10 USD / month** via `SUBSCRIPTION_PRICE_AMOUNT_CENTS = 1000` in `settings.py` (env-overridable)
- Persists **Invoice** and **Payment** rows; both registered in Django admin (with payment inline on invoices)
- Checkout Session API redirects users to Stripe hosted payment; webhook verifies signatures and upserts ledger idempotently

## API
- `POST /api/finance/checkout/` — JWT auth → `{ checkout_url, session_id }`
- `GET /api/finance/invoices/` / `GET /api/finance/payments/` — own records
- `POST /api/finance/webhooks/stripe/` — Stripe signature-verified webhook

## Config
Documented in `.env.example` / `.env.prod.example`:
`STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET`, optional `STRIPE_PRICE_ID`, `FRONTEND_BASE_URL`

## Test plan
- [x] `uv run python manage.py test finance` (17 tests)
- [ ] Set Stripe test keys locally; create checkout session; complete payment in Stripe test mode
- [ ] Confirm Invoice/Payment appear in `/admin/`
- [ ] Point Stripe webhook to `/api/finance/webhooks/stripe/` and verify `checkout.session.completed` / `invoice.paid`Reviewed-on: #23
This commit was merged in pull request #23.
This commit is contained in:
2026-07-26 17:35:06 -07:00
parent 9984d1c340
commit ad44359804
23 changed files with 1540 additions and 0 deletions
Generated
+15
View File
@@ -661,6 +661,7 @@ dependencies = [
{ name = "python-dateutil" },
{ name = "python-docx" },
{ name = "pytz" },
{ name = "stripe" },
{ name = "unstructured" },
{ name = "uvicorn" },
{ name = "whitenoise" },
@@ -707,6 +708,7 @@ requires-dist = [
{ name = "python-dateutil", specifier = "==2.9.0.post0" },
{ name = "python-docx", specifier = "==1.2.0" },
{ name = "pytz", specifier = "==2025.2" },
{ name = "stripe", specifier = ">=12.0.0,<14.0.0" },
{ name = "unstructured", specifier = "==0.18.21" },
{ name = "uvicorn", specifier = "==0.38.0" },
{ name = "whitenoise", specifier = "==6.9.0" },
@@ -3976,6 +3978,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
]
[[package]]
name = "stripe"
version = "13.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4f/c1/e687ba904a78eeb9ac8e0845630c625a03d380b7569647530c4bafd12677/stripe-13.2.0.tar.gz", hash = "sha256:ed6ad1c27725e1e32a336fa9d835cfa8f0bd6dafcc98b3ab7bc7e8791390453b", size = 1357785, upload-time = "2025-11-05T23:04:23.043Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/39/5a7a7ac27372e8bd1ec45f52b884d039f504900a1a41885745c5a7976519/stripe-13.2.0-py3-none-any.whl", hash = "sha256:e7d18bd44bab1812bc4e9e75da4ceacedd587f71737bb9193955edf420605f88", size = 1962001, upload-time = "2025-11-05T23:04:21.056Z" },
]
[[package]]
name = "tenacity"
version = "9.1.4"