Template
Populate the client website template with catalog feature flags.
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>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user