@@ -3,6 +3,7 @@ from authentication.exceptions.business_host_exceptions.already_account import ( ) from authentication.exceptions.business_host_exceptions.already_has_plan import ( AlreadyHasPlan, + InviteeHasPlan ) from authentication.exceptions.business_host_exceptions.already_host import ( AlreadyHost, @@ -10,6 +11,7 @@ from authentication.exceptions.business_host_exceptions.already_host import ( __all__ = ( 'AlreadyHasPlan', + 'InviteeHasPlan', 'AlreadyHost', 'AlreadyAccount', ) @@ -1,17 +1,19 @@ -from authentication.exceptions.business_host_exceptions.base_already import ( - BaseAlready, -) -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext as _ -class AlreadyHasPlan(BaseAlready): - def msg(self): - return dict( - message=_( - 'You already have an active tariff plan. You must request a ' - 'cancellation of your current tariff plan, after which ' - 'you will be able to create a Corporate Account.' - ), - user_id=self.user.uid, - plan_id=self.user.payment_plan.plan.uid, +class AlreadyHasPlan(Exception): + def __str__(self) -> str: + return _( + 'You already have an active tariff plan. You must request a ' + 'cancellation of your current tariff plan (via the \"Report an error\" button), after which ' + 'you will be able to create a Corporate Account' + ) + + +class InviteeHasPlan(Exception): + def __str__(self) -> str: + return _( + 'Invitee already has an active tariff plan. Invitee must request a ' + 'cancellation of his current tariff plan (via the \"Report an error\" button), after which ' + 'you will be able to invite him' ) @@ -89,6 +89,10 @@ class BusinessUserHost(BaseModel): is_log_history_enabled = models.BooleanField(default=False, verbose_name=_('Log history enabled')) + @property + def display_name(self) -> str: + return self.company_name or self.user.email + @property def accounts(self) -> QuerySet[BusinessAccount]: return self.accounts @@ -7,10 +7,7 @@ from rest_framework.request import Request from authentication.exceptions import business_host_exceptions from authentication.exceptions.business_account import BusinessAccountNotFound -from authentication.exceptions.business_host_exceptions import ( - AlreadyAccount, - AlreadyHasPlan, -) +from authentication.exceptions.business_host_exceptions import AlreadyAccount, InviteeHasPlan from authentication.exceptions.business_host_exceptions.access_denied import AccessDenied from authentication.exceptions.business_host_exceptions.already_host import ( AlreadyHost, @@ -95,7 +92,7 @@ class BusinessHostService: if AccountStatusSelector(user).is_business_account() and not user.is_deleted: raise AlreadyAccount(user) if PaymentPlanSelector(user).is_plan_paid(): - raise AlreadyHasPlan(user) + raise InviteeHasPlan(user) except CustomUserModel.DoesNotExist: return self.create_account( email=email, @@ -1,11 +1,13 @@ import logging +from typing import Any, Sequence import dns.resolver from django.conf import settings from django.core.mail import EmailMessage, send_mail from django.template import TemplateDoesNotExist from django.template.loader import get_template -from django.utils.html import format_html, strip_tags +from django.utils.html import strip_tags +from django.utils.safestring import SafeString from django.utils.translation import gettext_lazy as _ from authentication.exceptions.email_exceptions import LetterNotFound, LetterUnknownException @@ -13,8 +15,7 @@ from authentication.exceptions.user import DomainNotFound from authentication.models import BusinessAccount, BusinessUserHost from authentication.models.user import CustomUserModel from authentication.services.email_token_service import EmailTokenService -from ml_model.services.minio_service import MinIOService -from reports.models.error_report import ErrorReport +from reports.domain import Report logger = logging.getLogger(__name__) @@ -28,16 +29,18 @@ class EmailService: self.user.save() return self.user.is_subscribed_to_emails - def send_email(self, subject: str, message: str, user_email: str): + @staticmethod + def send_email(subject: str, message: str, user_emails: Sequence[str]) -> None: try: - email_domain = user_email.split('@')[-1] - dns.resolver.resolve(email_domain, 'MX') + for user_email in user_emails: + email_domain = user_email.split('@')[-1] + dns.resolver.resolve(email_domain, 'MX') send_mail( subject=subject, message=strip_tags(message), html_message=message, from_email=settings.EMAIL_HOST_USER, - recipient_list=(user_email,), + recipient_list=user_emails, auth_password=settings.EMAIL_HOST_PASSWORD, ) except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.exception.Timeout): @@ -46,27 +49,29 @@ class EmailService: logger.exception(exc) raise Exception(_('Error occured when proceed email sending')) + @staticmethod + def _render_letter_template(template_name: str, context: dict[str, Any]) -> SafeString: + try: + template = get_template(f'{template_name}.html') + html_message = template.render(context=context) + except TemplateDoesNotExist: + raise LetterNotFound + except Exception as exc: + raise LetterUnknownException from exc + return html_message + def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() + html_message = self._render_letter_template( + template_name='authentication/account_confirmation', + context={ + 'email': self.user.email, + 'confirmation_url': settings.USER_CONFIRMATION_URL, + 'confirmation_token': token.key, + }, + ) self.send_email( - subject='Подтверждение аккаунта', - message=format_html( - """ - Добро пожаловать на платформу AIR! - - Ваша почта на платформе - {} - Для того, чтобы подтвердить регистрацию - перейдите по ссылке ниже: - [Нажмите для активации] - Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку: - {}?token={} - """, - self.user.email, - settings.USER_CONFIRMATION_URL, - token.key, - settings.USER_CONFIRMATION_URL, - token.key, - ), - user_email=self.user.email, + subject='Подтверждение аккаунта', message=html_message, user_emails=(self.user.email,) ) def send_password_reset_email(self): @@ -74,106 +79,82 @@ class EmailService: self.send_email( subject='Сброс пароля', message=f'{settings.USER_PASSWORD_RESET_URL}?token={token.key}', - user_email=self.user.email, + user_emails=(self.user.email,), ) - def send_error_email(self, error: ErrorReport): - message = f""" - Пользователь {error.author.email} сообщил об ошибке: - {error.report_text} - Скриншоты ошибки (если пользователь их приложил) - во вложении - """ - + def send_error_email(self, report: Report) -> None: + html_message = self._render_letter_template( + template_name='authentication/error_email', + context={'email': self.user.email, 'report_text': report.message}, + ) mail = EmailMessage( - subject=f'Ошибка у пользователя {error.author.email}!', - body=message, + subject=f'Ошибка у пользователя {self.user.email}!', + body=html_message, from_email=settings.EMAIL_HOST_USER, to=settings.ERROR_EMAIL_RECIPIENTS, - reply_to=(error.author.email,), + # reply_to=(error.author.email,), ) - if error.additional_images is not None: - for _, img in error.additional_images.items(): - image = MinIOService().get_object('air-errors', img) - mail.attach(img, image) - + for img in report.attachments: + mail.attach(img.name, img.read(), img.content_type) + img.close() mail.send(fail_silently=True) def send_copr_purchase_email(self, host: BusinessUserHost): - message = f""" - Пользователь {host.user.email} зарегистрировал корпоративный аккаунт: - - Компания: {host.company_name} - Сектор: {host.company_sector} - ИНН: {host.ITN} - ОГРН: {host.PSRN} - Контактное лицо: - {host.preferred_name}, {host.job_title} - {host.corporate_phone} - {host.corporate_email} - """ - - mail = EmailMessage( + html_message = self._render_letter_template( + template_name='authentication/corporate_purchase_email', context={'host': host} + ) + self.send_email( subject='Новый корпоративный аккаунт', - body=message, - from_email=settings.EMAIL_HOST_USER, - to=('a.ippolitov@air.fail', 't.bikbov@air.fail'), + message=html_message, + user_emails=('a.ippolitov@air.fail', 't.bikbov@air.fail'), ) - mail.send(fail_silently=True) def send_corporate_greeting_email(self, account: BusinessAccount, password: str | None = None): if self.user.host_account is None: raise Exception(_('Regular users cannot send introductory letters')) token = EmailTokenService(account.user).generate_user_token() - try: - template = get_template('authentication/corporate_greeting_email.html') - html_message = template.render( - context={ - 'company_name': self.user.host_account.company_name, - 'invitation_url': settings.INVITATION_RESPONSE_URL, - 'token': token.key, - 'email': account.user.email, - 'password': password, - } - ) - except TemplateDoesNotExist: - raise LetterNotFound - except Exception as exc: - raise LetterUnknownException from exc + html_message = self._render_letter_template( + template_name='authentication/corporate_greeting_email', + context={ + 'business_account': account, + 'invitation_url': settings.INVITATION_RESPONSE_URL, + 'token': token.key, + 'password': password, + }, + ) self.send_email( - subject='Ваш аккаунт на платформе AIR', message=html_message, user_email=account.user.email + subject='Ваш аккаунт на платформе AIR', message=html_message, user_emails=(account.user.email,) ) def send_reinvited_email(self, account: BusinessAccount, password: str | None = None) -> None: - try: - template = get_template('authentication/reinvited_employee_letter.html') - html_message = template.render(context={'email': account.user.email, 'password': password}) - except TemplateDoesNotExist: - raise LetterNotFound - except Exception as exc: - raise LetterUnknownException from exc + html_message = self._render_letter_template( + template_name='authentication/reinvited_employee_letter', + context={'email': account.user.email, 'password': password}, + ) self.send_email( - subject='Ваш аккаунт на платформе AIR', message=html_message, user_email=account.user.email + subject='Ваш аккаунт на платформе AIR', message=html_message, user_emails=(account.user.email,) ) - def send_corporate_invitation_email(self, account: BusinessAccount): + def send_corporate_invitation_email(self, account: BusinessAccount) -> None: if self.user.host_account is None: raise Exception(_('Regular users cannot send invitation letters')) token = EmailTokenService(account.user).generate_user_token() - message = format_html( - """ - Добрый день, {}! - Компания {} приглашает вас получить доступ - к корпоративному аккаунту на нашей платформе. - Чтобы ответить на приглашение - перейдите по ссылке ниже: - [Нажмите для подтверждения] - Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку: - {}?token={} - """, - account.user.email, - self.user.host_account.company_name, - settings.INVITATION_RESPONSE_URL, - token.key, - settings.INVITATION_RESPONSE_URL, - token.key, + html_message = self._render_letter_template( + template_name='authentication/corporate_invitation_email', + context={ + 'email': account.user.email, + 'host': self.user.host_account, + 'invite_url': settings.INVITATION_RESPONSE_URL, + 'token': token.key, + }, + ) + self.send_email('Приглашение на платформу AIR', html_message, (account.user.email,)) + + @classmethod + def send_low_balance_email(cls, host: BusinessUserHost) -> None: + html_message = cls._render_letter_template( + template_name='authentication/low_balance_email', context={'host': host} + ) + cls.send_email( + f'AIR: баланс корпоративного аккаунта ниже {host.token_cap}', html_message, host.token_cap_emails ) - self.send_email('', message, account.user.email) @@ -0,0 +1,16 @@ + + +
+ +Добро пожаловать на платформу AIR!
+Ваша почта на платформе - {{ email }}
+Для того, чтобы подтвердить регистрацию - перейдите по ссылке ниже:
+ +Если ссылка не открывается по кнопке, попробуйте вставить ссылку в адресную строку:
+{{ confirmation_url }}?token={{ confirmation_token }}
+ + \ No newline at end of file @@ -7,7 +7,7 @@Добро пожаловать на платформу AIR!
- Этот аккаунт был создан для вас {{ company_name }} в рабочих целях.
+ Этот аккаунт был создан для вас {{ business_account.parent_company.display_name }} в рабочих целях.
Вы можете использовать все ресурсы корпоративного аккаунта — оплачивать
подписку нет необходимости.
Ваши данные для входа:
- E-mail: {{ email }}
+ E-mail: {{ business_account.user.email }}
Пароль: {{ password }}
Всегда с вами, команда AIR