Files

107 lines
6.2 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):
from .identity import receipt_identity
customer = revision.receipt.customer
return {'customer_name': receipt_identity(customer, revision.depositor_name),
'depositor_name': revision.depositor_name,
'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):
# Submission/revision services authorize staff before reaching this queue.
# Keep customer submissions restricted to their own receipt.
if actor.account_type != 'STAFF' and (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)
if alert.context.get('depositor_name') and '{customer_name}' not in alert.message_template:
alert.rendered_body += '\nمشتری / واریزکننده: ' + alert.context['customer_name']
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__})')