Repository: dotkom/onlineweb4

Issue:
Receipts are sent multiple times for individual transactions
**Describe the bug**

Receipts are mailed multiple times, even though only one payment occurs. The receipts have differing IDs.

**To Reproduce**

- Purchase something in the webshop, and observe that two receipt mails are received.
- Add money to your wallet for the same behavior.

Probably also occurs when paying for events.

**Additional context**

My guess, I haven't checked that this is precisely the case: Receipts are sent when PaymentRelation objects are saved. Following the recent changes to payments using Stripe, payments get saved multiple times. Perhaps getting asked for 3D-Secure authentication results in a third mail?


Discussion:
I would suggest this stems from the receipts being created every time a transaction/relation is saved.
Receipts should rather be created when the transaction or relation changes updates status from `SUCCEEDED` and `REFUNDED`, as those are the only appropriate times to send them.
I don't think we send receipts for refunds? It's probably something we want, but it might belong in another issue entirely.

Resolving patch:
diff --git a/apps/payment/models.py b/apps/payment/models.py
--- a/apps/payment/models.py
+++ b/apps/payment/models.py
@@ -348,7 +348,7 @@ def get_items(self):
         return items
 
     def get_from_mail(self):
-        return settings.EMAIL_ARRKOM
+        return self.payment.responsible_mail()
 
     def get_to_mail(self):
         return self.user.email
@@ -371,9 +371,6 @@ def save(self, *args, **kwargs):
         if not self.unique_id:
             self.unique_id = str(uuid.uuid4())
         super(PaymentRelation, self).save(*args, **kwargs)
-        receipt = PaymentReceipt(object_id=self.id,
-                                 content_type=ContentType.objects.get_for_model(self))
-        receipt.save()
 
     def __str__(self):
         return self.payment.description() + " - " + str(self.user)
@@ -456,12 +453,6 @@ def get_to_mail(self):
     def __str__(self):
         return str(self.user) + " - " + str(self.amount) + "(" + str(self.datetime) + ")"
 
-    def save(self, *args, **kwargs):
-        super().save(*args, **kwargs)
-        receipt = PaymentReceipt(object_id=self.id,
-                                 content_type=ContentType.objects.get_for_model(self))
-        receipt.save()
-
     class Meta:
         ordering = ['-datetime']
         verbose_name = _('transaksjon')
diff --git a/apps/payment/signals.py b/apps/payment/signals.py
--- a/apps/payment/signals.py
+++ b/apps/payment/signals.py
@@ -1,8 +1,10 @@
-from django.db.models.signals import pre_save
+from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
+from django.db.models.signals import post_save, pre_save
 from django.dispatch import receiver
 
 from . import status
-from .models import PaymentRelation, PaymentTransaction
+from .models import PaymentReceipt, PaymentRelation, PaymentTransaction
 
 
 @receiver(signal=pre_save, sender=PaymentRelation)
@@ -32,3 +34,13 @@ def handle_payment_transaction_status_change(sender, instance: PaymentTransactio
 
         # Pass transaction to the next strip, which is REMOVED
         instance.status = status.REMOVED
+
+
+@receiver(signal=post_save, sender=PaymentRelation)
+@receiver(signal=post_save, sender=PaymentTransaction)
+def send_receipt_after_payment(sender, instance, **kwargs):
+    content_type = ContentType.objects.get_for_model(instance)
+    receipt_exists = PaymentReceipt.objects.filter(Q(object_id=instance.id) & Q(content_type=content_type)).exists()
+    if instance.status == status.DONE and not receipt_exists:
+        receipt = PaymentReceipt(object_id=instance.id, content_type=content_type)
+        receipt.save()


Test patch: