92 lines
5.6 KiB
Python
92 lines
5.6 KiB
Python
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from apps.notifications.services import enqueue
|
|
from .models import PaymentNotification, PaymentNotificationAttempt
|
|
|
|
EVENTS = {'PAYMENT_NOTICE_CREATED', 'PAYMENT_RECEIPT_APPROVED', 'PAYMENT_RECEIPT_REJECTED'}
|
|
TAGS = ['recipient_name', 'company_name', 'notice_number', 'receipt_number', 'amount_rial', 'invoice_number', 'payment_link', 'rejection_reason']
|
|
|
|
|
|
def notice_sms_pending(notice):
|
|
return PaymentNotification.objects.filter(notice=notice, job__status__in=['PENDING', 'PROCESSING', 'RETRYING']).exists()
|
|
|
|
|
|
def context(customer, notice=None, receipt=None, amount=0):
|
|
profile = getattr(customer, 'customer_profile', None)
|
|
return {'recipient_name': str(profile) if profile else customer.username, 'company_name': settings.COMPANY_NAME,
|
|
'notice_number': notice.number if notice else '', 'receipt_number': receipt.number if receipt else '',
|
|
'amount_rial': f'{amount:,}', 'invoice_number': notice.invoice_number if notice else '', 'rejection_reason': '',
|
|
'payment_link': settings.PUBLIC_BASE_URL + (reverse('payments:receipt_detail', args=[receipt.public_id]) if receipt else reverse('payments:notices') + f'?notice={notice.public_id}')}
|
|
|
|
|
|
def queue_notice(notice):
|
|
job = enqueue('SMS', 'PAYMENT_NOTICE_CREATED', None, f'payment-notice:{notice.public_id}', customer=notice.customer, staff_user=notice.created_by)
|
|
PaymentNotification.objects.get_or_create(job=job, defaults={'notice': notice, 'unit': notice.owning_unit, 'context': context(notice.customer, notice=notice, amount=notice.amount)})
|
|
|
|
|
|
def queue_approval(receipt, cycle):
|
|
job = enqueue('SMS', 'PAYMENT_RECEIPT_APPROVED', None, f'payment-approved:{cycle.pk}', customer=receipt.customer, staff_user=cycle.finalized_by)
|
|
PaymentNotification.objects.get_or_create(job=job, defaults={'cycle': cycle, 'unit': receipt.owning_unit, 'context': context(receipt.customer, notice=receipt.notice, receipt=receipt, amount=cycle.revision.amount)})
|
|
|
|
|
|
def queue_rejection(receipt, cycle, reason):
|
|
job = enqueue('SMS', 'PAYMENT_RECEIPT_REJECTED', None, f'payment-rejected:{cycle.pk}', customer=receipt.customer, staff_user=cycle.finalized_by)
|
|
details = context(receipt.customer, notice=receipt.notice, receipt=receipt, amount=cycle.revision.amount)
|
|
PaymentNotification.objects.get_or_create(job=job, defaults={'cycle': cycle, 'unit': receipt.owning_unit,
|
|
'context': {**details, 'rejection_reason': reason}})
|
|
|
|
|
|
def payment_sms_body(job, notification):
|
|
"""Editable templates must always include the customer's rejection reason."""
|
|
from apps.notifications.management.commands.process_notifications import _render_sms
|
|
body = _render_sms(job.event_type, notification.context, notification.unit)
|
|
reason = notification.context.get('rejection_reason', '')
|
|
if reason and reason not in body:
|
|
body += '\nدلیل کارشناس: ' + reason
|
|
return body
|
|
|
|
|
|
def send_payment_sms(job, provider=None, provider_factory=None):
|
|
from apps.notifications.models import SMSTemplate
|
|
notification = PaymentNotification.objects.get(job=job)
|
|
# Persist the attempt before rendering/provider construction: failed preparation is history too.
|
|
attempt = PaymentNotificationAttempt.objects.create(notification=notification)
|
|
phase = 'آمادهسازی قالب پیامک'
|
|
try:
|
|
with transaction.atomic():
|
|
notification = PaymentNotification.objects.select_for_update().get(pk=notification.pk)
|
|
if not notification.access_link_id:
|
|
from .access import issue_payment_link
|
|
receipt = notification.cycle.revision.receipt if notification.cycle_id else None
|
|
link, url = issue_payment_link(notice=notification.notice, receipt=receipt)
|
|
notification.access_link = link
|
|
old_url = notification.context.get('payment_link', '')
|
|
if old_url and notification.rendered_body:
|
|
notification.rendered_body = notification.rendered_body.replace(old_url, url)
|
|
notification.context = {**notification.context, 'payment_link': url}
|
|
notification.save(update_fields=['context', 'access_link', 'rendered_body'])
|
|
if not notification.rendered_body:
|
|
body = payment_sms_body(job, notification)
|
|
template = SMSTemplate.objects.filter(event_type=job.event_type, is_active=True, unit=notification.unit).first() if notification.unit_id else None
|
|
template = template or SMSTemplate.objects.get(event_type=job.event_type, is_active=True, unit__isnull=True)
|
|
notification.rendered_body = body
|
|
notification.template_snapshot = {'id': template.pk, 'updated_at': template.updated_at.isoformat()}
|
|
notification.save(update_fields=['rendered_body', 'template_snapshot', 'context', 'access_link'])
|
|
phase = 'ارتباط با ارائهدهنده پیامک'
|
|
provider = provider if provider is not None else provider_factory()
|
|
result = provider.send_message(job.customer.mobile, notification.rendered_body)
|
|
attempt.success = result.success
|
|
attempt.provider_id = str(result.provider_id or '')[:500]
|
|
attempt.provider_code = str(result.provider_code or '')[:50]
|
|
attempt.error = str(result.error or '')[:1000]
|
|
return result, None
|
|
except Exception:
|
|
attempt.success = False
|
|
attempt.error = 'خطا در ' + phase
|
|
raise
|
|
finally:
|
|
attempt.finished_at = timezone.now()
|
|
attempt.save()
|