diff --git a/lemarche/api/tenders/tests.py b/lemarche/api/tenders/tests.py index 3ff4b41de..c345cdfb5 100644 --- a/lemarche/api/tenders/tests.py +++ b/lemarche/api/tenders/tests.py @@ -1,3 +1,6 @@ +from unittest.mock import patch + +from django.conf import settings from django.test import TestCase from django.urls import reverse @@ -44,11 +47,29 @@ class TenderCreateApiTest(TestCase): def setUpTestData(cls): cls.url = reverse("api:tenders-list") + "?token=admin" cls.user = UserFactory() + cls.user_buyer = UserFactory(kind=User.KIND_BUYER, company_name="Entreprise Buyer") cls.user_with_token = UserFactory(email="admin@example.com", api_key="admin") cls.perimeter = PerimeterFactory() cls.sector_1 = SectorFactory() cls.sector_2 = SectorFactory() + @patch("lemarche.api.tenders.views.get_or_create_user_from_anonymous_content") + def setup_mock_user_and_tender_creation(self, mock_get_user, user=None, title="Test Tally", extra_data=None): + """Helper method to setup mock user and create a tender.""" + user = user if user else self.user + mock_get_user.return_value = user + + # Tender data + tender_data = TENDER_JSON.copy() + tender_data["title"] = title + tender_data["extra_data"] = extra_data or {} + + # Tender creation + response = self.client.post(self.url, data=tender_data, content_type="application/json") + tender = Tender.objects.get(title=title) + + return response, tender, user + def test_anonymous_user_cannot_create_tender(self): url = reverse("api:tenders-list") response = self.client.post(url, data=TENDER_JSON, content_type="application/json") @@ -67,7 +88,7 @@ def test_user_with_valid_api_key_can_create_tender(self): self.assertEqual(response.status_code, 201) self.assertIn("slug", response.data.keys()) tender = Tender.objects.get(title="Test author 1") - self.assertEqual(User.objects.count(), 2 + 1) # created a new user + self.assertEqual(User.objects.count(), 3 + 1) # created a new user self.assertEqual(tender.author.email, USER_CONTACT_EMAIL) self.assertEqual(tender.status, tender_constants.STATUS_PUBLISHED) self.assertEqual(tender.source, tender_constants.SOURCE_API) @@ -81,7 +102,7 @@ def test_user_with_valid_api_key_can_create_tender(self): self.assertEqual(response.status_code, 201) self.assertIn("slug", response.data.keys()) tender = Tender.objects.get(title="Test author 2") - self.assertEqual(User.objects.count(), 3) # did not create a new user + self.assertEqual(User.objects.count(), 4) # did not create a new user self.assertEqual(tender.author, self.user_with_token) self.assertEqual(tender.status, tender_constants.STATUS_PUBLISHED) self.assertEqual(tender.source, tender_constants.SOURCE_API) @@ -135,14 +156,45 @@ def test_create_tender_with_sectors(self): response = self.client.post(self.url, data=tender_data) self.assertEqual(response.status_code, 400) - def test_create_tender_with_tally_source(self): - tender_data = TENDER_JSON.copy() - tender_data["title"] = "Test tally" - tender_data["extra_data"] = {"source": "TALLY"} - response = self.client.post(self.url, data=tender_data, content_type="application/json") + @patch("lemarche.api.tenders.views.add_to_contact_list") + def test_create_tender_with_tally_source(self, mock_add_to_contact_list): + extra_data = {"source": "TALLY"} + response, tender, user = self.setup_mock_user_and_tender_creation(title="Test tally", extra_data=extra_data) + + mock_add_to_contact_list.assert_called_once() + args, kwargs = mock_add_to_contact_list.call_args + self.assertEqual(response.status_code, 201) - tender = Tender.objects.get(title="Test tally") self.assertEqual(tender.source, tender_constants.SOURCE_TALLY) + # Check other arguments like user, type, and source + self.assertEqual(kwargs["user"], user) + self.assertEqual(kwargs["type"], "signup") + self.assertEqual(kwargs["source"], user_constants.SOURCE_TALLY_FORM) + # Verify that `tender` is an instance of Tender + self.assertIsInstance( + kwargs.get("tender"), Tender, "Expected an instance of Tender for the 'tender' argument." + ) + + @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CreateContact") + def test_create_contact_call_has_user_buyer_attributes(self, mock_create_contact): + """Test CreateContact call contains user buyer attributes""" + extra_data = {"source": "TALLY"} + _, tender, user = self.setup_mock_user_and_tender_creation( + title="Test tally", user=self.user_buyer, extra_data=extra_data + ) + sectors = tender.sectors.all() + + mock_create_contact.assert_called_once() + args, kwargs = mock_create_contact.call_args + attributes = kwargs["attributes"] + + self.assertEqual(kwargs["email"], user.email) + self.assertIn(settings.BREVO_CL_SIGNUP_BUYER_ID, kwargs["list_ids"]) + self.assertEqual(attributes["MONTANT_BESOIN_ACHETEUR"], tender.amount_int) + self.assertEqual(attributes["TYPE_BESOIN_ACHETEUR"], tender.kind) + + if sectors.exists(): + attributes["TYPE_VERTICALE_ACHETEUR"] = sectors.first().name def test_create_tender_with_different_contact_data(self): tender_data = TENDER_JSON.copy() diff --git a/lemarche/api/tenders/views.py b/lemarche/api/tenders/views.py index 4b3f5c6f0..15965529c 100644 --- a/lemarche/api/tenders/views.py +++ b/lemarche/api/tenders/views.py @@ -8,6 +8,7 @@ from lemarche.tenders import constants as tender_constants from lemarche.tenders.models import Tender from lemarche.users import constants as user_constants +from lemarche.utils.emails import add_to_contact_list from lemarche.www.tenders.utils import get_or_create_user_from_anonymous_content @@ -73,13 +74,14 @@ def perform_create(self, serializer: TenderSerializer): serializer.validated_data.pop("contact_kind", None) serializer.validated_data.pop("contact_buyer_kind_detail", None) # create Tender - serializer.save( + tender = serializer.save( author=user, status=tender_constants.STATUS_PUBLISHED, published_at=timezone.now(), source=tender_source, import_raw_object=self.request.data, ) + add_to_contact_list(user=user, type="signup", source=user_source, tender=tender) class TenderKindViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): diff --git a/lemarche/utils/apis/api_brevo.py b/lemarche/utils/apis/api_brevo.py index c68899e26..b85317879 100644 --- a/lemarche/utils/apis/api_brevo.py +++ b/lemarche/utils/apis/api_brevo.py @@ -4,9 +4,11 @@ import sib_api_v3_sdk from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist from huey.contrib.djhuey import task from sib_api_v3_sdk.rest import ApiException +from lemarche.tenders import constants as tender_constants from lemarche.utils.constants import EMAIL_SUBJECT_PREFIX from lemarche.utils.data import sanitize_to_send_by_email from lemarche.utils.urls import get_object_admin_url, get_object_share_url @@ -28,7 +30,7 @@ def get_api_client(): return sib_api_v3_sdk.ApiClient(config) -def create_contact(user, list_id: int): +def create_contact(user, list_id: int, tender=None): """ Brevo docs - Python library: https://github.com/sendinblue/APIv3-python-library/blob/master/docs/CreateContact.md @@ -36,18 +38,43 @@ def create_contact(user, list_id: int): """ api_client = get_api_client() api_instance = sib_api_v3_sdk.ContactsApi(api_client) + + attributes = { + "NOM": sanitize_to_send_by_email(user.last_name.capitalize()), + "PRENOM": sanitize_to_send_by_email(user.first_name.capitalize()), + "DATE_INSCRIPTION": user.created_at, + "TYPE_ORGANISATION": user.buyer_kind_detail, + "NOM_ENTREPRISE": sanitize_to_send_by_email(user.company_name.capitalize()), + "SMS": sanitize_to_send_by_email(user.phone_display), + "MONTANT_BESOIN_ACHETEUR": None, + "TYPE_BESOIN_ACHETEUR": None, + "TYPE_VERTICALE_ACHETEUR": None, + # WHATSAPP, TYPE_ORGANISATION, LIEN_FICHE_COMMERCIALE, TAUX_DE_COMPLETION + } + + try: + tender = user.tenders.get(id=tender.id) + first_sector = tender.sectors.first() + attributes["MONTANT_BESOIN_ACHETEUR"] = tender.amount_int + attributes["TYPE_BESOIN_ACHETEUR"] = tender.kind + + # Check if there is one sector whose tender source is TALLY + if tender.source == tender_constants.SOURCE_TALLY and first_sector: + attributes["TYPE_VERTICALE_ACHETEUR"] = first_sector.name + else: + attributes["TYPE_VERTICALE_ACHETEUR"] = None + + except ObjectDoesNotExist: + print("L'objet Tender demandé n'existe pas pour cet utilisateur.") + except AttributeError as e: + print(f"Erreur d'attribut : {e}") + except Exception as e: + print(f"Une erreur inattendue est survenue : {e}") + new_contact = sib_api_v3_sdk.CreateContact( email=user.email, list_ids=[list_id], - attributes={ - "NOM": sanitize_to_send_by_email(user.last_name.capitalize()), - "PRENOM": sanitize_to_send_by_email(user.first_name.capitalize()), - "DATE_INSCRIPTION": user.created_at, - "TYPE_ORGANISATION": user.buyer_kind_detail, - "NOM_ENTREPRISE": sanitize_to_send_by_email(user.company_name.capitalize()), - "SMS": sanitize_to_send_by_email(user.phone_display), - # WHATSAPP, TYPE_ORGANISATION, LIEN_FICHE_COMMERCIALE, TAUX_DE_COMPLETION - }, + attributes=attributes, ext_id=str(user.id), update_enabled=True, ) diff --git a/lemarche/utils/emails.py b/lemarche/utils/emails.py index 9c621da4f..262e8232d 100644 --- a/lemarche/utils/emails.py +++ b/lemarche/utils/emails.py @@ -54,7 +54,7 @@ def whitelist_recipient_list(recipient_list): return [email for email in recipient_list if (email and email.endswith("beta.gouv.fr"))] -def add_to_contact_list(user, type: str, source: str = user_constants.SOURCE_SIGNUP_FORM): +def add_to_contact_list(user, type: str, tender=None, source: str = user_constants.SOURCE_SIGNUP_FORM): """Add user to contactlist Args: @@ -65,7 +65,7 @@ def add_to_contact_list(user, type: str, source: str = user_constants.SOURCE_SIG if type == "signup": contact_list_id = api_mailjet.get_mailjet_cl_on_signup(user, source) if user.kind == user.KIND_BUYER: - api_brevo.create_contact(user=user, list_id=settings.BREVO_CL_SIGNUP_BUYER_ID) + api_brevo.create_contact(user=user, list_id=settings.BREVO_CL_SIGNUP_BUYER_ID, tender=tender) elif user.kind == user.KIND_SIAE: api_brevo.create_contact(user=user, list_id=settings.BREVO_CL_SIGNUP_SIAE_ID) elif type == "buyer_search": diff --git a/lemarche/www/pages/views.py b/lemarche/www/pages/views.py index 525f012ac..f1575188d 100644 --- a/lemarche/www/pages/views.py +++ b/lemarche/www/pages/views.py @@ -18,6 +18,7 @@ from lemarche.tenders.models import Tender, TenderStepsData from lemarche.users import constants as user_constants from lemarche.users.models import User +from lemarche.utils.emails import add_to_contact_list from lemarche.utils.tracker import track from lemarche.www.pages.forms import ( CompanyReferenceCalculatorForm, @@ -342,6 +343,7 @@ def csrf_failure(request, reason=""): # noqa C901 # create tender if is_adding: tender: Tender = create_tender_from_dict(tender_dict) + add_to_contact_list(user=user, type="signup", source=user_constants.SOURCE_TENDER_FORM, tender=tender) elif is_update: slug = request.path.split("/")[-1] tender: Tender = Tender.objects.get(slug=slug) diff --git a/lemarche/www/tenders/tests.py b/lemarche/www/tenders/tests.py index 3a41368bc..cd9c5b52d 100644 --- a/lemarche/www/tenders/tests.py +++ b/lemarche/www/tenders/tests.py @@ -1,6 +1,7 @@ import json from datetime import timedelta from unittest import mock +from unittest.mock import patch from django.conf import settings from django.contrib.gis.geos import Point @@ -21,6 +22,7 @@ from lemarche.tenders.enums import SurveyDoesNotExistQuestionChoices, SurveyScaleQuestionChoices from lemarche.tenders.factories import TenderFactory, TenderQuestionFactory from lemarche.tenders.models import Tender, TenderSiae, TenderStepsData +from lemarche.users import constants as user_constants from lemarche.users.factories import UserFactory from lemarche.users.models import User from lemarche.utils import constants @@ -30,6 +32,7 @@ class TenderCreateViewTest(TestCase): @classmethod def setUpTestData(cls): + cls.user = UserFactory() cls.user_siae = UserFactory(kind=User.KIND_SIAE) cls.user_buyer = UserFactory(kind=User.KIND_BUYER, company_name="Entreprise Buyer") cls.sectors = [SectorFactory().slug for _ in range(3)] @@ -101,6 +104,18 @@ def _check_every_step(self, tenders_step_data, final_redirect_page: str = revers tender_step_data.steps_data[-1]["tender_create_multi_step_view-current_step"], ) + @patch("lemarche.www.tenders.views.get_or_create_user") + def setup_mock_user_and_tender_creation(self, mock_get_user, user=None, title="Test Tender Form"): + """Helper method to setup mock user""" + user = user if user else self.user + mock_get_user.return_value = user + + tenders_step_data = self._generate_fake_data_form({"general-title": title}) + self._check_every_step(tenders_step_data, final_redirect_page=reverse("siae:search_results")) + tender = Tender.objects.get(title=title) + + return tender, user + def test_anyone_can_access_create_tender(self): # anonymous user url = reverse("tenders:create") @@ -249,6 +264,41 @@ def test_tender_wizard_form_questions_list(self): self.assertEqual(tender.questions.count(), len(initial_data_questions_list)) # count is 2 self.assertEqual(tender.questions_list()[0].get("text"), initial_data_questions_list[0].get("text")) + @patch("lemarche.www.tenders.views.add_to_contact_list") + def test_args_in_add_to_contact_list_call(self, mock_add_to_contact_list): + """Test arguments in `add_to_contact_list` call""" + tender, user = self.setup_mock_user_and_tender_creation() + + mock_add_to_contact_list.assert_called_once() + args, kwargs = mock_add_to_contact_list.call_args + + # Check arguments like user, type, and source + self.assertEqual(kwargs["user"], user) + self.assertEqual(kwargs["type"], "signup") + self.assertEqual(kwargs["source"], user_constants.SOURCE_TENDER_FORM) + # Verify that `tender` is an instance of Tender + self.assertIsInstance( + kwargs.get("tender"), Tender, "Expected an instance of Tender for the 'tender' argument." + ) + + @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CreateContact") + def test_create_contact_call_has_user_buyer_attributes(self, mock_create_contact): + """Test CreateContact call contains user buyer attributes""" + tender, user = self.setup_mock_user_and_tender_creation(user=self.user_buyer) + tender.save() + + mock_create_contact.assert_called_once() + args, kwargs = mock_create_contact.call_args + attributes = kwargs["attributes"] + + self.assertEqual(kwargs["email"], user.email) + self.assertIn(settings.BREVO_CL_SIGNUP_BUYER_ID, kwargs["list_ids"]) + self.assertEqual(attributes["MONTANT_BESOIN_ACHETEUR"], tender.amount_int) + self.assertEqual(attributes["TYPE_BESOIN_ACHETEUR"], tender.kind) + self.assertIsNone( + attributes["TYPE_VERTICALE_ACHETEUR"], "Expected TYPE_VERTICALE_ACHETEUR to be None for non-TALLY sources" + ) + class TenderMatchingTest(TestCase): @classmethod diff --git a/lemarche/www/tenders/utils.py b/lemarche/www/tenders/utils.py index fa77a7e3c..006684233 100644 --- a/lemarche/www/tenders/utils.py +++ b/lemarche/www/tenders/utils.py @@ -4,7 +4,6 @@ from lemarche.tenders.models import Tender, TenderQuestion from lemarche.users import constants as user_constants from lemarche.users.models import User -from lemarche.utils.emails import add_to_contact_list from lemarche.www.auth.tasks import send_new_user_password_reset_link @@ -79,7 +78,6 @@ def get_or_create_user_from_anonymous_content( ) if created and settings.BITOUBI_ENV == "prod": send_new_user_password_reset_link(user) - add_to_contact_list(user=user, type="signup", source=source) return user diff --git a/lemarche/www/tenders/views.py b/lemarche/www/tenders/views.py index 2121b4613..0aa63d0c7 100644 --- a/lemarche/www/tenders/views.py +++ b/lemarche/www/tenders/views.py @@ -18,6 +18,7 @@ from lemarche.users.models import User from lemarche.utils import constants, settings_context_processors from lemarche.utils.data import get_choice +from lemarche.utils.emails import add_to_contact_list from lemarche.utils.mixins import ( SesameSiaeMemberRequiredMixin, SesameTenderAuthorRequiredMixin, @@ -229,7 +230,6 @@ def done(self, _, form_dict, **kwargs): tender_dict = cleaned_data | {"author": user, "source": tender_constants.SOURCE_FORM} is_draft: bool = self.request.POST.get("is_draft", False) self.save_instance_tender(tender_dict=tender_dict, form_dict=form_dict, is_draft=is_draft) - # remove steps data uuid = self.request.session.get("tender_steps_data_uuid", None) if uuid: @@ -254,6 +254,8 @@ def done(self, _, form_dict, **kwargs): message=self.get_success_message(cleaned_data, self.instance, is_draft=is_draft), extra_tags="modal_message_bizdev", ) + tender = self.instance + add_to_contact_list(user=user, type="signup", source=user_constants.SOURCE_TENDER_FORM, tender=tender) return redirect(self.get_success_url()) def get_success_url(self):