"""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__})')