Repository: svthalia/concrexit

Issue:
Handle moneybird exception 400: contact cannot be archived
### Describe the bug
[CONCREXIT-18Z](https://thalia.sentry.io/issues/4723527093/)

When trying to delete/archive a dataminimized use on moneybird, we sometimes get a 400. I checked with Roel and it appears that these users have already been archived. So I'm thinking we either need to just clean up these 6 cases on production once, or we can ignore the 400 and proceed normally by just deleting the moneybirdcontact from out database.

### How to reproduce
<!-- Steps to reproduce the behaviour -->
1. In moneybird, look up one of the contacts that we tried to archive at night and see that they're archived already.





Discussion:

Resolving patch:
diff --git a/website/moneybirdsynchronization/administration.py b/website/moneybirdsynchronization/administration.py
--- a/website/moneybirdsynchronization/administration.py
+++ b/website/moneybirdsynchronization/administration.py
@@ -56,6 +56,9 @@ def __init__(self, status_code: int, description: str | None = None):
             if description:
                 msg += f": {description}"
 
+            self.status_code = status_code
+            self.description = description
+
             super().__init__(msg)
 
     class Unauthorized(Error):
diff --git a/website/moneybirdsynchronization/services.py b/website/moneybirdsynchronization/services.py
--- a/website/moneybirdsynchronization/services.py
+++ b/website/moneybirdsynchronization/services.py
@@ -67,23 +67,30 @@ def create_or_update_contact(member: Member):
     return moneybird_contact
 
 
-def delete_contact(member):
-    """Delete a Django user/member from Moneybird."""
+def delete_contact(contact: MoneybirdContact):
+    """Delete or archive a contact on Moneybird, and delete our record of it."""
     if not settings.MONEYBIRD_SYNC_ENABLED:
         return
 
-    try:
-        moneybird_contact = MoneybirdContact.objects.get(member=member)
-    except MoneybirdContact.DoesNotExist:
-        return
-
-    if moneybird_contact.moneybird_id is None:
-        moneybird_contact.delete()
+    if contact.moneybird_id is None:
+        contact.delete()
         return
 
     moneybird = get_moneybird_api_service()
-    moneybird.delete_contact(moneybird_contact.moneybird_id)
-    moneybird_contact.delete()
+    try:
+        moneybird.delete_contact(contact.moneybird_id)
+        contact.delete()
+    except Administration.InvalidData as e:
+        if e.status_code == 400 and e.description == "Contact can not be archived":
+            # Contact is most likely already archived, so we can delete it.
+            logger.warning(
+                "Contact %s for member %s could not be archived.",
+                contact.moneybird_id,
+                contact.member,
+            )
+            contact.delete()
+        else:
+            raise
 
 
 def create_or_update_external_invoice(obj):
@@ -262,7 +269,7 @@ def _sync_outdated_invoices():
 
 def _sync_contacts():
     logger.info("Synchronizing contacts...")
-    # make moneybird contacts for people that dont have it
+    # Make moneybird contacts for people that dont have.
     for member in Member.objects.filter(
         moneybird_contact__isnull=True, profile__is_minimized=False
     ):
@@ -272,7 +279,7 @@ def _sync_contacts():
             logger.exception("Moneybird synchronization error: %s", e)
             send_sync_error(e, member)
 
-    # update moneybird contacts that need synchronization
+    # Update moneybird contacts that need synchronization.
     for contact in MoneybirdContact.objects.filter(needs_synchronization=True):
         try:
             create_or_update_contact(contact.member)
@@ -280,13 +287,13 @@ def _sync_contacts():
             logger.exception("Moneybird synchronization error: %s", e)
             send_sync_error(e, contact.member)
 
-    # archive moneybrid contacts where mb contact has not been archived but user was minimized
-    for minimized in Member.objects.filter(profile__is_minimized=True):
+    # Archive moneybrid contacts where mb contact has not been archived but user was minimized.
+    for contact in MoneybirdContact.objects.filter(member__profile__is_minimized=True):
         try:
-            delete_contact(minimized)
+            delete_contact(contact)
         except Administration.Error as e:
             logger.exception("Moneybird synchronization error: %s", e)
-            send_sync_error(e, minimized)
+            send_sync_error(e, contact)
 
     _sync_contacts_with_outdated_mandates()
 


Test patch:
diff --git a/website/moneybirdsynchronization/tests/test_services.py b/website/moneybirdsynchronization/tests/test_services.py
--- a/website/moneybirdsynchronization/tests/test_services.py
+++ b/website/moneybirdsynchronization/tests/test_services.py
@@ -520,7 +520,7 @@ def test_sync_event_registrations(
     @mock.patch("moneybirdsynchronization.services.create_or_update_contact")
     @mock.patch(
         "moneybirdsynchronization.services._sync_contacts_with_outdated_mandates"
-    )  # this is to prevent sync_contacts from actaully calling the function, since it is testet in another test
+    )  # Prevent sync_contacts from actaully calling the function, since it is tested separately.
     def test_sync_contacts(
         self,
         mock_sync_contacts_with_outdated_mandates,
@@ -591,4 +591,4 @@ def test_sync_contacts(
 
         self.assertEqual(mock_create_or_update_contact.call_count, 0)
         self.assertEqual(mock_delete_contact.call_count, 1)
-        mock_delete_contact.assert_any_call(self.member)
+        mock_delete_contact.assert_any_call(self.member.moneybird_contact)