Files
customerTicket/apps/payments/access.py
T
2026-10-06 12:30:28 +03:30

139 lines
5.4 KiB
Python

"""A payment link never authenticates a user or grants access to their whole account."""
import hashlib
import uuid
from datetime import timedelta
from django.conf import settings
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from apps.core.session_access import grant_scoped_session_access
from apps.accounts.models import TicketMagicLink
from . import policies
from .models import PaymentAccessLink, PaymentNotice
SESSION_KEY = 'payment_access_link_id'
def token_digest(token):
return hashlib.sha256(token.encode()).hexdigest()
def issue_payment_link(*, notice=None, receipt=None):
target = receipt or notice
if target is None:
raise ValueError('Payment link requires a notice or receipt')
if receipt is not None:
notice = receipt.notice
for attempt in range(5):
token = TicketMagicLink.generate_secure_token(10)
try:
with transaction.atomic():
link = PaymentAccessLink.objects.create(customer=target.customer, notice=notice, receipt=receipt,
token_hash=token_digest(token),
expires_at=timezone.now() + timedelta(days=settings.PAYMENT_LINK_TTL_DAYS))
return link, settings.PUBLIC_BASE_URL.rstrip('/') + reverse('payment_short_link', args=[token])
except IntegrityError:
if attempt == 4:
raise RuntimeError('عدم موفقیت در تولید لینک یکتای پرداخت.')
def active_links():
return PaymentAccessLink.objects.select_related('customer', 'notice', 'receipt').filter(
notice__deleted_at__isnull=True,
revoked_at__isnull=True, expires_at__gt=timezone.now(), customer__is_active=True,
customer__account_type='CUSTOMER')
def grant_payment_access(request, link):
# The session may already contain a ticket grant; each grant checks its own expiry.
grant_scoped_session_access(request, SESSION_KEY, link.pk, link.expires_at, preserve_expiry=True)
def get_payment_access(request):
link_id = request.session.get(SESSION_KEY)
if not isinstance(link_id, int):
return None
link = active_links().filter(pk=link_id).first()
if link is None:
request.session.pop(SESSION_KEY, None)
return link
def visible_payments(request, queryset):
link = getattr(request, 'payment_access_link', None)
if link is None:
return policies.visible(queryset, request.user)
owned = queryset.filter(customer_id=link.customer_id)
if queryset.model == PaymentNotice:
return owned.filter(pk=link.notice_id)
scope = Q(pk=link.receipt_id) | Q(access_links=link)
if link.notice_id:
scope |= Q(notice_id=link.notice_id)
return owned.filter(scope).distinct()
def customer_context(request, **kwargs):
return {'payment_link_access': getattr(request, 'payment_access_link', None), **kwargs}
def prepare_payment_request(request, *, staff=False, allow_payment_link=False):
if request.user.is_authenticated:
customer = request.user.is_active and request.user.account_type == 'CUSTOMER'
permitted = policies.is_staff(request.user) if staff else customer
if staff is None:
permitted = customer or policies.is_staff(request.user)
if not permitted:
raise PermissionDenied
request.payment_customer = request.user
return None
had_link = SESSION_KEY in request.session
link = get_payment_access(request) if allow_payment_link else None
if link:
request.payment_access_link = link
request.payment_customer = link.customer
return None
if had_link and allow_payment_link:
return unavailable_link(request)
return redirect_to_login(request.get_full_path(), '/staff/login/' if staff else '/login/')
def receipt_initial(request, customer):
link = getattr(request, 'payment_access_link', None)
identifier = request.GET.get('notice')
initial = {}
if link and not identifier:
if request.GET.get('mode') == 'independent' or not link.notice_id or link.notice.cancelled_at:
return {'receipt_mode': 'independent'}
identifier = str(link.notice.public_id)
if not identifier:
return initial
try:
identifier = uuid.UUID(identifier)
except ValueError:
raise Http404
notices = visible_payments(request, PaymentNotice.objects.all()) if link else PaymentNotice.objects.filter(customer=customer)
notice = get_object_or_404(notices, public_id=identifier, cancelled_at__isnull=True)
return {'notice_id': notice.pk, 'destination_account': notice.account_id, 'deposit_id': notice.deposit_id}
def validate_link_submission(link, customer, data):
if not active_links().select_related(None).select_for_update(of=('self',)).filter(pk=link.pk, customer=customer).first():
raise PermissionDenied
notice = data.get('notice_id')
if notice is not None and notice.pk != link.notice_id:
raise PermissionDenied
def unavailable_link(request):
response = render(request, 'payments/link_unavailable.html', {'financial_page': True}, status=410)
response['Referrer-Policy'] = 'no-referrer'
response['X-Robots-Tag'] = 'noindex, nofollow'
return response