diff --git a/lemarche/api/tenders/tests.py b/lemarche/api/tenders/tests.py index b5bb806f9..93d318444 100644 --- a/lemarche/api/tenders/tests.py +++ b/lemarche/api/tenders/tests.py @@ -42,6 +42,7 @@ } +@patch("lemarche.utils.apis.api_brevo.create_deal") class TenderCreateApiTest(TestCase): @classmethod def setUpTestData(cls): diff --git a/lemarche/companies/models.py b/lemarche/companies/models.py index 449614092..0880dc7f2 100644 --- a/lemarche/companies/models.py +++ b/lemarche/companies/models.py @@ -86,6 +86,6 @@ def save(self, *args, **kwargs): @receiver(post_save, sender=Company) def create_company_in_brevo(sender, instance, created, **kwargs): if created: - from lemarche.utils.apis.api_brevo import create_company + from lemarche.utils.apis.api_brevo import create_brevo_company_from_company - create_company(instance) + create_brevo_company_from_company(instance) diff --git a/lemarche/companies/tests/tests.py b/lemarche/companies/tests/tests.py index a334b8194..8f83a706c 100644 --- a/lemarche/companies/tests/tests.py +++ b/lemarche/companies/tests/tests.py @@ -1,4 +1,5 @@ from django.test import TestCase +from unittest.mock import patch from lemarche.companies.factories import CompanyFactory from lemarche.companies.models import Company @@ -20,7 +21,10 @@ def test_str(self): class CompanyQuerysetTest(TestCase): @classmethod - def setUpTestData(cls): + @patch("lemarche.utils.apis.api_brevo.create_deal") + def setUpTestData(cls, mock_create_deal): + mock_create_deal.return_value = None + cls.user_1 = UserFactory() cls.user_2 = UserFactory() TenderFactory(author=cls.user_1) diff --git a/lemarche/crm/management/commands/crm_brevo_sync_companies.py b/lemarche/crm/management/commands/crm_brevo_sync_companies.py index 7e31f912c..7cee2b475 100644 --- a/lemarche/crm/management/commands/crm_brevo_sync_companies.py +++ b/lemarche/crm/management/commands/crm_brevo_sync_companies.py @@ -39,7 +39,7 @@ def handle(self, recently_updated: bool, **options): # Step 2: loop on the siaes for index, siae in enumerate(siaes_qs): - api_brevo.create_company(siae) + api_brevo.create_brevo_company_from_siae(siae) if (index % 10) == 0: # avoid API rate-limiting time.sleep(1) if (index % 500) == 0: diff --git a/lemarche/crm/tests.py b/lemarche/crm/tests.py index d0a67f0b8..cc9703b00 100644 --- a/lemarche/crm/tests.py +++ b/lemarche/crm/tests.py @@ -20,6 +20,9 @@ @override_settings(BITOUBI_ENV="production", BREVO_API_KEY="fake-key") +@patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") +@patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.Configuration") +@patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.ApiClient") class CrmBrevoSyncCompaniesCommandTest(TestCase): @classmethod def setUpTestData(cls): @@ -55,66 +58,39 @@ def setUpTestData(cls): Siae.objects.with_tender_stats(since_days=90).filter(id=cls.siae_with_brevo_id.id).first() ) - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.Configuration") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.ApiClient") def test_new_siaes_are_synced_in_brevo(self, mock_api_client, mock_configuration, mock_companies_api): """Test new siaes are synced in brevo""" - mock_config = MagicMock() - mock_configuration.return_value = mock_config - - mock_client = MagicMock() - mock_api_client.return_value = mock_client - - mock_api = MagicMock() - mock_companies_api.return_value = mock_api + mock_configuration.return_value = MagicMock() + mock_api_client.return_value = MagicMock() + mock_companies_api.return_value = MagicMock() mock_response = MagicMock() mock_response.id = 12345 - mock_api.companies_post.return_value = mock_response + + mock_companies_api.companies_post.return_value = mock_response expected_count = Siae.objects.filter(brevo_company_id__isnull=True).count() # Run the command call_command("crm_brevo_sync_companies") - actual_count = mock_api.companies_post.call_count + actual_count = mock_companies_api.companies_post.call_count self.assertEqual(actual_count, expected_count, f"Expected {expected_count} API calls, got {actual_count}") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.Configuration") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.ApiClient") def test_siae_has_tender_stats(self, mock_api_client, mock_configuration, mock_companies_api): - # Setup mock API chain similar to above - mock_config = MagicMock() - mock_configuration.return_value = mock_config - mock_client = MagicMock() - mock_api_client.return_value = mock_client - mock_api = MagicMock() - mock_companies_api.return_value = mock_api - mock_response = MagicMock() - mock_response.id = 12345 - mock_api.companies_post.return_value = mock_response + mock_configuration.return_value = MagicMock() + mock_api_client.return_value = MagicMock() + mock_companies_api.return_value = MagicMock() self.assertIsNotNone(self.siae_with_user_stats) self.assertIsNotNone(self.siae_with_brevo_id_all_stats) - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.Configuration") - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.ApiClient") def test_siae_extra_data_is_preserved(self, mock_api_client, mock_configuration, mock_companies_api): """Test that creating a company in Brevo preserves existing extra_data""" - # Setup mock API chain - mock_config = MagicMock() - mock_configuration.return_value = mock_config - mock_client = MagicMock() - mock_api_client.return_value = mock_client - mock_api = MagicMock() - mock_companies_api.return_value = mock_api - mock_response = MagicMock() - mock_response.id = 12345 - mock_api.companies_post.return_value = mock_response + mock_configuration.return_value = MagicMock() + mock_api_client.return_value = MagicMock() + mock_companies_api.return_value = MagicMock() # Set initial extra_data and ensure other Siaes have brevo_company_id initial_extra_data = {"test_data": "test value"} @@ -123,13 +99,16 @@ def test_siae_extra_data_is_preserved(self, mock_api_client, mock_configuration, self.siae_with_name.brevo_company_id = "999999" self.siae_with_name.save() - mock_api.companies_post.reset_mock() + mock_companies_api.return_value = MagicMock() + mock_response = MagicMock() + mock_response.id = 12345 + mock_companies_api.companies_post.return_value = mock_response call_command("crm_brevo_sync_companies", recently_updated=True) self.siae_with_user.refresh_from_db() - mock_api.companies_post.assert_called_once() + mock_companies_api.companies_post.assert_called_once() self.assertEqual(self.siae_with_user.brevo_company_id, "12345") diff --git a/lemarche/siaes/models.py b/lemarche/siaes/models.py index 4d360be28..8dd62dd18 100644 --- a/lemarche/siaes/models.py +++ b/lemarche/siaes/models.py @@ -1283,9 +1283,9 @@ def siae_post_save(sender, instance, created, **kwargs): # Handle Brevo company creation if created: - from lemarche.utils.apis.api_brevo import create_company + from lemarche.utils.apis.api_brevo import create_brevo_company_from_siae - create_company(instance) + create_brevo_company_from_siae(instance) @receiver(m2m_changed, sender=Siae.users.through) diff --git a/lemarche/siaes/tests/test_models.py b/lemarche/siaes/tests/test_models.py index cb72739fd..a60d15fa6 100644 --- a/lemarche/siaes/tests/test_models.py +++ b/lemarche/siaes/tests/test_models.py @@ -681,48 +681,20 @@ def test_last_activity_is_updated_at(self): class SiaeSignalTest(TestCase): - @patch("lemarche.utils.apis.api_brevo.create_company") - def test_create_siae_in_brevo_signal(self, mock_create_company): - """Test that creating a new SIAE triggers the Brevo sync""" - # Mock the environment check to always return True - with patch("lemarche.utils.apis.api_brevo.settings.BITOUBI_ENV", "production"): - # Create a new SIAE - siae = SiaeFactory() - - # Verify create_company was called once with the SIAE instance - mock_create_company.assert_called_once_with(siae) + @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") + def test_siae_creation_with_brevo_integration(self, mock_companies_api): + mock_response = MagicMock() + mock_response.id = 12345 + mock_companies_api_instance = mock_companies_api.return_value + mock_companies_api_instance.companies_post.return_value = mock_response - # Create another SIAE - siae2 = SiaeFactory() - self.assertEqual(mock_create_company.call_count, 2) - mock_create_company.assert_called_with(siae2) + siae = SiaeFactory(name="Test SIAE", website="https://example.com") - # Update existing SIAE - siae.name = "Updated Name" - siae.save() + self.assertTrue(mock_companies_api_instance.companies_post.called) + args, kwargs = mock_companies_api_instance.companies_post.call_args + body_obj = args[0] - # Call count should still be 2 since we only sync on creation - self.assertEqual(mock_create_company.call_count, 2) + self.assertEqual(body_obj.name, "Test SIAE") + self.assertEqual(body_obj.attributes, {"domain": "https://example.com", "app_id": siae.id, "siae": True}) - @patch("lemarche.utils.apis.api_brevo.sib_api_v3_sdk.CompaniesApi") - def test_siae_attributes_sent_to_brevo(self, mock_companies_api): - """Test the attributes sent to Brevo when creating a SIAE""" - # Mock the environment check to always return True - with patch("lemarche.utils.apis.api_brevo.settings.BITOUBI_ENV", "production"): - # Setup the mock response - mock_response = MagicMock() - mock_response.id = 12345 - mock_instance = mock_companies_api.return_value - mock_instance.companies_post.return_value = mock_response - - # Create a SIAE - siae = SiaeFactory(name="Test SIAE", website="https://example.com") - - # Get the Body instance that was passed to companies_post - args, kwargs = mock_instance.companies_post.call_args - body_obj = args[0] - - self.assertEqual(body_obj.name, "Test SIAE") - self.assertEqual(body_obj.attributes, {"domain": "https://example.com", "app_id": siae.id, "siae": True}) - - self.assertEqual(siae.brevo_company_id, 12345) + self.assertEqual(siae.brevo_company_id, 12345) diff --git a/lemarche/tenders/models.py b/lemarche/tenders/models.py index 4d2553879..b324e37d7 100644 --- a/lemarche/tenders/models.py +++ b/lemarche/tenders/models.py @@ -1289,8 +1289,4 @@ def tender_post_save(sender, instance, created, **kwargs): if created: from lemarche.utils.apis.api_brevo import create_deal - try: - create_deal(tender=instance) - except Exception as e: - # Log the error but don't prevent tender creation - logger.error(f"Error creating Brevo deal for tender {instance.id}: {e}") + create_deal(tender=instance) diff --git a/lemarche/tenders/tests/test_models.py b/lemarche/tenders/tests/test_models.py index 02f05b533..ccd323eec 100644 --- a/lemarche/tenders/tests/test_models.py +++ b/lemarche/tenders/tests/test_models.py @@ -1184,36 +1184,33 @@ def test_set_validated_handles_brevo_error(self, mock_create_deal): self.assertEqual(tender.logs[0]["action"], "validate") -@patch("lemarche.utils.apis.api_brevo.create_deal") -def test_set_validated_only_works_on_draft(self, mock_create_deal): - """Test that set_validated only works on draft tenders""" - tender = TenderFactory(status=tender_constants.STATUS_VALIDATED) - - tender.set_validated() +class BrevoTestCase(TestCase): + @patch("lemarche.utils.apis.api_brevo.create_deal") + def test_create_deal_on_tender_creation(self, mock_create_deal): + """Test that creating a new tender creates a Brevo deal""" + tender = TenderFactory() - # Verify no API call was made - mock_create_deal.assert_not_called() + # Verify create_deal was called once with the tender instance + mock_create_deal.assert_called_once_with(tender=tender) - # Verify no changes were made - self.assertEqual(tender.status, tender_constants.STATUS_VALIDATED) + # Create another tender + tender2 = TenderFactory() + self.assertEqual(mock_create_deal.call_count, 2) + mock_create_deal.assert_called_with(tender=tender2) + # Update existing tender + tender.title = "Updated Title" + tender.save() -@patch("lemarche.utils.apis.api_brevo.create_deal") -def test_create_deal_on_tender_creation(self, mock_create_deal): - """Test that creating a new tender creates a Brevo deal""" - tender = TenderFactory() - - # Verify create_deal was called once with the tender instance - mock_create_deal.assert_called_once_with(tender=tender) + # Call count should still be 2 since we only sync on creation + self.assertEqual(mock_create_deal.call_count, 2) - # Create another tender - tender2 = TenderFactory() - self.assertEqual(mock_create_deal.call_count, 2) - mock_create_deal.assert_called_with(tender=tender2) + @patch("lemarche.utils.apis.api_brevo.create_deal") + def test_set_validated_only_works_on_draft(self, mock_create_deal): + """Test that set_validated only works on draft tenders""" + tender = TenderFactory(status=tender_constants.STATUS_VALIDATED) - # Update existing tender - tender.title = "Updated Title" - tender.save() + tender.set_validated() - # Call count should still be 2 since we only sync on creation - self.assertEqual(mock_create_deal.call_count, 2) + # Verify no changes were made + self.assertEqual(tender.status, tender_constants.STATUS_VALIDATED) diff --git a/lemarche/users/tests.py b/lemarche/users/tests.py index 8be475ef3..467ae9c0c 100644 --- a/lemarche/users/tests.py +++ b/lemarche/users/tests.py @@ -57,48 +57,52 @@ def test_kind_detail_display(self): ) +@patch("lemarche.utils.apis.api_brevo.create_deal") +@patch("lemarche.utils.apis.api_brevo.create_contact") +@patch("lemarche.utils.apis.api_brevo.create_company") +@patch("lemarche.utils.apis.api_brevo.send_transactional_email_with_template") class UserModelQuerysetTest(TestCase): @classmethod def setUpTestData(cls): cls.user = UserFactory() - def test_is_admin_bizdev(self): + def test_is_admin_bizdev(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): UserFactory(kind=user_constants.KIND_ADMIN, is_staff=True) UserFactory(kind=user_constants.KIND_ADMIN, is_staff=True, position="BizDev") UserFactory(kind=user_constants.KIND_ADMIN, is_staff=True, position="Bizdev") self.assertEqual(User.objects.count(), 1 + 3) self.assertEqual(User.objects.is_admin_bizdev().count(), 2) - def test_has_company(self): + def test_has_company(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() CompanyFactory(users=[user_2]) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.has_company().count(), 1) - def test_has_siae(self): + def test_has_siae(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() SiaeFactory(users=[user_2]) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.has_siae().count(), 1) - def test_has_tender(self): + def test_has_tender(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() TenderFactory(author=user_2) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.has_tender().count(), 1) - def test_has_favorite_list(self): + def test_has_favorite_list(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() FavoriteListFactory(user=user_2) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.has_favorite_list().count(), 1) - def test_has_api_key(self): + def test_has_api_key(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): UserFactory(api_key="coucou") self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.has_api_key().count(), 1) - def test_has_email_domain(self): + def test_has_email_domain(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): UserFactory(email="test@ain.fr") UserFactory(email="test@plateau-urbain.fr") self.assertEqual(User.objects.count(), 1 + 2) @@ -106,21 +110,21 @@ def test_has_email_domain(self): with self.subTest(email_domain=EMAIL_DOMAIN): self.assertEqual(User.objects.has_email_domain(email_domain=EMAIL_DOMAIN).count(), 1) - def test_with_siae_stats(self): + def test_with_siae_stats(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() SiaeFactory(users=[user_2]) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.with_siae_stats().filter(id=self.user.id).first().siae_count_annotated, 0) self.assertEqual(User.objects.with_siae_stats().filter(id=user_2.id).first().siae_count_annotated, 1) - def test_with_tender_stats(self): + def test_with_tender_stats(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory() TenderFactory(author=user_2) self.assertEqual(User.objects.count(), 1 + 1) self.assertEqual(User.objects.with_tender_stats().filter(id=self.user.id).first().tender_count_annotated, 0) self.assertEqual(User.objects.with_tender_stats().filter(id=user_2.id).first().tender_count_annotated, 1) - def test_chain_querysets(self): + def test_chain_querysets(self, mock_send_email, mock_create_company, mock_create_contact, mock_create_deal): user_2 = UserFactory(api_key="chain") siae = SiaeFactory() siae.users.add(user_2) diff --git a/lemarche/utils/apis/api_brevo.py b/lemarche/utils/apis/api_brevo.py index acef79a1f..b957261fa 100644 --- a/lemarche/utils/apis/api_brevo.py +++ b/lemarche/utils/apis/api_brevo.py @@ -1,6 +1,6 @@ import logging import time -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional import sib_api_v3_sdk from django.conf import settings @@ -11,15 +11,6 @@ from lemarche.utils.constants import EMAIL_SUBJECT_PREFIX from lemarche.utils.data import sanitize_to_send_by_email -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from lemarche.users.models import User - from lemarche.tenders.models import Tender - from lemarche.siaes.models import Siae - from lemarche.companies.models import Company - - logger = logging.getLogger(__name__) ENV_NOT_ALLOWED = ("dev", "test") @@ -36,7 +27,7 @@ def get_api_client(): return sib_api_v3_sdk.ApiClient(config) -def create_contact(user: "User", list_id: int, tender: Optional["Tender"] = None) -> None: +def create_contact(user, list_id: int, tender=None) -> None: """ Brevo docs - Python library: https://github.com/sendinblue/APIv3-python-library/blob/master/docs/CreateContact.md @@ -107,14 +98,36 @@ def update_contact_email_blacklisted(user_identifier: str, email_blacklisted: bo logger.error(f"Exception when calling Brevo->ContactsApi->update_contact to update email_blacklisted: {e}") -def create_company(company: Union["Siae", "Company"]) -> None: +def create_brevo_company_from_company(company) -> None: + """ + Creates a Brevo company from a Company instance. + + Brevo docs + - Python library: https://github.com/sendinblue/APIv3-python-library/blob/master/docs/CompaniesApi.md + - API: https://developers.brevo.com/reference/post_companies + """ + create_company(company) + + +def create_brevo_company_from_siae(siae) -> None: + """ + Creates a Brevo company from a Siae instance. + + Brevo docs + - Python library: https://github.com/sendinblue/APIv3-python-library/blob/master/docs/CompaniesApi.md + - API: https://developers.brevo.com/reference/post_companies + """ + create_company(siae) + + +def create_company(company_or_siae) -> None: """ Brevo docs - Python library: https://github.com/sendinblue/APIv3-python-library/blob/master/docs/CompaniesApi.md - API: https://developers.brevo.com/reference/post_companies Args: - company: Union["Siae", "Company"] instance to create in Brevo + company_or_siae: instance to create in Brevo """ api_client = get_api_client() api_instance = sib_api_v3_sdk.CompaniesApi(api_client) @@ -122,28 +135,28 @@ def create_company(company: Union["Siae", "Company"]) -> None: # Determine if this is a SIAE from lemarche.siaes.models import Siae - is_siae = isinstance(company, Siae) + is_siae = isinstance(company_or_siae, Siae) company_data = sib_api_v3_sdk.Body( - name=company.name, + name=company_or_siae.name, attributes={ - "domain": company.website if hasattr(company, "website") else "", - "app_id": company.id, + "domain": company_or_siae.website if hasattr(company_or_siae, "website") else "", + "app_id": company_or_siae.id, "siae": is_siae, }, ) - if not company.brevo_company_id: + if not company_or_siae.brevo_company_id: try: api_response = api_instance.companies_post(company_data) logger.info(f"Success Brevo->CompaniesApi->create_company (create): {api_response}") - company.brevo_company_id = api_response.id - company.save(update_fields=["brevo_company_id"]) + company_or_siae.brevo_company_id = api_response.id + company_or_siae.save(update_fields=["brevo_company_id"]) except ApiException as e: logger.error(f"Exception when calling Brevo->CompaniesApi->create_company (create): {e}") -def create_deal(tender: "Tender") -> None: +def create_deal(tender) -> None: """ Creates a new deal in Brevo CRM from a tender and logs the result. @@ -184,7 +197,7 @@ def create_deal(tender: "Tender") -> None: raise ApiException(e) -def link_company_with_contact_list(siae: "Siae", contact_list: list = None): +def link_company_with_contact_list(siae, contact_list=None): """ Links a Brevo company to a list of contacts. If no contact list is provided, it defaults to linking the company with the siae's users.