From 97d849c1ff5b746bc02a14ca659fbbe65b1aa493 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 8 Jul 2026 13:05:08 +0330 Subject: [PATCH] transactions notifications implemented --- .../test_transaction_notifications.py | 103 ++++++++++++++++++ ...003_alter_transactionparticipant_status.py | 18 +++ apps/transaction/models.py | 1 + apps/transaction/signals.py | 58 +++++++++- 4 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 apps/account/tests/notifications/test_transaction_notifications.py create mode 100644 apps/transaction/migrations/0003_alter_transactionparticipant_status.py diff --git a/apps/account/tests/notifications/test_transaction_notifications.py b/apps/account/tests/notifications/test_transaction_notifications.py new file mode 100644 index 0000000..c9123b2 --- /dev/null +++ b/apps/account/tests/notifications/test_transaction_notifications.py @@ -0,0 +1,103 @@ +import datetime +from django.utils import timezone +from django.test import TestCase +from unittest.mock import patch +from django.core.files.uploadedfile import SimpleUploadedFile + +from dj_language.models import Language +from apps.account.models import StudentUser, Notification +from apps.course.models.course import Course, CourseCategory +from apps.transaction.models import TransactionParticipant + +class TransactionNotificationsFlowTests(TestCase): + def setUp(self): + # Create languages + self.lang_fa, _ = Language.objects.update_or_create( + code='fa', + defaults={'name': 'Persian', 'status': True, 'countries': []} + ) + self.lang_en, _ = Language.objects.update_or_create( + code='en', + defaults={'name': 'English', 'status': True, 'countries': []} + ) + + # Create category + self.category = CourseCategory.objects.create( + name=[{"title": "Category", "language_code": "en"}], + slug=[{"title": "category", "language_code": "en"}] + ) + + # Create student user + self.student = StudentUser.objects.create( + email='student_tx@example.com', + username='student_tx', + fullname='Tx Student', + language=self.lang_fa, + fcm='token_tx_student' + ) + + # Create Course + thumbnail = SimpleUploadedFile('thumb.jpg', b'filecontent', content_type='image/jpeg') + self.course = Course.objects.create( + title=[ + {"title": "Course 1", "language_code": "en"}, + {"title": "دوره ۱", "language_code": "fa"} + ], + slug=[{"title": "course-1", "language_code": "en"}], + category=self.category, + thumbnail=thumbnail, + video_type=Course.VedioTypeChoices.YOUTUBE_LINK, + video_link='https://example.com/video', + is_online=False, + level=Course.LevelChoices.BEGINNER, + duration=10, + lessons_count=0, + description='Description', + short_description='Short description', + status=[{"title": "ongoing", "language_code": "en"}], + is_free=True, + ) + + # Create transaction with PENDING status + self.tx = TransactionParticipant.objects.create( + user=self.student, + course=self.course, + price=150.00, + status=TransactionParticipant.TransactionStatus.PENDING + ) + + @patch('apps.account.notification_service.send_notification') + def test_payment_successful_notification(self, mock_send): + # Update status to SUCCESS + self.tx.status = TransactionParticipant.TransactionStatus.SUCCESS + self.tx.save() + + # Check DB notification (in Persian) + notif = Notification.objects.filter(user=self.student, title="پرداخت موفقیت‌آمیز").first() + self.assertIsNotNone(notif) + self.assertIn("دوره ۱", notif.message) + self.assertTrue(mock_send.called) + + @patch('apps.account.notification_service.send_notification') + def test_payment_failed_notification(self, mock_send): + # Update status to FAILED + self.tx.status = TransactionParticipant.TransactionStatus.FAILED + self.tx.save() + + # Check DB notification (in Persian) + notif = Notification.objects.filter(user=self.student, title="خطا در پرداخت").first() + self.assertIsNotNone(notif) + self.assertIn("دوره ۱", notif.message) + self.assertTrue(mock_send.called) + + @patch('apps.account.notification_service.send_notification') + def test_refund_completed_notification(self, mock_send): + # Update status to REFUNDED + self.tx.status = TransactionParticipant.TransactionStatus.REFUNDED + self.tx.save() + + # Check DB notification (in Persian) + notif = Notification.objects.filter(user=self.student, title="بازگشت وجه انجام شد").first() + self.assertIsNotNone(notif) + self.assertIn("دوره ۱", notif.message) + self.assertTrue(mock_send.called) diff --git a/apps/transaction/migrations/0003_alter_transactionparticipant_status.py b/apps/transaction/migrations/0003_alter_transactionparticipant_status.py new file mode 100644 index 0000000..a5c364d --- /dev/null +++ b/apps/transaction/migrations/0003_alter_transactionparticipant_status.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.12 on 2026-07-08 13:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transaction', '0002_alter_participantinfo_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='transactionparticipant', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('waiting_approval', 'Waiting for Approval'), ('success', 'Success'), ('failed', 'Failed'), ('refunded', 'Refunded')], default='pending', max_length=20, verbose_name='Transaction Status'), + ), + ] diff --git a/apps/transaction/models.py b/apps/transaction/models.py index 46b603e..d37ee61 100644 --- a/apps/transaction/models.py +++ b/apps/transaction/models.py @@ -21,6 +21,7 @@ class TransactionParticipant(models.Model): WAITING_APPROVAL = 'waiting_approval', _('Waiting for Approval') SUCCESS = 'success', _('Success') FAILED = 'failed', _('Failed') + REFUNDED = 'refunded', _('Refunded') class PaymentMethods(models.TextChoices): RECEIPT = 'receipt', _('Receipt') diff --git a/apps/transaction/signals.py b/apps/transaction/signals.py index fa42c8c..13677ec 100644 --- a/apps/transaction/signals.py +++ b/apps/transaction/signals.py @@ -64,4 +64,60 @@ def create_course_participant(transaction_instance): print(f"Course participant already exists: {existing_participant}") except Exception as e: - print(f"Error creating course participant: {e}") \ No newline at end of file + print(f"Error creating course participant: {e}") + + +from apps.account.notification_service import create_and_send_notification +from apps.course.models.course import get_localized_field + +@receiver(post_save, sender=TransactionParticipant) +def notify_transaction_status_changed(sender, instance, created, **kwargs): + """ + Send notifications when transaction status transitions to SUCCESS, FAILED, or REFUNDED. + """ + status_changed = False + + if created: + status_changed = True + elif hasattr(instance, '_previous_status') and instance._previous_status != instance.status: + status_changed = True + + if not status_changed: + return + + course_title_en = get_localized_field('en', instance.course.title) + course_title_fa = get_localized_field('fa', instance.course.title) + + # 1. SUCCESS + if instance.status == TransactionParticipant.TransactionStatus.SUCCESS: + create_and_send_notification( + user=instance.user, + title_en="Payment Successful", + body_en=f"Your payment for course '{course_title_en}' was successful.", + title_fa="پرداخت موفقیت‌آمیز", + body_fa=f"پرداخت شما برای دوره «{course_title_fa}» با موفقیت انجام شد.", + service='imam-javad', + data={'type': 'payment_successful', 'transaction_id': instance.id, 'course_id': instance.course.id} + ) + # 2. FAILED + elif instance.status == TransactionParticipant.TransactionStatus.FAILED: + create_and_send_notification( + user=instance.user, + title_en="Payment Failed", + body_en=f"Your payment attempt for course '{course_title_en}' failed.", + title_fa="خطا در پرداخت", + body_fa=f"عملیات پرداخت شما برای دوره «{course_title_fa}» ناموفق بود.", + service='imam-javad', + data={'type': 'payment_failed', 'transaction_id': instance.id, 'course_id': instance.course.id} + ) + # 3. REFUNDED + elif instance.status == TransactionParticipant.TransactionStatus.REFUNDED: + create_and_send_notification( + user=instance.user, + title_en="Refund Completed", + body_en=f"Your refund for course '{course_title_en}' has been processed.", + title_fa="بازگشت وجه انجام شد", + body_fa=f"مبلغ پرداختی شما برای دوره «{course_title_fa}» بازگردانده (ریفاند) شد.", + service='imam-javad', + data={'type': 'refund_completed', 'transaction_id': instance.id, 'course_id': instance.course.id} + ) \ No newline at end of file