From 3775153f6f480dfdc7c20eb3b737b135213a650c Mon Sep 17 00:00:00 2001 From: SebastienReuiller Date: Wed, 24 Apr 2024 09:36:34 +0200 Subject: [PATCH] =?UTF-8?q?feat(D=C3=A9p=C3=B4t=20de=20besoins):=20envoi?= =?UTF-8?q?=20d'un=20email=20=C3=A0=20l'auteur=20d'un=20d=C3=A9p=C3=B4t=20?= =?UTF-8?q?de=20besoin=20avec=205=20ESI=20(#1167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clevercloud/cron.json | 1 + ..._send_author_list_of_super_siaes_emails.sh | 22 +++ config/settings/base.py | 2 + .../send_author_list_of_super_siaes_emails.py | 87 +++++++++ lemarche/tenders/tests/__init__.py | 0 lemarche/tenders/tests/test_commands.py | 175 ++++++++++++++++++ .../{tests.py => tests/test_models.py} | 0 lemarche/utils/apis/api_brevo.py | 2 - lemarche/www/tenders/tasks.py | 46 ++++- 9 files changed, 332 insertions(+), 3 deletions(-) create mode 100755 clevercloud/tenders_send_author_list_of_super_siaes_emails.sh create mode 100644 lemarche/tenders/management/commands/send_author_list_of_super_siaes_emails.py create mode 100644 lemarche/tenders/tests/__init__.py create mode 100644 lemarche/tenders/tests/test_commands.py rename lemarche/tenders/{tests.py => tests/test_models.py} (100%) diff --git a/clevercloud/cron.json b/clevercloud/cron.json index f3a358146..07504baf1 100644 --- a/clevercloud/cron.json +++ b/clevercloud/cron.json @@ -14,6 +14,7 @@ "50 7 * * 1 $ROOT/clevercloud/companies_update_users_and_count_fields.sh", "55 7 * * 1 $ROOT/clevercloud/crm_brevo_sync.sh", "0 7 * * 2 $ROOT/clevercloud/siaes_send_completion_reminder_emails.sh", + "30 7 * * * $ROOT/clevercloud/tenders_send_author_list_of_super_siaes_emails.sh", "0 8 * * * $ROOT/clevercloud/siaes_send_user_request_reminder_emails.sh", "30 8 * * * $ROOT/clevercloud/tenders_send_author_transactioned_question_emails.sh", "35 8 * * * $ROOT/clevercloud/tenders_send_siae_transactioned_question_emails.sh", diff --git a/clevercloud/tenders_send_author_list_of_super_siaes_emails.sh b/clevercloud/tenders_send_author_list_of_super_siaes_emails.sh new file mode 100755 index 000000000..b9d3be2e5 --- /dev/null +++ b/clevercloud/tenders_send_author_list_of_super_siaes_emails.sh @@ -0,0 +1,22 @@ +#!/bin/bash -l + +# Find tender without insterested siae and send email to author with top 5 siaes + +# Do not run if this env var is not set: +if [[ -z "$CRON_TENDER_SEND_AUTHOR_LIST_OF_SUPER_SIAES_EMAILS_ENABLED" ]]; then + echo "CRON_TENDER_SEND_AUTHOR_LIST_OF_SUPER_SIAES_EMAILS_ENABLED not set. Exiting..." + exit 0 +fi + +# About clever cloud cronjobs: +# https://www.clever-cloud.com/doc/tools/crons/ + +if [[ "$INSTANCE_NUMBER" != "0" ]]; then + echo "Instance number is ${INSTANCE_NUMBER}. Stop here." + exit 0 +fi + +# $APP_HOME is set by default by clever cloud. +cd $APP_HOME + +django-admin send_author_list_of_super_siaes_emails diff --git a/config/settings/base.py b/config/settings/base.py index 71913e33d..e5fbbf1a1 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -399,6 +399,8 @@ INBOUND_EMAIL_IS_ACTIVATED = env.bool("INBOUND_EMAIL_IS_ACTIVATED", True) +BREVO_TENDERS_AUTHOR_SUPER_SIAES_TEMPLATE_ID = env.int("BREVO_TENDERS_AUTHOR_SUPER_SIAES_TEMPLATE_ID", 61) + # -- hubspot HUBSPOT_API_KEY = env.str("HUBSPOT_API_KEY", "set-it") HUBSPOT_IS_ACTIVATED = env.bool("HUBSPOT_IS_ACTIVATED", False) diff --git a/lemarche/tenders/management/commands/send_author_list_of_super_siaes_emails.py b/lemarche/tenders/management/commands/send_author_list_of_super_siaes_emails.py new file mode 100644 index 000000000..bfa03743b --- /dev/null +++ b/lemarche/tenders/management/commands/send_author_list_of_super_siaes_emails.py @@ -0,0 +1,87 @@ +from datetime import timedelta + +from django.utils import timezone + +from lemarche.tenders.models import Tender +from lemarche.utils.commands import BaseCommand +from lemarche.www.tenders.tasks import send_super_siaes_email_to_author + + +class Command(BaseCommand): + """ + Daily script to check tender without insterested siae, + if it was sent first time more than two days ago, send email to author with list of five siaes with super badge + When? J+2 (but doesn't run on weekends!) + + Usage: + python manage.py send_author_list_of_super_siaes_emails --dry-run + python manage.py send_author_list_of_super_siaes_emails --days-since-tender-sent-date 2 + python manage.py send_author_list_of_super_siaes_emails --tender-id 1 + python manage.py send_author_list_of_super_siaes_emails + """ + + def add_arguments(self, parser): + parser.add_argument( + "--days-since-tender-sent-date", + dest="days_since_tender_sent_date", + type=int, + default=1, + help="Laps de temps depuis la date du premier envoi (first_sent_at)", + ) + parser.add_argument( + "--tender-id", dest="tender_id", type=int, default=None, help="Restreindre sur un besoin donné" + ) + parser.add_argument("--dry-run", dest="dry_run", action="store_true", help="Dry run, no sends") + + def handle(self, dry_run=False, **options): + self.stdout_info("Script to send Super Siae to tender author...") + + current_weekday = timezone.now().weekday() + if current_weekday > 4: + self.stdout_error("Weekend... Stopping. Come back on Monday :)") + else: + self.stdout_messages_info("Step 1: Find Tender") + self.stdout_messages_info( + f"- where sent J-{options['days_since_tender_sent_date']} and no siae interested" + ) + + lt_days_ago = timezone.now() - timedelta(days=options["days_since_tender_sent_date"]) + gte_days_ago = timezone.now() - timedelta(days=options["days_since_tender_sent_date"] + 1) + # The script doesn't run on weekends + if current_weekday == 0: + gte_days_ago = gte_days_ago - timedelta(days=2) + + tender_list = Tender.objects.with_siae_stats().filter( + first_sent_at__gte=gte_days_ago, + first_sent_at__lt=lt_days_ago, + siae_detail_contact_click_count_annotated=0, + ) + if options["tender_id"]: + tender_list = tender_list.filter(id=options["tender_id"]) + self.stdout_info( + f"Found {tender_list.count()} Tender without siaes interested between {gte_days_ago} and {lt_days_ago}" + ) + + self.stdout_messages_info(f"Step 2: {'display top siaes' if dry_run else 'send emails'} for each tender") + for tender in tender_list: + top_siaes = tender.siaes.all().order_by_super_siaes()[:5] + self.stdout_info(f"{top_siaes.count()} top siaes finded for #{tender.id} {tender}") + if len(top_siaes) > 1: + if not dry_run: + send_super_siaes_email_to_author(tender, top_siaes) + self.stdout_success(f"Email sent to tender author {tender.author}") + else: + for siae in top_siaes: + self.stdout_messages_info( + [ + siae.name_display, + siae.get_kind_display(), + siae.contact_full_name, + siae.contact_phone, + siae.contact_email, + ] + ) + else: + self.stdout_error(f"Not enough siaes to send an email for #{tender.id}") + + self.stdout_messages_success("Done!") diff --git a/lemarche/tenders/tests/__init__.py b/lemarche/tenders/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lemarche/tenders/tests/test_commands.py b/lemarche/tenders/tests/test_commands.py new file mode 100644 index 000000000..86a4b2b46 --- /dev/null +++ b/lemarche/tenders/tests/test_commands.py @@ -0,0 +1,175 @@ +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase +from django.utils import timezone + +from lemarche.sectors.factories import SectorFactory +from lemarche.siaes import constants as siae_constants +from lemarche.siaes.factories import SiaeFactory +from lemarche.tenders.factories import TenderFactory +from lemarche.tenders.models import TenderSiae +from lemarche.users.factories import UserFactory +from lemarche.users.models import User + + +class TestSendAuthorListOfSuperSiaesEmails(TestCase): + @classmethod + def setUpTestData(cls): + cls.sector = SectorFactory() + + cls.siae1 = SiaeFactory( + is_active=True, + kind=siae_constants.KIND_AI, + presta_type=[siae_constants.PRESTA_PREST, siae_constants.PRESTA_BUILD], + geo_range=siae_constants.GEO_RANGE_COUNTRY, + ) + cls.siae1.sectors.add(cls.sector) + + cls.siae2 = SiaeFactory( + is_active=True, + kind=siae_constants.KIND_AI, + presta_type=[siae_constants.PRESTA_PREST, siae_constants.PRESTA_BUILD], + geo_range=siae_constants.GEO_RANGE_COUNTRY, + ) + cls.siae2.sectors.add(cls.sector) + + cls.siae3 = SiaeFactory( + is_active=True, + kind=siae_constants.KIND_AI, + presta_type=[siae_constants.PRESTA_PREST, siae_constants.PRESTA_BUILD], + geo_range=siae_constants.GEO_RANGE_COUNTRY, + ) + cls.siae3.sectors.add(cls.sector) + + cls.author = UserFactory(kind=User.KIND_BUYER) + cls.tender_before = TenderFactory( + presta_type=[siae_constants.PRESTA_BUILD], + sectors=[cls.sector], + is_country_area=True, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 7, 15)), + author=cls.author, + ) + cls.tender_before.set_siae_found_list() + cls.tender_before.refresh_from_db() + + cls.tender_during1 = TenderFactory( + presta_type=[siae_constants.PRESTA_BUILD], + sectors=[cls.sector], + is_country_area=True, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 8, 9)), + author=cls.author, + ) + cls.tender_during1.set_siae_found_list() + cls.tender_during1.refresh_from_db() + + cls.tender_during2 = TenderFactory( + presta_type=[siae_constants.PRESTA_BUILD], + sectors=[cls.sector], + is_country_area=True, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 8, 15)), + author=cls.author, + ) + cls.tender_during2.set_siae_found_list() + cls.tender_during2.refresh_from_db() + + # Tender with siaes interested + cls.tender_during3 = TenderFactory( + presta_type=[siae_constants.PRESTA_BUILD], + sectors=[cls.sector], + is_country_area=True, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 8, 16)), + author=cls.author, + ) + cls.tender_during3.set_siae_found_list() + cls.tender_during3.refresh_from_db() + # add a siae interested + TenderSiae.objects.create( + tender=cls.tender_during3, + siae=cls.siae1, + detail_display_date=timezone.make_aware(timezone.datetime(2024, 4, 8, 17)), + detail_contact_click_date=timezone.make_aware(timezone.datetime(2024, 4, 8, 18)), + ) + + # Tender no matching any siaes + cls.tender_during4 = TenderFactory( + presta_type=[siae_constants.PRESTA_DISP], + sectors=[], + is_country_area=False, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 9, 6)), + author=cls.author, + ) + cls.tender_during4.set_siae_found_list() + cls.tender_during4.refresh_from_db() + + cls.tender_after = TenderFactory( + presta_type=[siae_constants.PRESTA_BUILD], + sectors=[cls.sector], + is_country_area=True, + first_sent_at=timezone.make_aware(timezone.datetime(2024, 4, 10, 10)), + author=cls.author, + ) + cls.tender_after.set_siae_found_list() + cls.tender_after.refresh_from_db() + + @patch("lemarche.www.tenders.tasks.send_super_siaes_email_to_author") + @patch("django.utils.timezone.now") + def test_command_on_weekend(self, mock_now, mock_send_email): + # Assume today is Sunday + mock_now.return_value = timezone.make_aware(timezone.datetime(2024, 4, 7)) + + out = StringIO() + call_command("send_author_list_of_super_siaes_emails", stdout=out) + + self.assertIn("Weekend... Stopping. Come back on Monday :)", out.getvalue()) + self.assertNotIn("Step 1: Find Tender", out.getvalue()) + self.assertFalse(mock_send_email.called) + + @patch("lemarche.www.tenders.tasks.send_super_siaes_email_to_author") + @patch("django.utils.timezone.now") + def test_command_on_weekday(self, mock_now, mock_send_email): + # Assume today is a weekday (e.g., Wednesday) + mock_now.return_value = timezone.make_aware(timezone.datetime(2024, 4, 10, 7, 30)) + + out = StringIO() + call_command("send_author_list_of_super_siaes_emails", stdout=out) + + self.assertEqual(self.tender_before.siaes.count(), 3) + + output = out.getvalue() + + self.assertNotIn("Weekend... Stopping. Come back on Monday :)", output) + self.assertIn("Step 1: Find Tender", output) + self.assertIn("Step 2: send emails for each tender", output) + self.assertIn("Found 3 Tender without siaes interested", output) + + self.assertIn(f"3 top siaes finded for #{self.tender_during1.id}", output) + self.assertIn(f"3 top siaes finded for #{self.tender_during2.id}", output) + self.assertIn(f"0 top siaes finded for #{self.tender_during4.id}", output) + self.assertIn(f"Not enough siaes to send an email for #{self.tender_during4.id}", output) + + self.assertNotIn(f"top siaes finded for #{self.tender_before.id}", output) + self.assertNotIn(f"top siaes finded for #{self.tender_during3.id}", output) # with interested siae + self.assertNotIn(f"top siaes finded for #{self.tender_after.id}", output) + + self.assertEqual(mock_send_email.call_count, 2) + + @patch("lemarche.www.tenders.tasks.send_super_siaes_email_to_author") + @patch("django.utils.timezone.now") + def test_command_on_weekday_dry_run(self, mock_now, mock_send_email): + # Assume today is a weekday (e.g., Wednesday) + mock_now.return_value = timezone.make_aware(timezone.datetime(2024, 4, 10, 7, 30)) + + out = StringIO() + call_command("send_author_list_of_super_siaes_emails", stdout=out, dry_run=True) + + output = out.getvalue() + + self.assertNotIn("Weekend... Stopping. Come back on Monday :)", output) + self.assertIn("Step 1: Find Tender", output) + self.assertIn("Step 2: display top siaes for each tender", output) + self.assertIn("Found 3 Tender without siaes interested", output) + self.assertNotIn("Email sent to tender author", output) + + mock_send_email.assert_not_called() diff --git a/lemarche/tenders/tests.py b/lemarche/tenders/tests/test_models.py similarity index 100% rename from lemarche/tenders/tests.py rename to lemarche/tenders/tests/test_models.py diff --git a/lemarche/utils/apis/api_brevo.py b/lemarche/utils/apis/api_brevo.py index c92623341..29a7eb103 100644 --- a/lemarche/utils/apis/api_brevo.py +++ b/lemarche/utils/apis/api_brevo.py @@ -115,7 +115,6 @@ def create_or_update_company(siae): @task() def send_transactional_email_with_template( template_id: int, - subject: str, recipient_email: str, recipient_name: str, variables: dict, @@ -127,7 +126,6 @@ def send_transactional_email_with_template( send_smtp_email = sib_api_v3_sdk.SendSmtpEmail( sender={"email": from_email, "name": from_name}, to=[{"email": recipient_email, "name": recipient_name}], - subject=subject, template_id=template_id, params=variables, ) diff --git a/lemarche/www/tenders/tasks.py b/lemarche/www/tenders/tasks.py index 8d34c4749..42af827e1 100644 --- a/lemarche/www/tenders/tasks.py +++ b/lemarche/www/tenders/tasks.py @@ -11,7 +11,7 @@ from lemarche.tenders.models import PartnerShareTender, Tender, TenderSiae from lemarche.users.models import User from lemarche.utils import constants -from lemarche.utils.apis import api_hubspot, api_mailjet, api_slack +from lemarche.utils.apis import api_brevo, api_hubspot, api_mailjet, api_slack from lemarche.utils.data import date_to_string from lemarche.utils.emails import send_mail_async, whitelist_recipient_list from lemarche.utils.urls import get_domain_url, get_object_admin_url, get_object_share_url @@ -670,3 +670,47 @@ def notify_admin_siae_wants_cocontracting(tender: Tender, siae: Siae): if settings.BITOUBI_ENV == "prod": api_slack.send_message_to_channel(text=email_body, service_id=settings.SLACK_WEBHOOK_C4_TENDER_CHANNEL) + + +def send_super_siaes_email_to_author(tender: Tender, top_siaes: list[Siae]): + recipient_list = whitelist_recipient_list([tender.author.email]) + if recipient_list: + recipient_email = recipient_list[0] if recipient_list else "" + recipient_name = tender.author.full_name + + # Use transaction parameters of Brevo with loop for siaes, documentation : + # https://help.brevo.com/hc/en-us/articles/4402386448530-Customize-your-emails-using-transactional-parameters + variables = { + "author_name": recipient_name, + "tender_title": tender.title, + "tender_kind": tender.get_kind_display().lower(), + "siaes_count": len(top_siaes), + "siaes": [], + } + for siae in top_siaes: + variables["siaes"].append( + { + "name": siae.name_display, + "kind": siae.get_kind_display(), + "contact_name": siae.contact_full_name, + "contact_phone": siae.contact_phone, + "contact_email": siae.contact_email, + } + ) + + api_brevo.send_transactional_email_with_template( + template_id=settings.BREVO_TENDERS_AUTHOR_SUPER_SIAES_TEMPLATE_ID, + recipient_email=recipient_email, + recipient_name=recipient_name, + variables=variables, + ) + + # log email + log_item = { + "action": "email_super_siaes", + "email_to": recipient_email, + "email_timestamp": timezone.now().isoformat(), + "email_variables": variables, + } + tender.logs.append(log_item) + tender.save()