Skip to content
Open
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
38 changes: 37 additions & 1 deletion app/common/schemas/notifications_sch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from enum import StrEnum, auto
from typing import Annotated, Literal
from uuid import UUID

from pydantic import BaseModel, Field
from pydantic import AwareDatetime, BaseModel, Field

from app.common.schemas.classrooms_sch import ClassroomRole

Expand All @@ -17,6 +18,14 @@ class NotificationKind(StrEnum):
RECIPIENT_INVOICE_CREATED_V1 = auto()
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()

SINGLE_CLASSROOM_EVENT_CREATED_V1 = auto()
CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1 = auto()
CLASSROOM_EVENT_INSTANCE_CANCELLED_V1 = auto()

REPEATING_CLASSROOM_EVENT_CREATED_V1 = auto()
CLASSROOM_EVENT_REPETITION_UPDATED_V1 = auto()
CLASSROOM_EVENT_REPETITION_CANCELLED_V1 = auto()

CUSTOM_V1 = auto()


Expand Down Expand Up @@ -53,6 +62,28 @@ class RecipientInvoiceNotificationPayloadSchema(BaseModel):
recipient_invoice_id: int


class ClassroomEventInstanceNotificationPayloadSchema(BaseModel):
kind: Literal[
NotificationKind.SINGLE_CLASSROOM_EVENT_CREATED_V1,
NotificationKind.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1,
NotificationKind.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1,
]

classroom_id: int
event_instance_id: UUID


class ClassroomScheduleFocusNotificationPayloadSchema(BaseModel):
kind: Literal[
NotificationKind.REPEATING_CLASSROOM_EVENT_CREATED_V1,
NotificationKind.CLASSROOM_EVENT_REPETITION_UPDATED_V1,
NotificationKind.CLASSROOM_EVENT_REPETITION_CANCELLED_V1,
]

classroom_id: int
focused_at: AwareDatetime


class CustomNotificationPayloadSchema(BaseModel):
kind: Literal[NotificationKind.CUSTOM_V1]

Expand All @@ -69,6 +100,8 @@ class CustomNotificationPayloadSchema(BaseModel):
| EnrollmentNotificationPayloadSchema
| ClassroomNotificationPayloadSchema
| RecipientInvoiceNotificationPayloadSchema
| ClassroomEventInstanceNotificationPayloadSchema
| ClassroomScheduleFocusNotificationPayloadSchema
| CustomNotificationPayloadSchema,
Field(discriminator="kind"),
]
Expand Down Expand Up @@ -106,3 +139,6 @@ class NotificationInputV2Schema(BaseModel):
list[AnyRecipientFilterSchema],
Field(min_length=1, max_length=100),
]


# TODO (?) add recipient logic to payload instead?
16 changes: 15 additions & 1 deletion app/common/schemas/pochta_sch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class EmailMessageKind(StrEnum):
RECIPIENT_INVOICE_CREATED_V1 = auto()
STUDENT_RECIPIENT_INVOICE_PAYMENT_CONFIRMED_V1 = auto()

UNIVERSAL_V3 = auto()


class CustomEmailMessagePayloadSchema(BaseModel):
kind: Literal[EmailMessageKind.CUSTOM_V1]
Expand Down Expand Up @@ -72,11 +74,23 @@ class RecipientInvoiceNotificationEmailMessagePayloadSchema(
recipient_invoice_id: int


class UniversalEmailMessagePayloadSchema(BaseModel):
kind: Literal[EmailMessageKind.UNIVERSAL_V3] = EmailMessageKind.UNIVERSAL_V3

theme: str
pre_header: str
header: str
content: str
button_text: str
button_link: str


AnyEmailMessagePayload = Annotated[
CustomEmailMessagePayloadSchema
| TokenEmailMessagePayloadSchema
| ClassroomNotificationEmailMessagePayloadSchema
| RecipientInvoiceNotificationEmailMessagePayloadSchema,
| RecipientInvoiceNotificationEmailMessagePayloadSchema
| UniversalEmailMessagePayloadSchema,
Field(discriminator="kind"),
]

Expand Down
105 changes: 104 additions & 1 deletion app/notifications/services/adapters/base_adapter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from abc import ABC, abstractmethod
from typing import assert_never, cast
from typing import Any, assert_never, cast
from urllib.parse import urlencode

from app.common.config import settings
from app.common.schemas.notifications_sch import (
ClassroomEventInstanceNotificationPayloadSchema,
ClassroomNotificationPayloadSchema,
ClassroomScheduleFocusNotificationPayloadSchema,
CustomNotificationPayloadSchema,
EnrollmentNotificationPayloadSchema,
InvitationAcceptanceNotificationPayloadSchema,
Expand All @@ -16,6 +20,39 @@ class BaseNotificationAdapter[T](ABC):
def __init__(self, notification: Notification) -> None:
self.notification = notification

def build_url(self, path: str, params: dict[str, Any]) -> str:
query_string = urlencode(
{
**params,
"read_notification_id": self.notification.id,
}
)
return f"{settings.frontend_app_base_url}{path}?{query_string}"

def build_student_classroom_event_instance_url(
self, payload: ClassroomEventInstanceNotificationPayloadSchema
) -> str:
return self.build_url(
path=f"/classrooms/{payload.classroom_id}",
params={
"tab": "schedule",
"role": "student",
"event_instance_id": payload.event_instance_id,
},
)

def build_student_classroom_schedule_focus_url(
self, payload: ClassroomScheduleFocusNotificationPayloadSchema
) -> str:
return self.build_url(
path=f"/classrooms/{payload.classroom_id}",
params={
"tab": "schedule",
"role": "student",
"focused_at": payload.focused_at.isoformat(),
},
)

@abstractmethod
def adapt_individual_invitation_accepted_v1(
self,
Expand Down Expand Up @@ -58,6 +95,48 @@ def adapt_student_recipient_invoice_payment_confirmed_v1(
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_single_classroom_event_created_v1(
self,
payload: ClassroomEventInstanceNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_classroom_event_instance_rescheduled_v1(
self,
payload: ClassroomEventInstanceNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_classroom_event_instance_cancelled_v1(
self,
payload: ClassroomEventInstanceNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_repeating_classroom_event_created_v1(
self,
payload: ClassroomScheduleFocusNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_classroom_event_repetition_updated_v1(
self,
payload: ClassroomScheduleFocusNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_classroom_event_repetition_cancelled_v1(
self,
payload: ClassroomScheduleFocusNotificationPayloadSchema,
) -> T:
raise NotImplementedError

@abstractmethod
def adapt_custom_v1(
self,
Expand Down Expand Up @@ -93,6 +172,30 @@ def adapt(self) -> T:
return self.adapt_student_recipient_invoice_payment_confirmed_v1(
cast(RecipientInvoiceNotificationPayloadSchema, payload)
)
case NotificationKind.SINGLE_CLASSROOM_EVENT_CREATED_V1:
return self.adapt_single_classroom_event_created_v1(
cast(ClassroomEventInstanceNotificationPayloadSchema, payload)
)
case NotificationKind.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1:
return self.adapt_classroom_event_instance_rescheduled_v1(
cast(ClassroomEventInstanceNotificationPayloadSchema, payload)
)
case NotificationKind.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1:
return self.adapt_classroom_event_instance_cancelled_v1(
cast(ClassroomEventInstanceNotificationPayloadSchema, payload)
)
case NotificationKind.REPEATING_CLASSROOM_EVENT_CREATED_V1:
return self.adapt_repeating_classroom_event_created_v1(
cast(ClassroomScheduleFocusNotificationPayloadSchema, payload)
)
case NotificationKind.CLASSROOM_EVENT_REPETITION_UPDATED_V1:
return self.adapt_classroom_event_repetition_updated_v1(
cast(ClassroomScheduleFocusNotificationPayloadSchema, payload)
)
case NotificationKind.CLASSROOM_EVENT_REPETITION_CANCELLED_V1:
return self.adapt_classroom_event_repetition_cancelled_v1(
cast(ClassroomScheduleFocusNotificationPayloadSchema, payload)
)
case NotificationKind.CUSTOM_V1:
return self.adapt_custom_v1(
cast(CustomNotificationPayloadSchema, payload)
Expand Down
76 changes: 76 additions & 0 deletions app/notifications/services/adapters/email_message_adapter.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from app.common.schemas.notifications_sch import (
ClassroomEventInstanceNotificationPayloadSchema,
ClassroomNotificationPayloadSchema,
ClassroomScheduleFocusNotificationPayloadSchema,
CustomNotificationPayloadSchema,
EnrollmentNotificationPayloadSchema,
InvitationAcceptanceNotificationPayloadSchema,
Expand All @@ -11,7 +13,9 @@
CustomEmailMessagePayloadSchema,
EmailMessageKind,
RecipientInvoiceNotificationEmailMessagePayloadSchema,
UniversalEmailMessagePayloadSchema,
)
from app.notifications import texts
from app.notifications.services.adapters.base_adapter import BaseNotificationAdapter


Expand Down Expand Up @@ -78,6 +82,78 @@ def adapt_student_recipient_invoice_payment_confirmed_v1(
notification_id=self.notification.id,
)

def adapt_single_classroom_event_created_v1(
self, payload: ClassroomEventInstanceNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.SINGLE_CLASSROOM_EVENT_CREATED_V1_EMAIL_THEME,
pre_header=texts.SINGLE_CLASSROOM_EVENT_CREATED_V1_EMAIL_PRE_HEADER,
header=texts.SINGLE_CLASSROOM_EVENT_CREATED_V1_EMAIL_HEADER,
content=texts.SINGLE_CLASSROOM_EVENT_CREATED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_EVENT_INSTANCE_BUTTON_TEXT,
button_link=self.build_student_classroom_event_instance_url(payload),
)

def adapt_classroom_event_instance_rescheduled_v1(
self, payload: ClassroomEventInstanceNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1_EMAIL_THEME,
pre_header=texts.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1_EMAIL_PRE_HEADER,
header=texts.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1_EMAIL_HEADER,
content=texts.CLASSROOM_EVENT_INSTANCE_RESCHEDULED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_EVENT_INSTANCE_BUTTON_TEXT,
button_link=self.build_student_classroom_event_instance_url(payload),
)

def adapt_classroom_event_instance_cancelled_v1(
self, payload: ClassroomEventInstanceNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1_EMAIL_THEME,
pre_header=texts.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1_EMAIL_PRE_HEADER,
header=texts.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1_EMAIL_HEADER,
content=texts.CLASSROOM_EVENT_INSTANCE_CANCELLED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_EVENT_INSTANCE_BUTTON_TEXT,
button_link=self.build_student_classroom_event_instance_url(payload),
)

def adapt_repeating_classroom_event_created_v1(
self, payload: ClassroomScheduleFocusNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.REPEATING_CLASSROOM_EVENT_CREATED_V1_EMAIL_THEME,
pre_header=texts.REPEATING_CLASSROOM_EVENT_CREATED_V1_EMAIL_PRE_HEADER,
header=texts.REPEATING_CLASSROOM_EVENT_CREATED_V1_EMAIL_HEADER,
content=texts.REPEATING_CLASSROOM_EVENT_CREATED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_SCHEDULE_FOCUS_BUTTON_TEXT,
button_link=self.build_student_classroom_schedule_focus_url(payload),
)

def adapt_classroom_event_repetition_updated_v1(
self, payload: ClassroomScheduleFocusNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.CLASSROOM_EVENT_REPETITION_UPDATED_V1_EMAIL_THEME,
pre_header=texts.CLASSROOM_EVENT_REPETITION_UPDATED_V1_EMAIL_PRE_HEADER,
header=texts.CLASSROOM_EVENT_REPETITION_UPDATED_V1_EMAIL_HEADER,
content=texts.CLASSROOM_EVENT_REPETITION_UPDATED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_SCHEDULE_FOCUS_BUTTON_TEXT,
button_link=self.build_student_classroom_schedule_focus_url(payload),
)

def adapt_classroom_event_repetition_cancelled_v1(
self, payload: ClassroomScheduleFocusNotificationPayloadSchema
) -> UniversalEmailMessagePayloadSchema:
return UniversalEmailMessagePayloadSchema(
theme=texts.CLASSROOM_EVENT_REPETITION_CANCELLED_V1_EMAIL_THEME,
pre_header=texts.CLASSROOM_EVENT_REPETITION_CANCELLED_V1_EMAIL_PRE_HEADER,
header=texts.CLASSROOM_EVENT_REPETITION_CANCELLED_V1_EMAIL_HEADER,
content=texts.CLASSROOM_EVENT_REPETITION_CANCELLED_V1_EMAIL_CONTENT,
button_text=texts.CLASSROOM_SCHEDULE_FOCUS_BUTTON_TEXT,
button_link=self.build_student_classroom_schedule_focus_url(payload),
)

def adapt_custom_v1(
self, payload: CustomNotificationPayloadSchema
) -> CustomEmailMessagePayloadSchema:
Expand Down
Loading
Loading