Send welcome email on member signup

Python · Django · advanced · modification

Adds the membership signup endpoint: validate signup data, create the user and membership atomically, place an authorization hold on the card, and return the membership details. A welcome email is sent via a `post_save` signal on `User`. The transaction rolls back cleanly when the card is declined (the hold expires automatically), and the welcome email lands in the happy path.

The membership signup endpoint is the front door of the paid product. `charge_card` places an authorization hold on the card; a declined card must leave no trace — no ghost accounts, no phantom memberships, no stray welcome emails. `membership/models.py` (`Membership` with `user` and `plan`, migrated), `membership/serializers.py` (`SignupSerializer`, which validates the fields of requirement 1, rejects an email already in use and caps `email` at 150 characters) and `membership/services.py` (`charge_card`, `PaymentDeclinedError`) exist from the previous PR and are unchanged. The app is in `INSTALLED_APPS`, this endpoint is the only place a `User` is created, and the configured mail backend queues messages and never raises at send time.

Requirements

Files touched

--- membership/views.py
+"""Membership signup API."""
+
+from decimal import Decimal
+
+from django.contrib.auth.models import User
+from django.db import transaction
+from rest_framework import status
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+
+from .serializers import SignupSerializer
+from .models import Membership
+from .services import charge_card, PaymentDeclinedError
+
+PLAN_PRICES = {'basic': Decimal('9.00'), 'pro': Decimal('29.00')}
+
+
+@api_view(['POST'])
+def signup(request):
+    """Create a new member, charge the card, and activate the membership.
+
+    All three steps are wrapped in a single database transaction so a
+    declined card leaves no trace of the signup.
+    """
+    serializer = SignupSerializer(data=request.data)
+    serializer.is_valid(raise_exception=True)
+    data = serializer.validated_data
+
+    try:

Review this PR

Python practice