diff --git a/contrib/env-sample b/contrib/env-sample index 01a6d581..7dcbb76c 100644 --- a/contrib/env-sample +++ b/contrib/env-sample @@ -68,4 +68,5 @@ HOTZAPP_API_URL=https://hotzapp.me DISCORD_APP_CLIENT_ID=your-app-client-id DISCORD_APP_CLIENT_SECRET=your-app-client-secret DISCORD_APP_BOT_TOKEN=your-bot-token -DISCORD_GUILD_ID=your-discord_server_id \ No newline at end of file +DISCORD_GUILD_ID=your-discord_server_id +DISCORD_GUILD_SALES_CHANNEL_ID=your-guild-discord-channel-to-send-sales-messages \ No newline at end of file diff --git a/pythonpro/discord/bot.py b/pythonpro/discord/bot.py index 66ccb8cf..49f9bf06 100644 --- a/pythonpro/discord/bot.py +++ b/pythonpro/discord/bot.py @@ -2,4 +2,14 @@ from pythonpro.discord.api_client import DiscordBotClient -discord_bot_client = DiscordBotClient(settings.DISCORD_APP_BOT_TOKEN) + +class _DevProDiscordBotClient(DiscordBotClient): + """ + This class provides data respective to specific seetings for DevPro Discord Guild + """ + + def send_to_sales_channel(self, msg: str) -> dict: + return self.create_message(settings.DISCORD_GUILD_SALES_CHANNEL_ID, msg) + + +devpro_discord_bot_client = _DevProDiscordBotClient(settings.DISCORD_APP_BOT_TOKEN) diff --git a/pythonpro/discord/facade.py b/pythonpro/discord/facade.py index 6527c4f7..1369ecba 100644 --- a/pythonpro/discord/facade.py +++ b/pythonpro/discord/facade.py @@ -1,9 +1,15 @@ import logging +from datetime import timedelta from django.conf import settings +from django.contrib.auth import get_user_model +from django.db.models import Max -from pythonpro.discord.bot import discord_bot_client -from pythonpro.discord.tasks import clean_discord_user +from pythonpro.discord.bot import devpro_discord_bot_client +from pythonpro.discord.tasks import clean_discord_user, warn_subscription_expiration +from pythonpro.memberkit.models import Subscription + +from django.utils.timezone import datetime logger = logging.getLogger(__name__) @@ -11,7 +17,7 @@ def clean_discord_users(): discord_user_id = 0 while True: - discord_members = discord_bot_client.list_guild_members(settings.DISCORD_GUILD_ID, after=discord_user_id) + discord_members = devpro_discord_bot_client.list_guild_members(settings.DISCORD_GUILD_ID, after=discord_user_id) if len(discord_members) == 0: break for member in discord_members: @@ -25,4 +31,16 @@ def clean_discord_users(): def warn_users_about_subscription_expiration(): - return None \ No newline at end of file + today = datetime.today() + thirty_days_in_future = today + timedelta(days=30) + users_with_subscription_expirating = get_user_model().objects.annotate( + max_subscription_expiration_date=Max('subscriptions__expired_at') + ).filter( + subscriptions__status=Subscription.Status.ACTIVE, + max_subscription_expiration_date__lte=thirty_days_in_future + ).values('id', 'max_subscription_expiration_date') + for user_dct in users_with_subscription_expirating: + warn_subscription_expiration.delay( + user_dct['id'], + user_dct['max_subscription_expiration_date'].strftime('%d/%m/%Y') + ) diff --git a/pythonpro/discord/tasks.py b/pythonpro/discord/tasks.py index 6ae1cd65..8b9cd5fb 100644 --- a/pythonpro/discord/tasks.py +++ b/pythonpro/discord/tasks.py @@ -2,13 +2,27 @@ from celery import shared_task from django.conf import settings +from django.contrib.auth import get_user_model -from pythonpro.discord.bot import discord_bot_client -from pythonpro.discord.models import DiscordLead +from pythonpro.discord.bot import devpro_discord_bot_client +from pythonpro.discord.models import DiscordLead, DiscordUser from pythonpro.memberkit.models import Subscription logger = logging.getLogger(__name__) +msg = """Olá, sou o bot da DevPro no Discord. + +Eu não identifiquei sua conta de Discord em nosso sistema. Por isso eu removi seu acesso. + +Você pode conferir todo seus histórico de assinaturas acessando + +https://painel.dev.pro.br + +Se tiver qualquer dúvida, entre em contato pelo email suporte@dev.pro.br + +Um abraço do Bot da DevPro +""" + @shared_task( rate_limit=1, @@ -31,21 +45,76 @@ def clean_discord_user(discord_user_id): ) if not has_discord_access: - discord_bot_client.send_user_message(discord_user_id, msg) - discord_bot_client.remove_guild_member(settings.DISCORD_GUILD_ID, discord_user_id) + devpro_discord_bot_client.send_user_message(discord_user_id, msg) + devpro_discord_bot_client.remove_guild_member(settings.DISCORD_GUILD_ID, discord_user_id) logging.info(f'Clean discord user: {discord_user_id} with status: {lead_status.label}') -msg = """Olá, sou o bot da DevPro no Discord. +_SALES_MSG_TEMPLATE = """Usuário: {user_name} +Com licença expirando em {expiration_date} +Id: {user_id} +email; {user_email} +""" -Eu não identifiquei sua conta de Discord em nosso sistema. Por isso eu removi seu acesso. -Você pode conferir todo seus histórico de assinaturas acessando +_WARN_USER_TEMPLATE = """Olá {user_name}, -https://painel.dev.pro.br +Sua assinatura anual da DevPro está prestes a expirar, e queremos oferecer uma oportunidade imperdível para que você continue aproveitando todos os benefícios de ser nosso assinante. -Se tiver qualquer dúvida, entre em contato pelo email suporte@dev.pro.br +Renove agora e garanta um desconto exclusivo de R$ 300! -Um abraço do Bot da DevPro -""" +🔒 Por que renovar sua assinatura? + +* Economize R$ 300: Apenas para assinantes atuais, estamos oferecendo um desconto especial de R$ 300 na renovação anual. +* Acesso Ininterrupto: Continue desfrutando de conteúdos exclusivos, cursos atualizados e suporte especializado sem nenhuma interrupção. +* Encontros ao Vivo com Instrutores Experientes: Mantenha o acesso a sessões ao vivo com nossos instrutores especializados, proporcionando aprendizado personalizado e esclarecimento de dúvidas em tempo real. +* Novidades e Exclusividades: Esteja sempre à frente com as últimas novidades e ferramentas que a DevPro tem a oferecer. + +⚠️ Atenção: Este desconto de R$ 300 é válido apenas até o vencimento da sua assinatura atual. Após {expiration_date}, o valor cheio será aplicado, e você perderá essa oferta especial. + +Não deixe essa oportunidade escapar! Renove sua assinatura agora e continue sua jornada de aprendizado e crescimento profissional com a DevPro. + +Para renovar, acesse https://painel.dev.pro.br/checkout/pagarme/renovacao-comunidade-devpro ou entre em contato com nosso suporte através do suporte@dev.pro.br. + +Estamos ansiosos para continuar sendo parte do seu sucesso! + +Atenciosamente, +Bot DevPro + +OBS: Para você não correr os risco de perder essa oportunidade, vou te reenviar essa mensagem uma vez por dia. +""" # noqa: E501 W291 + + +@shared_task( + rate_limit=1, + max_retries=5, + retry_backoff=True, + retry_backoff_max=700, + retry_jitter=True +) +def warn_subscription_expiration(user_id: int, expiration_date: str): + user = get_user_model().objects.select_related('discorduser').get(id=user_id) + devpro_discord_bot_client.send_to_sales_channel(_SALES_MSG_TEMPLATE.format( + user_name=user.first_name, + user_email=user.email, + user_id=user_id, + expiration_date=expiration_date, + + )) + + logging.info(f'Warn msg sent to sales discord channel for user with id {user_id}') + try: + discorduser = user.discorduser + except DiscordUser.DoesNotExist: + logger.info(f'No discord user found for user with id {user.id}') + else: + devpro_discord_bot_client.send_user_message( + discorduser.discord_id, + _WARN_USER_TEMPLATE.format( + user_name=user.first_name, + expiration_date=expiration_date + ) + ) + logger.info(f'Subscription warn sent to user with id: {user.id}') + return None diff --git a/pythonpro/discord/tests/test_commands.py b/pythonpro/discord/tests/test_commands.py index c5598ffd..64e5fcf5 100644 --- a/pythonpro/discord/tests/test_commands.py +++ b/pythonpro/discord/tests/test_commands.py @@ -1,10 +1,51 @@ +from datetime import timedelta + from django.core import management +from django.utils.datetime_safe import datetime +from model_bakery import baker + +from pythonpro.memberkit.models import Subscription def test_clean_discord_users_command(mocker): - mocker.patch('pythonpro.discord.facade.discord_bot_client.list_guild_members', return_value=[]) + mocker.patch('pythonpro.discord.facade.devpro_discord_bot_client.list_guild_members', return_value=[]) management.call_command('clean_discord_users') -def test_warn_users_about_subscriptions(): +def test_warn_users_about_subscriptions(mocker, django_user_model): + mock = mocker.patch('pythonpro.discord.facade.warn_subscription_expiration', return_value=[]) + subscriber = baker.make(django_user_model) + expiration_date = datetime.today() + timedelta(days=29) + baker.make( + Subscription, status=Subscription.Status.ACTIVE, + expired_at=expiration_date, + subscriber=subscriber + ) + management.call_command('warn_users_about_subscription_expiration') + mock.delay.assert_called_once_with(subscriber.id, expiration_date.strftime('%d/%m/%Y')) + + +def test_dont_botter_user_with_inactive_subscriptions(mocker, django_user_model): + mock = mocker.patch('pythonpro.discord.facade.warn_subscription_expiration', return_value=[]) + subscriber = baker.make(django_user_model) + expiration_date = datetime.today() + timedelta(days=29) + baker.make( + Subscription, status=Subscription.Status.INACTIVE, + expired_at=expiration_date, + subscriber=subscriber + ) + management.call_command('warn_users_about_subscription_expiration') + assert not mock.delay.called + + +def test_dont_botter_user_with_more_than_30_days_subscription(mocker, django_user_model): + mock = mocker.patch('pythonpro.discord.facade.warn_subscription_expiration', return_value=[]) + subscriber = baker.make(django_user_model) + expiration_date = datetime.today() + timedelta(days=31) + baker.make( + Subscription, status=Subscription.Status.ACTIVE, + expired_at=expiration_date, + subscriber=subscriber + ) management.call_command('warn_users_about_subscription_expiration') + assert not mock.delay.called diff --git a/pythonpro/discord/tests/test_warn_subscription_expiration.py b/pythonpro/discord/tests/test_warn_subscription_expiration.py new file mode 100644 index 00000000..dc89e186 --- /dev/null +++ b/pythonpro/discord/tests/test_warn_subscription_expiration.py @@ -0,0 +1,77 @@ +from model_bakery import baker + +from pythonpro.discord.models import DiscordUser +from pythonpro.discord.tasks import warn_subscription_expiration, _SALES_MSG_TEMPLATE + + +def test_warn_subscription_expiration(django_user_model, mocker): + user = baker.make(django_user_model) + send_to_sales_mock = mocker.patch( + 'pythonpro.discord.facade.devpro_discord_bot_client.send_to_sales_channel', + return_value=[] + ) + expiration_date = '23/05/2024' + warn_subscription_expiration(user.id, expiration_date) + send_to_sales_mock.assert_called_once_with(_SALES_MSG_TEMPLATE.format( + user_name=user.first_name, + user_email=user.email, + user_id=user.id, + expiration_date=expiration_date, + + )) + + +WARN_MSG = """Olá John, + +Sua assinatura anual da DevPro está prestes a expirar, e queremos oferecer uma oportunidade imperdível para que você continue aproveitando todos os benefícios de ser nosso assinante. + +Renove agora e garanta um desconto exclusivo de R$ 300! + +🔒 Por que renovar sua assinatura? + +* Economize R$ 300: Apenas para assinantes atuais, estamos oferecendo um desconto especial de R$ 300 na renovação anual. +* Acesso Ininterrupto: Continue desfrutando de conteúdos exclusivos, cursos atualizados e suporte especializado sem nenhuma interrupção. +* Encontros ao Vivo com Instrutores Experientes: Mantenha o acesso a sessões ao vivo com nossos instrutores especializados, proporcionando aprendizado personalizado e esclarecimento de dúvidas em tempo real. +* Novidades e Exclusividades: Esteja sempre à frente com as últimas novidades e ferramentas que a DevPro tem a oferecer. + +⚠️ Atenção: Este desconto de R$ 300 é válido apenas até o vencimento da sua assinatura atual. Após 23/05/2024, o valor cheio será aplicado, e você perderá essa oferta especial. + +Não deixe essa oportunidade escapar! Renove sua assinatura agora e continue sua jornada de aprendizado e crescimento profissional com a DevPro. + +Para renovar, acesse https://painel.dev.pro.br/checkout/pagarme/renovacao-comunidade-devpro ou entre em contato com nosso suporte através do suporte@dev.pro.br. + +Estamos ansiosos para continuar sendo parte do seu sucesso! + +Atenciosamente, +Bot DevPro + +OBS: Para você não correr os risco de perder essa oportunidade, vou te reenviar essa mensagem uma vez por dia. +""" # noqa: E501 W291 + + +def test_warn_subscription_expiration_user_and_sales_channel(django_user_model, mocker): + django_user = baker.make(django_user_model, first_name='John') + discord_user = baker.make(DiscordUser, user=django_user, discord_id='946364767864504360') + + send_to_sales_mock = mocker.patch( + 'pythonpro.discord.facade.devpro_discord_bot_client.send_to_sales_channel', + return_value=[] + ) + + send_user_msg_mock = mocker.patch( + 'pythonpro.discord.facade.devpro_discord_bot_client.send_user_message', + return_value=[] + ) + expiration_date = '23/05/2024' + + warn_subscription_expiration(django_user.id, expiration_date) + + send_to_sales_mock.assert_called_once_with(_SALES_MSG_TEMPLATE.format( + user_name=django_user.first_name, + user_email=django_user.email, + user_id=django_user.id, + expiration_date=expiration_date, + + )) + + send_user_msg_mock.assert_called_once_with(discord_user.discord_id, WARN_MSG) diff --git a/pythonpro/settings.py b/pythonpro/settings.py index d33e4796..02f1d231 100644 --- a/pythonpro/settings.py +++ b/pythonpro/settings.py @@ -315,6 +315,7 @@ DISCORD_APP_CLIENT_SECRET = config('DISCORD_APP_CLIENT_SECRET') DISCORD_APP_BOT_TOKEN = config('DISCORD_APP_BOT_TOKEN') DISCORD_GUILD_ID = config('DISCORD_GUILD_ID') +DISCORD_GUILD_SALES_CHANNEL_ID = config('DISCORD_GUILD_SALES_CHANNEL_ID') # Celery config