Skip to content

Commit

Permalink
feat(Dépôt de besoins): envoi d'un email à l'auteur d'un dépôt de bes…
Browse files Browse the repository at this point in the history
…oin avec 5 ESI (#1167)
  • Loading branch information
SebastienReuiller authored Apr 24, 2024
1 parent 9f37452 commit 3775153
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 3 deletions.
1 change: 1 addition & 0 deletions clevercloud/cron.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions clevercloud/tenders_send_author_list_of_super_siaes_emails.sh
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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!")
Empty file.
175 changes: 175 additions & 0 deletions lemarche/tenders/tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -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()
File renamed without changes.
2 changes: 0 additions & 2 deletions lemarche/utils/apis/api_brevo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down
46 changes: 45 additions & 1 deletion lemarche/www/tenders/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

0 comments on commit 3775153

Please sign in to comment.