Skip to content

Commit

Permalink
Merge pull request freelawproject#4275 from freelawproject/4204-feat-…
Browse files Browse the repository at this point in the history
…retry-neon-creation-task

feat(neon): Adds retry logic for Neon account creation
  • Loading branch information
mlissner authored Aug 5, 2024
2 parents 2d72c0e + 9c55631 commit 1a71ef3
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 3 deletions.
3 changes: 2 additions & 1 deletion cl/donate/api_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down
59 changes: 59 additions & 0 deletions cl/donate/tests.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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": "[email protected]",
"first_name": "test",
"last_name": "test",
},
defaultdict(lambda: ""),
)

user_profile = await sync_to_async(UserProfileWithParentsFactory)(
user__email="[email protected]"
)
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": "[email protected]",
"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")
13 changes: 11 additions & 2 deletions cl/users/tasks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from http import HTTPStatus
from urllib.parse import urljoin

from celery import Task
Expand All @@ -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
Expand Down Expand Up @@ -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"])

Expand Down

0 comments on commit 1a71ef3

Please sign in to comment.