103 lines
5.8 KiB
Python
103 lines
5.8 KiB
Python
"""Receipt-only outbox handlers. No network operation runs during receipt registration."""
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.urls import reverse
|
|
|
|
from apps.core.templatetags.jalali import jalali_date, jalali_datetime
|
|
from apps.integrations.sms.base import SMSResult
|
|
from apps.notifications.services import enqueue
|
|
from .alert_templates import validate_alert_template
|
|
from .models import PaymentReceiptAlert, PaymentReceiptAlertConfiguration
|
|
|
|
logger = logging.getLogger(__name__)
|
|
RECEIPT_ALERT_EVENT = 'PAYMENT_RECEIPT_RECEIVED'
|
|
|
|
|
|
def receipt_alert_context(revision):
|
|
customer = revision.receipt.customer
|
|
profile = getattr(customer, 'customer_profile', None)
|
|
name = f'{profile.first_name} {profile.last_name}'.strip() if profile else ''
|
|
return {'customer_name': name or customer.get_full_name() or 'نام ثبت نشده',
|
|
'customer_mobile': customer.local_mobile, 'amount_rial': f'{revision.amount:,}',
|
|
'received_at': jalali_datetime(revision.created_at), 'paid_date': jalali_date(revision.paid_date),
|
|
'receipt_number': revision.receipt.number, 'revision_version': str(revision.version), 'manager_name': '',
|
|
'receipt_link': settings.PUBLIC_BASE_URL.rstrip('/') + reverse('staff_payments:receipt_detail', args=[revision.receipt.public_id])}
|
|
|
|
|
|
def queue_receipt_alerts(revision, actor):
|
|
if actor.account_type != 'CUSTOMER' or actor.pk != revision.receipt.customer_id:
|
|
return
|
|
configuration = PaymentReceiptAlertConfiguration.objects.filter(pk=1).first()
|
|
if not configuration or not (configuration.bale_enabled or configuration.sms_enabled) or (revision.version > 1 and not configuration.notify_revisions):
|
|
return
|
|
context = receipt_alert_context(revision)
|
|
destinations = []
|
|
if configuration.bale_enabled:
|
|
destinations.append(('BALE', None, configuration.bale_caption))
|
|
if configuration.sms_enabled:
|
|
destinations.extend(('SMS', recipient, configuration.sms_template) for recipient in configuration.recipients.filter(is_active=True))
|
|
for channel, recipient, template in destinations:
|
|
try:
|
|
with transaction.atomic():
|
|
job = enqueue(channel, RECEIPT_ALERT_EVENT, None,
|
|
f'receipt-alert:{revision.pk}:{channel}:{recipient.pk if recipient else "group"}', customer=revision.receipt.customer)
|
|
PaymentReceiptAlert.objects.get_or_create(job=job, defaults={'revision': revision, 'recipient': recipient,
|
|
'recipient_name': recipient.name if recipient else '', 'recipient_mobile': recipient.mobile if recipient else '',
|
|
'bale_chat_id': configuration.bale_chat_id if channel == 'BALE' else '', 'message_template': template,
|
|
'context': {**context, 'manager_name': recipient.name if recipient else ''}})
|
|
except Exception as error:
|
|
logger.error('Receipt alert destination queue failed revision=%s channel=%s type=%s', revision.pk, channel, type(error).__name__)
|
|
|
|
|
|
def safe_queue_receipt_alerts(revision, actor):
|
|
# A failed configuration lookup/queue write rolls back only this savepoint.
|
|
try:
|
|
with transaction.atomic():
|
|
queue_receipt_alerts(revision, actor)
|
|
except Exception as error:
|
|
logger.error('Receipt alert queue failed revision=%s type=%s', revision.pk, type(error).__name__)
|
|
|
|
|
|
def prepare_receipt_alert(job):
|
|
configuration = PaymentReceiptAlertConfiguration.objects.filter(pk=1).first()
|
|
alert = PaymentReceiptAlert.objects.select_related('recipient').get(job=job)
|
|
if job.channel == 'BALE':
|
|
enabled = configuration and configuration.bale_enabled and configuration.bale_chat_id == alert.bale_chat_id
|
|
else:
|
|
enabled = configuration and configuration.sms_enabled and alert.recipient and alert.recipient.is_active and alert.recipient.mobile == alert.recipient_mobile
|
|
if enabled:
|
|
return True
|
|
job.status = 'CANCELLED'
|
|
job.last_error = 'اطلاعرسانی این کانال یا گیرنده غیرفعال شده یا مقصد تغییر کرده است.'
|
|
job.save(update_fields=['status', 'last_error'])
|
|
return False
|
|
|
|
|
|
def send_receipt_alert(job, sms_provider_factory=None):
|
|
"""A destination failure is returned to the worker's existing bounded retry policy."""
|
|
try:
|
|
alert = PaymentReceiptAlert.objects.select_related('revision').get(job=job)
|
|
if not alert.rendered_body:
|
|
validate_alert_template(alert.message_template)
|
|
alert.rendered_body = alert.message_template.format(**alert.context)
|
|
alert.save(update_fields=['rendered_body'])
|
|
if job.channel == 'SMS':
|
|
from apps.integrations.sms.providers import get_sms_provider
|
|
return (sms_provider_factory or get_sms_provider)().send_message(alert.recipient_mobile, alert.rendered_body)
|
|
from apps.integrations.bale.providers import BaleBotProvider
|
|
configuration = PaymentReceiptAlertConfiguration.objects.get(pk=1)
|
|
if not configuration.bale_token:
|
|
return SMSResult(False, error='توکن ربات بله پرداخت تنظیم نشده است.')
|
|
if len(alert.rendered_body) > 4096:
|
|
return SMSResult(False, error='متن همراه تصویر بله بیش از ۴۰۹۶ کاراکتر است.')
|
|
with alert.revision.image.open('rb') as image:
|
|
return BaleBotProvider().send_photo(alert.bale_chat_id, image, alert.rendered_body,
|
|
token=configuration.bale_token, filename=f'receipt-{alert.revision.pk}{Path(alert.revision.image.name).suffix}',
|
|
mime_type=alert.revision.mime_type)
|
|
except Exception as error:
|
|
# HTTP exception strings can contain credentials in the request URL. Never persist them.
|
|
return SMSResult(False, error=f'خطا در ارسال اعلان فیش ({type(error).__name__})')
|