Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Ajout d'attributs dans Brevo pour les structures #1513

Merged
merged 6 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions lemarche/crm/management/commands/crm_brevo_sync_companies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from lemarche.utils.commands import BaseCommand


ten_days_ago = timezone.now() - timedelta(days=10)
two_weeks_ago = timezone.now() - timedelta(weeks=2)


class Command(BaseCommand):
Expand All @@ -34,11 +34,29 @@ def handle(self, recently_updated: bool, **options):
self.stdout.write(f"Sync Siae > Brevo: we have {Siae.objects.count()} siaes")
# Update only the recently updated siaes
if recently_updated:
siaes_qs = siaes_qs.filter(updated_at__gte=ten_days_ago)
siaes_qs = siaes_qs.filter(updated_at__gte=two_weeks_ago)
self.stdout.write(f"Sync Siae > Brevo: {siaes_qs.count()} recently updated")

# Step 2: loop on the siaes
# Step 2: Add the 90-day limited annotations
siaes_qs = siaes_qs.with_tender_stats(since_days=90)

# Step 3: loop on the siaes
for index, siae in enumerate(siaes_qs):
new_extra_data = {
"completion_rate": siae.completion_rate if siae.completion_rate is not None else 0,
"tender_received": siae.tender_email_send_count_annotated,
"tender_interest": siae.tender_detail_contact_click_count_annotated,
}

# extra_data update if needed
if siae.extra_data != new_extra_data:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if siae.extra_data != new_extra_data:
if siae.extra_data.get('brevo_company_data') != new_extra_data:

siae.extra_data.update(
{
"brevo_company_data": new_extra_data,
}
)
siae.save(update_fields=["extra_data"])

api_brevo.create_or_update_company(siae)
if (index % 10) == 0: # avoid API rate-limiting
time.sleep(1)
Expand Down
219 changes: 219 additions & 0 deletions lemarche/crm/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
from datetime import timedelta
from unittest.mock import patch

from django.core.management import call_command
from django.test import TestCase
from django.utils import timezone

from lemarche.siaes.factories import SiaeFactory
from lemarche.siaes.models import Siae
from lemarche.tenders.factories import TenderFactory
from lemarche.tenders.models import TenderSiae
from lemarche.users.factories import UserFactory
from lemarche.users.models import User


now = timezone.now()
date_tomorrow = now + timedelta(days=1)
old_date = timezone.now() - timedelta(days=91)
recent_date = now - timedelta(days=10)


class CrmBrevoSyncCompaniesCommandTest(TestCase):
@classmethod
def setUpTestData(cls):
"""Siae instances initialization"""
cls.user_siae = UserFactory(kind=User.KIND_SIAE)
cls.siae_with_name = SiaeFactory(name="Test Company 1")
cls.siae_with_user = SiaeFactory(users=[cls.user_siae])
cls.siae_with_brevo_id = SiaeFactory(
brevo_company_id="123456789",
completion_rate=50,
)

cls.tender_1 = TenderFactory(deadline_date=date_tomorrow)
cls.tender_2 = TenderFactory(deadline_date=date_tomorrow)

TenderSiae.objects.create(
tender=cls.tender_1,
siae=cls.siae_with_user,
detail_contact_click_date=recent_date,
)

TenderSiae.objects.create(
tender=cls.tender_1,
siae=cls.siae_with_brevo_id,
email_send_date=recent_date,
detail_contact_click_date=old_date,
)

cls.siae_with_user_stats = Siae.objects.with_tender_stats().filter(id=cls.siae_with_user.id).first()
cls.siae_with_brevo_id_all_stats = (
Siae.objects.with_tender_stats().filter(id=cls.siae_with_brevo_id.id).first()
)
cls.siae_with_brevo_id_recent_stats = (
Siae.objects.with_tender_stats(since_days=90).filter(id=cls.siae_with_brevo_id.id).first()
)

# siae_with_brevo_id.extra_data initialization
cls.siae_with_brevo_id.extra_data = {
"brevo_company_data": {
"completion_rate": cls.siae_with_brevo_id.completion_rate
if cls.siae_with_brevo_id.completion_rate is not None
else 0,
"tender_received": cls.siae_with_brevo_id_recent_stats.tender_email_send_count_annotated,
"tender_interest": cls.siae_with_brevo_id_recent_stats.tender_detail_contact_click_count_annotated,
}
}
cls.siae_with_brevo_id.save()
cls.initial_extra_data = cls.siae_with_brevo_id.extra_data.copy()

def test_annotated_fields_set_up(self):
"""Test annotated fields are correctly set up"""
self.assertEqual(
self.siae_with_user_stats.tender_email_send_count_annotated,
0,
"Le nombre total de besoins reçus devrait être 0",
)
self.assertEqual(
self.siae_with_user_stats.tender_detail_contact_click_count_annotated,
1,
"Le nombre total de besoins intéressés devrait être 1",
)
self.assertEqual(
self.siae_with_brevo_id_all_stats.tender_email_send_count_annotated,
1,
"Le nombre total de besoins reçus devrait être 1",
)
self.assertEqual(
self.siae_with_brevo_id_all_stats.tender_detail_contact_click_count_annotated,
1,
"Le nombre total de besoins intéressés devrait être 1",
)
self.assertEqual(
self.siae_with_brevo_id_recent_stats.tender_email_send_count_annotated,
1,
"Le nombre de besoins reçus dans les 90 derniers jours devrait être 1",
)
self.assertEqual(
self.siae_with_brevo_id_recent_stats.tender_detail_contact_click_count_annotated,
0,
"Le nombre de besoins intéressés dans les 90 derniers jours devrait être 0",
)

@patch("lemarche.utils.apis.api_brevo.create_or_update_company")
def test_new_siaes_are_synced_in_brevo(self, mock_create_or_update_company):
"""Test new siaes are synced in brevo"""
call_command("crm_brevo_sync_companies")

self.assertEqual(mock_create_or_update_company.call_count, 3)

def test_siae_has_tender_stats(self):
self.assertIsNotNone(
self.siae_with_user_stats,
"Cette SIAE devrait avoir des statistiques sur les besoins.",
)
self.assertIsNotNone(
self.siae_with_brevo_id_all_stats,
"Cette SIAE devrait avoir des statistiques sur les besoins.",
)

def test_siae_extra_data_is_set_on_first_sync(self):
"""
- Test siae is updated if extra_data is changed.
- Test siae.extra_data update does not erase existing data.
"""
initial_extra_data = self.siae_with_user.extra_data.copy()
initial_extra_data["test_data"] = "test value"

self.siae_with_user.extra_data = initial_extra_data
self.siae_with_user.save(update_fields=["extra_data"])

call_command("crm_brevo_sync_companies", recently_updated=True)

self.siae_with_user.refresh_from_db()

expected_extra_data = {
"brevo_company_data": {
"completion_rate": self.siae_with_user.completion_rate
if self.siae_with_user.completion_rate is not None
else 0,
"tender_received": self.siae_with_user_stats.tender_email_send_count_annotated,
"tender_interest": self.siae_with_user_stats.tender_detail_contact_click_count_annotated,
},
"test_data": "test value",
}

self.assertNotEqual(initial_extra_data, expected_extra_data, "siae.extra_data aurait dû être mis à jour.")
self.assertEqual(
self.siae_with_user.extra_data, expected_extra_data, "siae.extra_data n'est pas conforme aux attentes."
)

def test_siae_extra_data_is_not_updated_if_no_changes(self):
"""Test siae.extra_data is not updated if no changes."""
call_command("crm_brevo_sync_companies", recently_updated=True)

self.siae_with_brevo_id.refresh_from_db()
self.assertEqual(
self.initial_extra_data,
self.siae_with_brevo_id.extra_data,
"siae.extra_data a été mis à jour alors qu'il n'y avait pas de changement.",
)

def test_fields_update_within_90_days_and_ignore_older_changes(self):
"""Test fields update within 90 days and ignore older changes."""
TenderSiae.objects.create(
tender=self.tender_2,
siae=self.siae_with_brevo_id,
email_send_date=now,
detail_contact_click_date=now,
)

call_command("crm_brevo_sync_companies", recently_updated=True)

self.siae_with_brevo_id_all_stats = (
Siae.objects.with_tender_stats().filter(id=self.siae_with_brevo_id.id).first()
)
self.siae_with_brevo_id_recent_stats = (
Siae.objects.with_tender_stats(since_days=90).filter(id=self.siae_with_brevo_id.id).first()
)

# Tender stats without date limit
self.assertEqual(
self.siae_with_brevo_id_all_stats.tender_email_send_count_annotated,
2,
"Le nombre total des besoins reçus devrait être 2",
)
self.assertEqual(
self.siae_with_brevo_id_all_stats.tender_detail_contact_click_count_annotated,
2,
"Le nombre de bsoins interessés devrait être 2",
)

# Tender stats with date limit
self.assertEqual(
self.siae_with_brevo_id_recent_stats.tender_email_send_count_annotated,
2,
"Le nombre de besoins reçus dans les 90 jours devraient être 2",
)
self.assertEqual(
self.siae_with_brevo_id_recent_stats.tender_detail_contact_click_count_annotated,
1,
"Les nombre de bsoins interessés dans les 90 jours devraient être 1",
)

expected_extra_data = {
"brevo_company_data": {
"completion_rate": self.siae_with_brevo_id.completion_rate
if self.siae_with_brevo_id.completion_rate is not None
else 0,
"tender_received": self.siae_with_brevo_id_recent_stats.tender_email_send_count_annotated,
"tender_interest": self.siae_with_brevo_id_recent_stats.tender_detail_contact_click_count_annotated,
}
}

self.assertNotEqual(
self.initial_extra_data,
expected_extra_data,
"Les valeurs récentes dans extra_data devraient être mises à jour en fonction du filtre de 90 jours.",
)
60 changes: 29 additions & 31 deletions lemarche/siaes/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import timedelta
from uuid import uuid4

from django.conf import settings
Expand Down Expand Up @@ -78,6 +79,24 @@ def get_city_filter(perimeter, with_country=False):
return filters


def count_field(field_name, date_limit):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jolie

"""
Helper method to construct a conditional count annotation.
"""
condition = (
Q(**{f"tendersiae__{field_name}__gte": date_limit})
if date_limit
else Q(**{f"tendersiae__{field_name}__isnull": False})
)
return Sum(
Case(
When(condition, then=1),
default=0,
output_field=IntegerField(),
)
)


class SiaeGroupQuerySet(models.QuerySet):
def with_siae_stats(self):
return self.annotate(siae_count_annotated=Count("siaes", distinct=True))
Expand Down Expand Up @@ -425,41 +444,20 @@ def filter_with_tender_tendersiae_status(self, tender, tendersiae_status=None):

return qs.distinct()

def with_tender_stats(self):
def with_tender_stats(self, since_days=None):
"""
Enrich each Siae with stats on their linked Tender
Enrich each Siae with stats on their linked Tender.
Optionally, limit the stats to the last `since_days` days.
"""
date_limit = timezone.now() - timedelta(days=since_days) if since_days else None

return self.annotate(
tender_count_annotated=Count("tenders", distinct=True),
tender_email_send_count_annotated=Sum(
Case(When(tendersiae__email_send_date__isnull=False, then=1), default=0, output_field=IntegerField())
),
tender_email_link_click_count_annotated=Sum(
Case(
When(tendersiae__email_link_click_date__isnull=False, then=1),
default=0,
output_field=IntegerField(),
)
),
tender_detail_display_count_annotated=Sum(
Case(
When(tendersiae__detail_display_date__isnull=False, then=1), default=0, output_field=IntegerField()
)
),
tender_detail_contact_click_count_annotated=Sum(
Case(
When(tendersiae__detail_contact_click_date__isnull=False, then=1),
default=0,
output_field=IntegerField(),
)
),
tender_detail_not_interested_count_annotated=Sum(
Case(
When(tendersiae__detail_not_interested_click_date__isnull=False, then=1),
default=0,
output_field=IntegerField(),
)
),
tender_email_send_count_annotated=count_field("email_send_date", date_limit),
tender_email_link_click_count_annotated=count_field("email_link_click_date", date_limit),
tender_detail_display_count_annotated=count_field("detail_display_date", date_limit),
tender_detail_contact_click_count_annotated=count_field("detail_contact_click_date", date_limit),
tender_detail_not_interested_count_annotated=count_field("detail_not_interested_click_date", date_limit),
)

def with_brand_or_name(self, with_order_by=False):
Expand Down
3 changes: 3 additions & 0 deletions lemarche/utils/apis/api_brevo.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ def create_or_update_company(siae):
"geo_range": siae.geo_range,
"app_url": get_object_share_url(siae),
"app_admin_url": get_object_admin_url(siae),
"taux_de_completion": siae.extra_data.get("brevo_company_data", {}).get("completion_rate"),
"nombre_de_besoins_recus": siae.extra_data.get("brevo_company_data", {}).get("tender_received"),
"nombre_de_besoins_interesses": siae.extra_data.get("brevo_company_data", {}).get("tender_interest"),
},
)

Expand Down
Loading