Files
customerTicket/apps/payments/access.py
T
2026-10-07 13:39:29 +03:30

158 lines
6.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'
SHARE_SESSION_KEY = 'payment_page_share'
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 notice_share_url(request, notice):
"""Reuse a validated capability; only a logged-in owner can issue a new one."""
shared = request.session.get(SHARE_SESSION_KEY, {})
token = shared.get('token', '')
link = active_links().filter(pk=shared.get('link_id'), notice=notice,
token_hash=token_digest(token)).first() if token else None
grant = getattr(request, 'payment_access_link', None)
if grant and (link is None or link.pk != grant.pk):
raise PermissionDenied('برای کپی لینک، دوباره لینک پرداخت دریافتی را باز کنید.')
if link is None:
from .services import audit
link, url = issue_payment_link(notice=notice)
token = url.rstrip('/').rsplit('/', 1)[-1]
request.session[SHARE_SESSION_KEY] = {'link_id': link.pk, 'token': token}
audit(request.user, 'PAYMENT_LINK_CREATED', link)
return settings.PUBLIC_BASE_URL.rstrip('/') + reverse('payment_short_link', args=[token])
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