We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
ThaliaPay batch push throttled by Moneybird
Whenever we process a ThaliaPay batch, we are throttled by Moneybird on the amount of API requests we make.
This is most likely due to saving the bank accounts which are pushed to moneybird when the post_save signal is triggered.
This shouldn't be triggered when the last_used field of the bank account is updated, but it is.

Potential fix:
Make sure that updated_fields is passed (correctly) with the post_save signal for bank accounts.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `website/payments/services.py`
Content:
```
1 """The services defined by the payments package."""
2 import datetime
3 from typing import Union
4 
5 from django.conf import settings
6 from django.core import mail
7 from django.db.models import Model, Q, QuerySet, Sum
8 from django.urls import reverse
9 from django.utils import timezone
10 from django.utils.translation import gettext_lazy as _
11 
12 from members.models import Member
13 from utils.snippets import send_email
14 
15 from .exceptions import PaymentError
16 from .models import BankAccount, Payment, PaymentUser
17 from .payables import Payable, payables
18 from .signals import processed_batch
19 
20 
21 def create_payment(
22     model_payable: Union[Model, Payable],
23     processed_by: Member,
24     pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],
25 ) -> Payment:
26     """Create a new payment from a payable object.
27 
28     :param model_payable: Payable or Model object
29     :param processed_by: PaymentUser that processed this payment
30     :param pay_type: Payment type
31     :return: Payment object
32     """
33     if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):
34         raise PaymentError("Invalid payment type")
35 
36     if isinstance(model_payable, Payable):
37         payable = model_payable
38     else:
39         payable = payables.get_payable(model_payable)
40 
41     payer = (
42         PaymentUser.objects.get(pk=payable.payment_payer.pk)
43         if payable.payment_payer
44         else None
45     )
46 
47     if not (
48         (payer and payer == processed_by and pay_type == Payment.TPAY)
49         or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)
50     ):
51         raise PaymentError(
52             _("User processing payment does not have the right permissions")
53         )
54 
55     if payable.payment_amount == 0:
56         raise PaymentError(_("Payment amount 0 is not accepted"))
57 
58     if pay_type == Payment.TPAY and not payer.tpay_enabled:
59         raise PaymentError(_("This user does not have Thalia Pay enabled"))
60 
61     if not payable.paying_allowed:
62         raise PaymentError(_("Payment restricted"))
63 
64     if payable.payment is not None:
65         payable.payment.amount = payable.payment_amount
66         payable.payment.notes = payable.payment_notes
67         payable.payment.topic = payable.payment_topic
68         payable.payment.paid_by = payer
69         payable.payment.processed_by = processed_by
70         payable.payment.type = pay_type
71         payable.payment.save()
72     else:
73         payable.payment = Payment.objects.create(
74             processed_by=processed_by,
75             amount=payable.payment_amount,
76             notes=payable.payment_notes,
77             topic=payable.payment_topic,
78             paid_by=payer,
79             type=pay_type,
80         )
81     return payable.payment
82 
83 
84 def delete_payment(model: Model, member: Member = None, ignore_change_window=False):
85     """Remove a payment from a payable object.
86 
87     :param model: Payable or Model object
88     :param member: member deleting the payment
89     :param ignore_change_window: ignore the payment change window
90     :return:
91     """
92     payable = payables.get_payable(model)
93 
94     if member and not payable.can_manage_payment(member):
95         raise PaymentError(
96             _("User deleting payment does not have the right permissions.")
97         )
98 
99     payment = payable.payment
100     if (
101         payment.created_at
102         < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)
103         and not ignore_change_window
104     ):
105         raise PaymentError(_("This payment cannot be deleted anymore."))
106     if payment.batch and payment.batch.processed:
107         raise PaymentError(
108             _("This payment has already been processed and hence cannot be deleted.")
109         )
110 
111     payable.payment = None
112     payable.model.save()
113     payment.delete()
114 
115 
116 def update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:
117     """Update the last used field of a BankAccount queryset.
118 
119     :param queryset: Queryset of BankAccounts
120     :param date: date to set last_used to
121     :return: number of affected rows
122     """
123     if not date:
124         date = timezone.now().date()
125 
126     result = queryset.filter(
127         (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))
128         | Q(valid_until=None)
129     ).update(last_used=date)
130     return result
131 
132 
133 def revoke_old_mandates() -> int:
134     """Revoke all mandates that have not been used for 36 months or more.
135 
136     :return: number of affected rows
137     """
138     return BankAccount.objects.filter(
139         last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))
140     ).update(valid_until=timezone.now().date())
141 
142 
143 def process_batch(batch):
144     """Process a Thalia Pay batch.
145 
146     :param batch: the batch to be processed
147     :return:
148     """
149     batch.processed = True
150 
151     payments = batch.payments_set.select_related("paid_by")
152     for payment in payments:
153         bank_account = payment.paid_by.bank_accounts.last()
154         if not bank_account:  # pragma: no cover
155             # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)
156             payment.batch = None
157             payment.save()
158         else:
159             bank_account.last_used = batch.withdrawal_date
160             bank_account.save()
161 
162     batch.save()
163     processed_batch.send(sender=None, instance=batch)
164 
165     send_tpay_batch_processing_emails(batch)
166 
167 
168 def derive_next_mandate_no(member) -> str:
169     accounts = (
170         BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))
171         .exclude(mandate_no=None)
172         .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)
173     )
174     new_mandate_no = 1 + max(
175         (int(account.mandate_no.split("-")[1]) for account in accounts), default=0
176     )
177     return f"{member.pk}-{new_mandate_no}"
178 
179 
180 def send_tpay_batch_processing_emails(batch):
181     """Send withdrawal notice emails to all members in a batch."""
182     member_payments = batch.payments_set.values("paid_by").annotate(total=Sum("amount"))
183     with mail.get_connection() as connection:
184         for member_row in member_payments:
185             member = PaymentUser.objects.get(pk=member_row["paid_by"])
186             total_amount = member_row["total"]
187 
188             send_email(
189                 to=[member.email],
190                 subject="Thalia Pay withdrawal notice",
191                 txt_template="payments/email/tpay_withdrawal_notice_mail.txt",
192                 html_template="payments/email/tpay_withdrawal_notice_mail.html",
193                 connection=connection,
194                 context={
195                     "name": member.get_full_name(),
196                     "batch": batch,
197                     "bank_account": member.bank_accounts.filter(
198                         mandate_no__isnull=False
199                     ).last(),
200                     "creditor_id": settings.SEPA_CREDITOR_ID,
201                     "payments": batch.payments_set.filter(paid_by=member),
202                     "total_amount": total_amount,
203                     "payments_url": (
204                         settings.BASE_URL
205                         + reverse(
206                             "payments:payment-list",
207                         )
208                     ),
209                 },
210             )
211     return len(member_payments)
212 
213 
214 def execute_data_minimisation(dry_run=False):
215     """Anonymizes payments older than 7 years."""
216     # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late
217     payment_deletion_period = timezone.now().date() - timezone.timedelta(days=365 * 7)
218     bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=31 * 13)
219 
220     queryset_payments = Payment.objects.filter(
221         created_at__lte=payment_deletion_period
222     ).exclude(paid_by__isnull=True)
223 
224     # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months
225     # (13 months is the required time we need to keep the mandates for)
226     queryset_bankaccounts = BankAccount.objects.all()
227     queryset_bankaccounts = queryset_bankaccounts.filter(
228         valid_until__lt=timezone.now()
229     )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)
230     queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that
231         Q(
232             owner__paid_payment_set__type=Payment.TPAY
233         ),  # are used for Thalia Pay payments, AND
234         Q(
235             owner__paid_payment_set__batch__isnull=True
236         )  # have a payment that is in no batch, OR
237         | Q(
238             owner__paid_payment_set__batch__processed=False
239         )  # have an unprocessed batch, OR
240         | Q(
241             owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months
242         ),
243     )
244 
245     if not dry_run:
246         queryset_payments.update(paid_by=None, processed_by=None)
247         queryset_bankaccounts.delete()
248     return queryset_payments
249
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/website/payments/services.py b/website/payments/services.py
--- a/website/payments/services.py
+++ b/website/payments/services.py
@@ -152,12 +152,13 @@
     for payment in payments:
         bank_account = payment.paid_by.bank_accounts.last()
         if not bank_account:  # pragma: no cover
-            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)
+            # This should not happen, cannot happen, does not happen (right... ;p)
+            # but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)
             payment.batch = None
             payment.save()
         else:
             bank_account.last_used = batch.withdrawal_date
-            bank_account.save()
+            bank_account.save(update_fields=["last_used"])
 
     batch.save()
     processed_batch.send(sender=None, instance=batch)

{"golden_diff": "diff --git a/website/payments/services.py b/website/payments/services.py\n--- a/website/payments/services.py\n+++ b/website/payments/services.py\n@@ -152,12 +152,13 @@\n     for payment in payments:\n         bank_account = payment.paid_by.bank_accounts.last()\n         if not bank_account:  # pragma: no cover\n-            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n+            # This should not happen, cannot happen, does not happen (right... ;p)\n+            # but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n             payment.batch = None\n             payment.save()\n         else:\n             bank_account.last_used = batch.withdrawal_date\n-            bank_account.save()\n+            bank_account.save(update_fields=[\"last_used\"])\n \n     batch.save()\n     processed_batch.send(sender=None, instance=batch)\n", "issue": "ThaliaPay batch push throttled by Moneybird\nWhenever we process a ThaliaPay batch, we are throttled by Moneybird on the amount of API requests we make.\r\nThis is most likely due to saving the bank accounts which are pushed to moneybird when the post_save signal is triggered.\r\nThis shouldn't be triggered when the last_used field of the bank account is updated, but it is.\r\n\r\nPotential fix:\r\nMake sure that updated_fields is passed (correctly) with the post_save signal for bank accounts.\n", "before_files": [{"content": "\"\"\"The services defined by the payments package.\"\"\"\nimport datetime\nfrom typing import Union\n\nfrom django.conf import settings\nfrom django.core import mail\nfrom django.db.models import Model, Q, QuerySet, Sum\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom members.models import Member\nfrom utils.snippets import send_email\n\nfrom .exceptions import PaymentError\nfrom .models import BankAccount, Payment, PaymentUser\nfrom .payables import Payable, payables\nfrom .signals import processed_batch\n\n\ndef create_payment(\n    model_payable: Union[Model, Payable],\n    processed_by: Member,\n    pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],\n) -> Payment:\n    \"\"\"Create a new payment from a payable object.\n\n    :param model_payable: Payable or Model object\n    :param processed_by: PaymentUser that processed this payment\n    :param pay_type: Payment type\n    :return: Payment object\n    \"\"\"\n    if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):\n        raise PaymentError(\"Invalid payment type\")\n\n    if isinstance(model_payable, Payable):\n        payable = model_payable\n    else:\n        payable = payables.get_payable(model_payable)\n\n    payer = (\n        PaymentUser.objects.get(pk=payable.payment_payer.pk)\n        if payable.payment_payer\n        else None\n    )\n\n    if not (\n        (payer and payer == processed_by and pay_type == Payment.TPAY)\n        or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)\n    ):\n        raise PaymentError(\n            _(\"User processing payment does not have the right permissions\")\n        )\n\n    if payable.payment_amount == 0:\n        raise PaymentError(_(\"Payment amount 0 is not accepted\"))\n\n    if pay_type == Payment.TPAY and not payer.tpay_enabled:\n        raise PaymentError(_(\"This user does not have Thalia Pay enabled\"))\n\n    if not payable.paying_allowed:\n        raise PaymentError(_(\"Payment restricted\"))\n\n    if payable.payment is not None:\n        payable.payment.amount = payable.payment_amount\n        payable.payment.notes = payable.payment_notes\n        payable.payment.topic = payable.payment_topic\n        payable.payment.paid_by = payer\n        payable.payment.processed_by = processed_by\n        payable.payment.type = pay_type\n        payable.payment.save()\n    else:\n        payable.payment = Payment.objects.create(\n            processed_by=processed_by,\n            amount=payable.payment_amount,\n            notes=payable.payment_notes,\n            topic=payable.payment_topic,\n            paid_by=payer,\n            type=pay_type,\n        )\n    return payable.payment\n\n\ndef delete_payment(model: Model, member: Member = None, ignore_change_window=False):\n    \"\"\"Remove a payment from a payable object.\n\n    :param model: Payable or Model object\n    :param member: member deleting the payment\n    :param ignore_change_window: ignore the payment change window\n    :return:\n    \"\"\"\n    payable = payables.get_payable(model)\n\n    if member and not payable.can_manage_payment(member):\n        raise PaymentError(\n            _(\"User deleting payment does not have the right permissions.\")\n        )\n\n    payment = payable.payment\n    if (\n        payment.created_at\n        < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)\n        and not ignore_change_window\n    ):\n        raise PaymentError(_(\"This payment cannot be deleted anymore.\"))\n    if payment.batch and payment.batch.processed:\n        raise PaymentError(\n            _(\"This payment has already been processed and hence cannot be deleted.\")\n        )\n\n    payable.payment = None\n    payable.model.save()\n    payment.delete()\n\n\ndef update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:\n    \"\"\"Update the last used field of a BankAccount queryset.\n\n    :param queryset: Queryset of BankAccounts\n    :param date: date to set last_used to\n    :return: number of affected rows\n    \"\"\"\n    if not date:\n        date = timezone.now().date()\n\n    result = queryset.filter(\n        (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))\n        | Q(valid_until=None)\n    ).update(last_used=date)\n    return result\n\n\ndef revoke_old_mandates() -> int:\n    \"\"\"Revoke all mandates that have not been used for 36 months or more.\n\n    :return: number of affected rows\n    \"\"\"\n    return BankAccount.objects.filter(\n        last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))\n    ).update(valid_until=timezone.now().date())\n\n\ndef process_batch(batch):\n    \"\"\"Process a Thalia Pay batch.\n\n    :param batch: the batch to be processed\n    :return:\n    \"\"\"\n    batch.processed = True\n\n    payments = batch.payments_set.select_related(\"paid_by\")\n    for payment in payments:\n        bank_account = payment.paid_by.bank_accounts.last()\n        if not bank_account:  # pragma: no cover\n            # This should not happen, cannot haver, does not happen (right... ;p), but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n            payment.batch = None\n            payment.save()\n        else:\n            bank_account.last_used = batch.withdrawal_date\n            bank_account.save()\n\n    batch.save()\n    processed_batch.send(sender=None, instance=batch)\n\n    send_tpay_batch_processing_emails(batch)\n\n\ndef derive_next_mandate_no(member) -> str:\n    accounts = (\n        BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))\n        .exclude(mandate_no=None)\n        .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)\n    )\n    new_mandate_no = 1 + max(\n        (int(account.mandate_no.split(\"-\")[1]) for account in accounts), default=0\n    )\n    return f\"{member.pk}-{new_mandate_no}\"\n\n\ndef send_tpay_batch_processing_emails(batch):\n    \"\"\"Send withdrawal notice emails to all members in a batch.\"\"\"\n    member_payments = batch.payments_set.values(\"paid_by\").annotate(total=Sum(\"amount\"))\n    with mail.get_connection() as connection:\n        for member_row in member_payments:\n            member = PaymentUser.objects.get(pk=member_row[\"paid_by\"])\n            total_amount = member_row[\"total\"]\n\n            send_email(\n                to=[member.email],\n                subject=\"Thalia Pay withdrawal notice\",\n                txt_template=\"payments/email/tpay_withdrawal_notice_mail.txt\",\n                html_template=\"payments/email/tpay_withdrawal_notice_mail.html\",\n                connection=connection,\n                context={\n                    \"name\": member.get_full_name(),\n                    \"batch\": batch,\n                    \"bank_account\": member.bank_accounts.filter(\n                        mandate_no__isnull=False\n                    ).last(),\n                    \"creditor_id\": settings.SEPA_CREDITOR_ID,\n                    \"payments\": batch.payments_set.filter(paid_by=member),\n                    \"total_amount\": total_amount,\n                    \"payments_url\": (\n                        settings.BASE_URL\n                        + reverse(\n                            \"payments:payment-list\",\n                        )\n                    ),\n                },\n            )\n    return len(member_payments)\n\n\ndef execute_data_minimisation(dry_run=False):\n    \"\"\"Anonymizes payments older than 7 years.\"\"\"\n    # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late\n    payment_deletion_period = timezone.now().date() - timezone.timedelta(days=365 * 7)\n    bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=31 * 13)\n\n    queryset_payments = Payment.objects.filter(\n        created_at__lte=payment_deletion_period\n    ).exclude(paid_by__isnull=True)\n\n    # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months\n    # (13 months is the required time we need to keep the mandates for)\n    queryset_bankaccounts = BankAccount.objects.all()\n    queryset_bankaccounts = queryset_bankaccounts.filter(\n        valid_until__lt=timezone.now()\n    )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)\n    queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that\n        Q(\n            owner__paid_payment_set__type=Payment.TPAY\n        ),  # are used for Thalia Pay payments, AND\n        Q(\n            owner__paid_payment_set__batch__isnull=True\n        )  # have a payment that is in no batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processed=False\n        )  # have an unprocessed batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months\n        ),\n    )\n\n    if not dry_run:\n        queryset_payments.update(paid_by=None, processed_by=None)\n        queryset_bankaccounts.delete()\n    return queryset_payments\n", "path": "website/payments/services.py"}], "after_files": [{"content": "\"\"\"The services defined by the payments package.\"\"\"\nimport datetime\nfrom typing import Union\n\nfrom django.conf import settings\nfrom django.core import mail\nfrom django.db.models import Model, Q, QuerySet, Sum\nfrom django.urls import reverse\nfrom django.utils import timezone\nfrom django.utils.translation import gettext_lazy as _\n\nfrom members.models import Member\nfrom utils.snippets import send_email\n\nfrom .exceptions import PaymentError\nfrom .models import BankAccount, Payment, PaymentUser\nfrom .payables import Payable, payables\nfrom .signals import processed_batch\n\n\ndef create_payment(\n    model_payable: Union[Model, Payable],\n    processed_by: Member,\n    pay_type: Union[Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY],\n) -> Payment:\n    \"\"\"Create a new payment from a payable object.\n\n    :param model_payable: Payable or Model object\n    :param processed_by: PaymentUser that processed this payment\n    :param pay_type: Payment type\n    :return: Payment object\n    \"\"\"\n    if pay_type not in (Payment.CASH, Payment.CARD, Payment.WIRE, Payment.TPAY):\n        raise PaymentError(\"Invalid payment type\")\n\n    if isinstance(model_payable, Payable):\n        payable = model_payable\n    else:\n        payable = payables.get_payable(model_payable)\n\n    payer = (\n        PaymentUser.objects.get(pk=payable.payment_payer.pk)\n        if payable.payment_payer\n        else None\n    )\n\n    if not (\n        (payer and payer == processed_by and pay_type == Payment.TPAY)\n        or (payable.can_manage_payment(processed_by) and pay_type != Payment.TPAY)\n    ):\n        raise PaymentError(\n            _(\"User processing payment does not have the right permissions\")\n        )\n\n    if payable.payment_amount == 0:\n        raise PaymentError(_(\"Payment amount 0 is not accepted\"))\n\n    if pay_type == Payment.TPAY and not payer.tpay_enabled:\n        raise PaymentError(_(\"This user does not have Thalia Pay enabled\"))\n\n    if not payable.paying_allowed:\n        raise PaymentError(_(\"Payment restricted\"))\n\n    if payable.payment is not None:\n        payable.payment.amount = payable.payment_amount\n        payable.payment.notes = payable.payment_notes\n        payable.payment.topic = payable.payment_topic\n        payable.payment.paid_by = payer\n        payable.payment.processed_by = processed_by\n        payable.payment.type = pay_type\n        payable.payment.save()\n    else:\n        payable.payment = Payment.objects.create(\n            processed_by=processed_by,\n            amount=payable.payment_amount,\n            notes=payable.payment_notes,\n            topic=payable.payment_topic,\n            paid_by=payer,\n            type=pay_type,\n        )\n    return payable.payment\n\n\ndef delete_payment(model: Model, member: Member = None, ignore_change_window=False):\n    \"\"\"Remove a payment from a payable object.\n\n    :param model: Payable or Model object\n    :param member: member deleting the payment\n    :param ignore_change_window: ignore the payment change window\n    :return:\n    \"\"\"\n    payable = payables.get_payable(model)\n\n    if member and not payable.can_manage_payment(member):\n        raise PaymentError(\n            _(\"User deleting payment does not have the right permissions.\")\n        )\n\n    payment = payable.payment\n    if (\n        payment.created_at\n        < timezone.now() - timezone.timedelta(seconds=settings.PAYMENT_CHANGE_WINDOW)\n        and not ignore_change_window\n    ):\n        raise PaymentError(_(\"This payment cannot be deleted anymore.\"))\n    if payment.batch and payment.batch.processed:\n        raise PaymentError(\n            _(\"This payment has already been processed and hence cannot be deleted.\")\n        )\n\n    payable.payment = None\n    payable.model.save()\n    payment.delete()\n\n\ndef update_last_used(queryset: QuerySet, date: datetime.date = None) -> int:\n    \"\"\"Update the last used field of a BankAccount queryset.\n\n    :param queryset: Queryset of BankAccounts\n    :param date: date to set last_used to\n    :return: number of affected rows\n    \"\"\"\n    if not date:\n        date = timezone.now().date()\n\n    result = queryset.filter(\n        (Q(valid_from__gte=timezone.now()) & Q(valid_until__lt=timezone.now()))\n        | Q(valid_until=None)\n    ).update(last_used=date)\n    return result\n\n\ndef revoke_old_mandates() -> int:\n    \"\"\"Revoke all mandates that have not been used for 36 months or more.\n\n    :return: number of affected rows\n    \"\"\"\n    return BankAccount.objects.filter(\n        last_used__lte=(timezone.now() - timezone.timedelta(days=36 * 30))\n    ).update(valid_until=timezone.now().date())\n\n\ndef process_batch(batch):\n    \"\"\"Process a Thalia Pay batch.\n\n    :param batch: the batch to be processed\n    :return:\n    \"\"\"\n    batch.processed = True\n\n    payments = batch.payments_set.select_related(\"paid_by\")\n    for payment in payments:\n        bank_account = payment.paid_by.bank_accounts.last()\n        if not bank_account:  # pragma: no cover\n            # This should not happen, cannot happen, does not happen (right... ;p)\n            # but if it does, we don't want to crash, but just remove the payment from the batch (make it unprocessed)\n            payment.batch = None\n            payment.save()\n        else:\n            bank_account.last_used = batch.withdrawal_date\n            bank_account.save(update_fields=[\"last_used\"])\n\n    batch.save()\n    processed_batch.send(sender=None, instance=batch)\n\n    send_tpay_batch_processing_emails(batch)\n\n\ndef derive_next_mandate_no(member) -> str:\n    accounts = (\n        BankAccount.objects.filter(owner=PaymentUser.objects.get(pk=member.pk))\n        .exclude(mandate_no=None)\n        .filter(mandate_no__regex=BankAccount.MANDATE_NO_DEFAULT_REGEX)\n    )\n    new_mandate_no = 1 + max(\n        (int(account.mandate_no.split(\"-\")[1]) for account in accounts), default=0\n    )\n    return f\"{member.pk}-{new_mandate_no}\"\n\n\ndef send_tpay_batch_processing_emails(batch):\n    \"\"\"Send withdrawal notice emails to all members in a batch.\"\"\"\n    member_payments = batch.payments_set.values(\"paid_by\").annotate(total=Sum(\"amount\"))\n    with mail.get_connection() as connection:\n        for member_row in member_payments:\n            member = PaymentUser.objects.get(pk=member_row[\"paid_by\"])\n            total_amount = member_row[\"total\"]\n\n            send_email(\n                to=[member.email],\n                subject=\"Thalia Pay withdrawal notice\",\n                txt_template=\"payments/email/tpay_withdrawal_notice_mail.txt\",\n                html_template=\"payments/email/tpay_withdrawal_notice_mail.html\",\n                connection=connection,\n                context={\n                    \"name\": member.get_full_name(),\n                    \"batch\": batch,\n                    \"bank_account\": member.bank_accounts.filter(\n                        mandate_no__isnull=False\n                    ).last(),\n                    \"creditor_id\": settings.SEPA_CREDITOR_ID,\n                    \"payments\": batch.payments_set.filter(paid_by=member),\n                    \"total_amount\": total_amount,\n                    \"payments_url\": (\n                        settings.BASE_URL\n                        + reverse(\n                            \"payments:payment-list\",\n                        )\n                    ),\n                },\n            )\n    return len(member_payments)\n\n\ndef execute_data_minimisation(dry_run=False):\n    \"\"\"Anonymizes payments older than 7 years.\"\"\"\n    # Sometimes years are 366 days of course, but better delete 1 or 2 days early than late\n    payment_deletion_period = timezone.now().date() - timezone.timedelta(days=365 * 7)\n    bankaccount_deletion_period = timezone.now() - datetime.timedelta(days=31 * 13)\n\n    queryset_payments = Payment.objects.filter(\n        created_at__lte=payment_deletion_period\n    ).exclude(paid_by__isnull=True)\n\n    # Delete bank accounts that are not valid anymore, and have not been used in the last 13 months\n    # (13 months is the required time we need to keep the mandates for)\n    queryset_bankaccounts = BankAccount.objects.all()\n    queryset_bankaccounts = queryset_bankaccounts.filter(\n        valid_until__lt=timezone.now()\n    )  # We must always keep valid bank accounts. so we only select the ones that are not valid anymore (valid_until < now)\n    queryset_bankaccounts = queryset_bankaccounts.exclude(  # Also keep bank accounts that\n        Q(\n            owner__paid_payment_set__type=Payment.TPAY\n        ),  # are used for Thalia Pay payments, AND\n        Q(\n            owner__paid_payment_set__batch__isnull=True\n        )  # have a payment that is in no batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processed=False\n        )  # have an unprocessed batch, OR\n        | Q(\n            owner__paid_payment_set__batch__processing_date__gt=bankaccount_deletion_period  # or have a processed batch that is not older than 13 months\n        ),\n    )\n\n    if not dry_run:\n        queryset_payments.update(paid_by=None, processed_by=None)\n        queryset_bankaccounts.delete()\n    return queryset_payments\n", "path": "website/payments/services.py"}]}