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

POC a Brevo integration #290

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
30 changes: 29 additions & 1 deletion src/backend/core/authentication/backends.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Authentication Backends for the Meet core app."""

from django.conf import settings
from django.core.exceptions import SuspiciousOperation
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from django.utils.translation import gettext_lazy as _

import requests
Expand All @@ -10,6 +10,11 @@
)

from core.models import User
from core.services.marketing_service import (
ContactCreationError,
ContactData,
MarketingService,
)


class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
Expand Down Expand Up @@ -86,6 +91,10 @@ def get_or_create_user(self, access_token, id_token, payload):
password="!", # noqa: S106
**claims,
)

if settings.SIGNUP_NEW_USER_TO_MARKETING_EMAIL:
self.signup_to_marketing_email(email)

elif not user:
return None

Expand All @@ -96,6 +105,25 @@ def get_or_create_user(self, access_token, id_token, payload):

return user

@staticmethod
def signup_to_marketing_email(email):
"""Pragmatic approach to newsletter signup during authentication flow.

Details:
1. Uses a very short timeout (1s) to prevent blocking the auth process
2. Silently fails if the marketing service is down/slow to prioritize user experience
3. Trade-off: May miss some signups but ensures auth flow remains fast

Note: For a more robust solution, consider using Async task processing (Celery/Django-Q)
"""
try:
contact_data = ContactData(
email=email, attributes={"VISIO_SOURCE": ["SIGNIN"]}
)
MarketingService().create_contact(contact_data, timeout=1)
except (ContactCreationError, ImproperlyConfigured):
pass

def get_existing_user(self, sub, email):
"""Fetch existing user by sub or email."""
try:
Expand Down
Empty file.
105 changes: 105 additions & 0 deletions src/backend/core/services/marketing_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Marketing service in charge of pushing data for marketing automation."""

import logging
from dataclasses import dataclass
from typing import Dict, List, Optional

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured

import brevo_python

logger = logging.getLogger(__name__)


class ContactCreationError(Exception):
"""Raised when the contact creation fails."""


@dataclass
class ContactData:
"""Data structure for contact information."""

email: str
attributes: Optional[Dict[str, str]] = None
list_ids: Optional[List[int]] = None
update_enabled: bool = True


class MarketingService:
"""Service for managing marketing automation.

Currently, implements Brevo as our marketing automation platform for:
- Contact list management
- User segmentation
- Marketing campaign automation
- Email communications

Note: This is an initial implementation focused on Brevo integration.
Future iterations may abstract common marketing operations to support
additional platforms or migration needs. Consider refactoring core
functionality into platform-agnostic interfaces as requirements evolve.
"""

def __init__(self):
"""Initialize the marketing service."""

if not settings.BREVO_API_KEY:
raise ImproperlyConfigured("Brevo API key is required")

configuration = brevo_python.Configuration()
configuration.api_key["api-key"] = settings.BREVO_API_KEY

self._api_client = brevo_python.ApiClient(configuration)

def create_contact(self, contact_data: ContactData, timeout=None) -> dict:
"""Create or update a contact in Brevo.

Contacts are automatically added to both a shared list for "La Suite"
and Visio list. Both lists' ids are configured via settings.BREVO_API_CONTACT_LIST_IDS.
Additional list assignments can be specified through contact_data.list_ids.

Contact attributes are essential for segmentation within lists and power marketing
automation workflows.

Each product should define its own specific attributes to avoid conflicts. As our Brevo
integration is new, we expect to iterate on which attributes to track based on marketing
automation needs. Attributes must be pre-configured in Brevo's interface.

Note: Take care when defining new attributes or modifying existing ones, as these
changes can impact marketing workflows and segmentation across products.
"""

if not settings.BREVO_API_CONTACT_LIST_IDS:
raise ImproperlyConfigured(
"Default Brevo List IDs must be configured in settings."
)

contact_api = brevo_python.ContactsApi(self._api_client)

attributes = {
**settings.BREVO_API_CONTACT_ATTRIBUTES,
**(contact_data.attributes or {}),
}

list_ids = (contact_data.list_ids or []) + settings.BREVO_API_CONTACT_LIST_IDS

contact = brevo_python.CreateContact(
email=contact_data.email,
attributes=attributes,
list_ids=list_ids,
update_enabled=contact_data.update_enabled,
)

kwargs = {}

if timeout is not None:
kwargs["_request_timeout"] = timeout

try:
response = contact_api.create_contact(contact, **kwargs)
except brevo_python.rest.ApiException as err:
logger.exception("Failed to create contact in Brevo")
raise ContactCreationError("Failed to create contact in Brevo") from err

return response
19 changes: 19 additions & 0 deletions src/backend/meet/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,25 @@ class Base(Configuration):
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
)

# Marketing and communication settings
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
True,
environ_name="SIGNUP_NEW_USERS_TO_NEWSLETTER",
environ_prefix=None,
help_text=(
"When enabled, new users are automatically added to Brevo mailing list "
"for product updates, marketing communications, and customized emails. "
),
)

BREVO_API_KEY = values.Value(
None, environ_name="BREVO_API_KEY", environ_prefix=None
)
BREVO_API_CONTACT_LIST_IDS = values.ListValue(
[], environ_name="BREVO_API_CONTACT_LIST_IDS", environ_prefix=None
)
BREVO_API_CONTACT_ATTRIBUTES = values.DictValue({"VISIO_USER": True})

# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
Expand Down
1 change: 1 addition & 0 deletions src/backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.35.76",
"brevo-python==1.1.2",
"Brotli==1.1.0",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
Expand Down
Loading