Template
Extract always-on public/portal/UTM plus optional email_sms, directmail, blog, payments, social, and social_ai so new client sites can be bootstrapped from this seed. Refs #1 Refs #2 Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
3.6 KiB
Python
117 lines
3.6 KiB
Python
"""Stripe invoice + Checkout pay-link helpers. Requires FEATURE_EMAIL_SMS to send."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import date
|
|
|
|
from django.conf import settings
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
|
|
from payments.models import Invoice
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class PaymentsError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _stripe():
|
|
secret = (settings.STRIPE_SECRET_KEY or "").strip()
|
|
if not secret:
|
|
raise PaymentsError("STRIPE_SECRET_KEY is not configured")
|
|
try:
|
|
import stripe
|
|
except ImportError as exc:
|
|
raise PaymentsError("stripe package is not installed") from exc
|
|
stripe.api_key = secret
|
|
return stripe
|
|
|
|
|
|
def next_invoice_number() -> str:
|
|
today = date.today().strftime("%Y%m%d")
|
|
prefix = f"INV-{today}-"
|
|
existing = Invoice.objects.filter(number__startswith=prefix).count()
|
|
return f"{prefix}{existing + 1:03d}"
|
|
|
|
|
|
def create_checkout_session(invoice: Invoice, *, success_url: str, cancel_url: str) -> str:
|
|
stripe = _stripe()
|
|
session = stripe.checkout.Session.create(
|
|
mode="payment",
|
|
customer_email=invoice.contact.email or None,
|
|
line_items=[
|
|
{
|
|
"quantity": 1,
|
|
"price_data": {
|
|
"currency": (invoice.currency or "usd").lower(),
|
|
"unit_amount": invoice.amount_cents,
|
|
"product_data": {
|
|
"name": invoice.description or f"Invoice {invoice.number}",
|
|
},
|
|
},
|
|
}
|
|
],
|
|
metadata={"invoice_id": str(invoice.pk), "invoice_number": invoice.number},
|
|
success_url=success_url,
|
|
cancel_url=cancel_url,
|
|
)
|
|
invoice.stripe_checkout_session_id = session.id
|
|
invoice.hosted_invoice_url = session.url or ""
|
|
invoice.status = Invoice.Status.OPEN
|
|
invoice.save(
|
|
update_fields=[
|
|
"stripe_checkout_session_id",
|
|
"hosted_invoice_url",
|
|
"status",
|
|
"updated_at",
|
|
]
|
|
)
|
|
return session.url or ""
|
|
|
|
|
|
def mark_paid(invoice: Invoice, *, stripe_id: str = "") -> None:
|
|
invoice.status = Invoice.Status.PAID
|
|
invoice.paid_at = timezone.now()
|
|
if stripe_id and not invoice.stripe_invoice_id:
|
|
invoice.stripe_invoice_id = stripe_id
|
|
invoice.save(
|
|
update_fields=["status", "paid_at", "stripe_invoice_id", "updated_at"]
|
|
)
|
|
|
|
|
|
def send_invoice_email(invoice: Invoice, pay_url: str) -> bool:
|
|
"""Send pay-link email through Django mail (SMTP2GO)."""
|
|
to_email = (invoice.contact.email or "").strip()
|
|
if not to_email:
|
|
raise PaymentsError("Contact has no email address")
|
|
from django.core.mail import EmailMultiAlternatives
|
|
|
|
name = invoice.contact.full_name or "there"
|
|
amount = f"{invoice.amount} {invoice.currency.upper()}"
|
|
subject = f"Invoice {invoice.number} from {settings.SITE_NAME}"
|
|
text = (
|
|
f"Hi {name},\n\n"
|
|
f"Invoice {invoice.number} for {amount} is ready.\n"
|
|
f"{invoice.description}\n\n"
|
|
f"Pay securely: {pay_url}\n"
|
|
)
|
|
html = (
|
|
f"<p>Hi {name},</p>"
|
|
f"<p>Invoice <strong>{invoice.number}</strong> for "
|
|
f"<strong>{amount}</strong> is ready.</p>"
|
|
f"<p>{invoice.description}</p>"
|
|
f'<p><a href="{pay_url}">Pay this invoice</a></p>'
|
|
)
|
|
mail = EmailMultiAlternatives(
|
|
subject=subject,
|
|
body=text,
|
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
to=[to_email],
|
|
)
|
|
mail.attach_alternative(html, "text/html")
|
|
mail.send(fail_silently=False)
|
|
return True
|