Files
customerTicket/apps/payments/selectors.py
T

108 lines
5.8 KiB
Python

"""Bounded-query projections use only the current evidence revision for financial totals."""
from django.urls import reverse
from django.db.models import BigIntegerField, F, OuterRef, Subquery, Sum, Value
from django.db.models.functions import Coalesce
from .models import PaymentReceiptRevision
def notice_sms_history(notice):
"""Separate gateway acceptance from final handset delivery; preserve every send attempt."""
from django.db.models import Prefetch
from .models import PaymentNotificationAttempt
notifications = notice.paymentnotification_set.select_related('job__staff_user__staff_profile').prefetch_related(
Prefetch('attempt_log', queryset=PaymentNotificationAttempt.objects.order_by('-started_at', '-pk'), to_attr='send_attempts')
).order_by('-job__created_at', '-pk')
for notification in notifications:
job = notification.job
notification.manual_resend = ':resend:' in job.dedupe_key
notification.delivery_class = 'info'
if job.status != 'SENT':
notification.delivery_label = 'هنوز ارسال موفق ثبت نشده است.'
elif not job.provider_message_id:
notification.delivery_label = 'شناسه رهگیری ارائه نشده؛ دریافت توسط گوشی قابل تأیید نیست.'
elif not job.delivery_status:
notification.delivery_label = 'دریافت توسط گوشی قابل تأیید نیست.'
elif job.delivery_is_final:
notification.delivery_label = job.get_delivery_status_display()
notification.delivery_class = 'approved' if job.delivery_status == 'DELIVERED' else 'rejected'
elif job.delivery_status == 'PENDING':
notification.delivery_label = 'در انتظار استعلام دریافت توسط گوشی'
else:
suffix = 'در حال پیگیری' if job.delivery_next_check_at else 'دریافت توسط گوشی تأیید نشده است'
notification.delivery_label = f'نتیجه غیرقطعی: {job.get_delivery_status_display()}؛ {suffix}'
return notifications
def notices_with_totals(queryset):
approved = PaymentReceiptRevision.objects.filter(
receipt__notice_id=OuterRef('pk'), receipt__status='APPROVED',
version=F('receipt__current_version'), destination_mismatch=False
).values('receipt__notice_id').annotate(total=Sum('amount')).values('total')
return queryset.annotate(approved_total=Coalesce(Subquery(approved), Value(0), output_field=BigIntegerField()))
def receipts_with_current_values(queryset):
current = PaymentReceiptRevision.objects.filter(receipt_id=OuterRef('pk'), version=OuterRef('current_version'))
return queryset.annotate(current_amount=Subquery(current.values('amount')[:1]),
current_depositor_name=Subquery(current.values('depositor_name')[:1]),
current_paid_date=Subquery(current.values('paid_date')[:1]))
def notice_form_data(notice, staff=False):
"""Customer-owned notice summary; account numbers stay on the notice page."""
return {'id': str(notice.pk), 'subject': notice.subject, 'amount': str(notice.amount),
'account': notice.account_id, 'deposit': notice.deposit_id,
'invoice': notice.invoice_number,
'url': reverse('staff_payments:notice_detail' if staff else 'payments:notice_detail', args=[notice.public_id])}
def receipt_review_summaries(receipts):
"""Attach current-version decisions in one query, preserving snapshot actor names."""
from .models import ReceiptDecision
receipts = list(receipts)
indexed = {receipt.pk: receipt for receipt in receipts}
for receipt in receipts:
receipt.review_decisions = []
if indexed:
decisions = ReceiptDecision.objects.filter(
cycle__revision__receipt_id__in=indexed,
cycle__revision__version=F('cycle__revision__receipt__current_version')
).select_related('cycle__revision').order_by('created_at', 'pk')
for decision in decisions:
indexed[decision.cycle.revision.receipt_id].review_decisions.append(decision)
return receipts
def attach_notice_receipts(notices, visible_receipts):
"""Only fetch linked receipts visible to the current viewer, for this page."""
notices = list(notices)
indexed = {notice.pk: notice for notice in notices}
for notice in notices:
notice.related_receipts = []
if indexed:
receipts = receipt_review_summaries(receipts_with_current_values(
visible_receipts.filter(notice_id__in=indexed)))
for receipt in receipts:
indexed[receipt.notice_id].related_receipts.append(receipt)
return notices
def recent_pending_notice_choices(form, retain_original=None):
"""Limit the visible menu, keeping server-side ownership validation unsliced.
An explicitly selected pending notice replaces the last option when needed.
A revision retains its original notice even if it has since been settled.
This does not change which notices the financial service may validate.
"""
field = form.fields['notice_id']
pending = notices_with_totals(field.queryset).filter(amount__gt=F('approved_total'))
available = list(pending.order_by('-created_at', '-pk')[:10])
selected = form['notice_id'].value()
if selected and not any(str(n.pk) == str(selected) for n in available):
source = field.queryset if str(selected) == str(retain_original) else pending
selected_notice = source.filter(pk=selected).first() if str(selected).isdecimal() and len(str(selected)) <= 19 and int(selected) <= 9223372036854775807 else None
if selected_notice:
available = [selected_notice] + available[:9]
field.widget.choices = [('', field.empty_label)] + [(n.pk, field.label_from_instance(n)) for n in available]
return available