Add finance app with Stripe Checkout subscriptions (#21) (#23)
Unit Tests / test (push) Successful in 10s
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:
@@ -0,0 +1,71 @@
|
||||
"""Stripe Checkout session helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import stripe
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class StripeNotConfiguredError(RuntimeError):
|
||||
"""Raised when Stripe secret key is missing."""
|
||||
|
||||
|
||||
def configure_stripe() -> str:
|
||||
secret = settings.STRIPE_SECRET_KEY
|
||||
if not secret:
|
||||
raise StripeNotConfiguredError(
|
||||
"STRIPE_SECRET_KEY is not configured. Set it in the environment."
|
||||
)
|
||||
stripe.api_key = secret
|
||||
return secret
|
||||
|
||||
|
||||
def subscription_line_items() -> list[dict[str, Any]]:
|
||||
"""Build Checkout line_items from settings-backed subscription pricing."""
|
||||
price_id = settings.STRIPE_PRICE_ID
|
||||
if price_id:
|
||||
return [{"price": price_id, "quantity": 1}]
|
||||
|
||||
return [
|
||||
{
|
||||
"price_data": {
|
||||
"currency": settings.SUBSCRIPTION_PRICE_CURRENCY,
|
||||
"unit_amount": settings.SUBSCRIPTION_PRICE_AMOUNT_CENTS,
|
||||
"recurring": {"interval": settings.SUBSCRIPTION_PRICE_INTERVAL},
|
||||
"product_data": {
|
||||
"name": settings.SUBSCRIPTION_PRODUCT_NAME,
|
||||
},
|
||||
},
|
||||
"quantity": 1,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def create_checkout_session(
|
||||
*,
|
||||
user,
|
||||
success_url: str | None = None,
|
||||
cancel_url: str | None = None,
|
||||
):
|
||||
"""Create a Stripe Checkout Session for the subscription plan."""
|
||||
configure_stripe()
|
||||
|
||||
metadata = {
|
||||
"user_id": str(user.pk),
|
||||
"company_id": str(user.company_id) if user.company_id else "",
|
||||
}
|
||||
customer_email = getattr(user, "email", None) or None
|
||||
|
||||
session = stripe.checkout.Session.create(
|
||||
mode="subscription",
|
||||
line_items=subscription_line_items(),
|
||||
success_url=success_url or settings.STRIPE_CHECKOUT_SUCCESS_URL,
|
||||
cancel_url=cancel_url or settings.STRIPE_CHECKOUT_CANCEL_URL,
|
||||
customer_email=customer_email,
|
||||
client_reference_id=str(user.pk),
|
||||
metadata=metadata,
|
||||
subscription_data={"metadata": metadata},
|
||||
)
|
||||
return session
|
||||
Reference in New Issue
Block a user