From fb01e5f09ee4343088ed065f1ca4494498e98fe4 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Fri, 2 Aug 2024 17:37:28 -0400 Subject: [PATCH 1/2] feat(users): Adds retry logic for Neon account creation on HTTP 500 errors --- cl/users/tasks.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cl/users/tasks.py b/cl/users/tasks.py index 3efe2fc59c..1ac1f8d6ca 100644 --- a/cl/users/tasks.py +++ b/cl/users/tasks.py @@ -1,4 +1,5 @@ import logging +from http import HTTPStatus from urllib.parse import urljoin from celery import Task @@ -7,7 +8,7 @@ from django.core.mail import send_mail from django.template import loader from django.utils.timezone import now -from requests.exceptions import Timeout +from requests.exceptions import HTTPError, Timeout from cl.api.models import Webhook, WebhookEvent from cl.celery_init import app @@ -77,7 +78,15 @@ def create_neon_account(self: Task, user_id: int) -> None: if len(neon_accounts) == 0: # No account found, create one - new_account_id = neon_client.create_account(user) + try: + new_account_id = neon_client.create_account(user) + except HTTPError as exc: + if ( + exc.response.status_code != HTTPStatus.INTERNAL_SERVER_ERROR + or self.request.retries == self.max_retries + ): + raise exc + raise self.retry(exc=exc) profile.neon_account_id = new_account_id profile.save(update_fields=["neon_account_id"]) From 06d10b33eb36d5dc2d5b690c6685add9beeb8244 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Fri, 2 Aug 2024 17:45:55 -0400 Subject: [PATCH 2/2] fix(donate): Updates logic to match user records --- cl/donate/api_views.py | 3 ++- cl/donate/tests.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cl/donate/api_views.py b/cl/donate/api_views.py index b13deb9a1b..6b2503de8d 100644 --- a/cl/donate/api_views.py +++ b/cl/donate/api_views.py @@ -4,6 +4,7 @@ from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.db import transaction +from django.db.models import F from django.http import HttpResponse from rest_framework import mixins, serializers, viewsets from rest_framework.request import Request @@ -117,7 +118,7 @@ def _get_member_record(self, account_id: str) -> User: contact_data = neon_account["primaryContact"] users = User.objects.filter( email__iexact=contact_data["email1"] - ).order_by("-last_login") + ).order_by(F("last_login").desc(nulls_last=True)) if not users.exists(): address = self._get_address_from_neon_response( contact_data["addresses"] diff --git a/cl/donate/tests.py b/cl/donate/tests.py index 0d5d950822..9fa22504d7 100644 --- a/cl/donate/tests.py +++ b/cl/donate/tests.py @@ -1,7 +1,9 @@ +from collections import defaultdict from datetime import timedelta from http import HTTPStatus from unittest.mock import patch +from asgiref.sync import sync_to_async from django.core import mail from django.test import override_settings from django.test.client import AsyncClient, Client @@ -17,6 +19,7 @@ from cl.lib.test_helpers import UserProfileWithParentsFactory from cl.tests.cases import TestCase from cl.users.models import UserProfile +from cl.users.utils import create_stub_account class EmailCommandTest(TestCase): @@ -376,3 +379,59 @@ async def test_uses_insensitive_match_for_emails( # Check the neon_account_id was updated properly self.assertEqual(membership.user.profile.neon_account_id, "9524") + + @patch( + "cl.lib.neon_utils.NeonClient.get_acount_by_id", + ) + @patch.object( + MembershipWebhookViewSet, "_store_webhook_payload", return_value=None + ) + async def test_updates_account_with_recent_login( + self, mock_store_webhook, mock_get_account + ) -> None: + # Create two profile records - one stub, one regular user, + _, stub_profile = await sync_to_async(create_stub_account)( + { + "email": "test_4@email.com", + "first_name": "test", + "last_name": "test", + }, + defaultdict(lambda: ""), + ) + + user_profile = await sync_to_async(UserProfileWithParentsFactory)( + user__email="test_4@email.com" + ) + user = user_profile.user + # Updates last login field for the regular user + user.last_login = now() + await user.asave() + + # mocks the Neon API response + mock_get_account.return_value = { + "accountId": "1246", + "primaryContact": { + "email1": "test_4@email.com", + "firstName": "test", + "lastName": "test", + }, + } + + self.data["eventTrigger"] = "createMembership" + self.data["data"]["membership"]["accountId"] = "1246" + r = await self.async_client.post( + reverse("membership-webhooks-list", kwargs={"version": "v3"}), + data=self.data, + content_type="application/json", + ) + self.assertEqual(r.status_code, HTTPStatus.CREATED) + + # Refresh both profiles to ensure updated data + await stub_profile.arefresh_from_db() + await user_profile.arefresh_from_db() + + # Verify stub account remains untouched + self.assertEqual(stub_profile.neon_account_id, "") + + # Verify regular user account is updated with Neon data + self.assertEqual(user_profile.neon_account_id, "1246")