diff --git a/mpt_api_client/resources/notifications/notifications.py b/mpt_api_client/resources/notifications/notifications.py index 3dcacb22..34c7b78f 100644 --- a/mpt_api_client/resources/notifications/notifications.py +++ b/mpt_api_client/resources/notifications/notifications.py @@ -16,6 +16,10 @@ AsyncSubscribersService, SubscribersService, ) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) from mpt_api_client.resources.notifications.webhooks import ( AsyncWebhooksService, WebhooksService, @@ -80,6 +84,11 @@ def subscribers(self) -> SubscribersService: """Subscriptions service.""" return SubscribersService(http_client=self.http_client) + @property + def templates(self) -> TemplatesService: + """Templates service.""" + return TemplatesService(http_client=self.http_client) + @property def webhooks(self) -> WebhooksService: """Webhooks service.""" @@ -144,6 +153,11 @@ def subscribers(self) -> AsyncSubscribersService: """Subscriptions service.""" return AsyncSubscribersService(http_client=self.http_client) + @property + def templates(self) -> AsyncTemplatesService: + """Async Templates service.""" + return AsyncTemplatesService(http_client=self.http_client) + @property def webhooks(self) -> AsyncWebhooksService: """Async Webhooks service.""" diff --git a/mpt_api_client/resources/notifications/template_variants.py b/mpt_api_client/resources/notifications/template_variants.py new file mode 100644 index 00000000..753d1489 --- /dev/null +++ b/mpt_api_client/resources/notifications/template_variants.py @@ -0,0 +1,73 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncDisableMixin, + AsyncManagedResourceMixin, + CollectionMixin, + DisableMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model, ResourceData +from mpt_api_client.models.model import BaseModel + + +class TemplateVariant(Model): + """Notifications Template Variant resource. + + Attributes: + body: Body content of the variant. + default: Whether this variant is the default one of its template. + language_code: Language code of the variant. + template: Reference to the parent template. + status: Variant status. + subject: Subject of the messages created from this variant. + audit: Audit information (created, updated events). + """ + + body: str | None + default: bool | None + language_code: str | None + template: BaseModel | None + status: str | None + subject: str | None + audit: BaseModel | None + + +class TemplateVariantsServiceConfig: + """Notifications Template Variants service configuration.""" + + _endpoint = "/public/v1/notifications/templates/{template_id}/variants" + _model_class = TemplateVariant + _collection_key = "data" + + +class TemplateVariantsService( + DisableMixin[TemplateVariant], + ManagedResourceMixin[TemplateVariant], + CollectionMixin[TemplateVariant], + Service[TemplateVariant], + TemplateVariantsServiceConfig, +): + """Notifications Template Variants service.""" + + def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> TemplateVariant: + """Switch template variant to active state.""" + return self._resource(resource_id).post("activate", json=resource_data) + + +class AsyncTemplateVariantsService( + AsyncDisableMixin[TemplateVariant], + AsyncManagedResourceMixin[TemplateVariant], + AsyncCollectionMixin[TemplateVariant], + AsyncService[TemplateVariant], + TemplateVariantsServiceConfig, +): + """Async Notifications Template Variants service.""" + + async def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> TemplateVariant: + """Switch template variant to active state.""" + return await self._resource(resource_id).post("activate", json=resource_data) diff --git a/mpt_api_client/resources/notifications/templates.py b/mpt_api_client/resources/notifications/templates.py new file mode 100644 index 00000000..9315faa7 --- /dev/null +++ b/mpt_api_client/resources/notifications/templates.py @@ -0,0 +1,117 @@ +from mpt_api_client.http import AsyncService, Service +from mpt_api_client.http.mixins import ( + AsyncCollectionMixin, + AsyncDisableMixin, + AsyncManagedResourceMixin, + CollectionMixin, + DisableMixin, + ManagedResourceMixin, +) +from mpt_api_client.models import Model, ResourceData +from mpt_api_client.models.model import BaseModel +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) + + +class Template(Model): + """Notifications Template resource. + + Attributes: + name: Template name. + category: Reference to the notification category of the template. + criteria: Criteria triggering the template automatically. + default_variant: Reference to the default variant of the template. + description: Template description. + last_used: Timestamp of the last time the template was used. + owner: Reference to the account owning the template. + schedule: Reference to the schedule triggering the template. + statistics: Usage statistics of the template. + status: Template status. + type: How the template is triggered (Event, Scheduled or Manual). + variants: References to the language-specific variants of the template. + external_id: External identifier of the template. + audit: Audit information (created, updated events). + """ + + name: str | None + category: BaseModel | None + criteria: BaseModel | None + default_variant: BaseModel | None + description: str | None + last_used: str | None + owner: BaseModel | None + schedule: BaseModel | None + statistics: BaseModel | None + status: str | None + type: str | None + variants: list[BaseModel] | None + external_id: str | None + audit: BaseModel | None + + +class TemplatesServiceConfig: + """Notifications Templates service configuration.""" + + _endpoint = "/public/v1/notifications/templates" + _model_class = Template + _collection_key = "data" + + +class TemplatesService( + DisableMixin[Template], + ManagedResourceMixin[Template], + CollectionMixin[Template], + Service[Template], + TemplatesServiceConfig, +): + """Notifications Templates service.""" + + def activate(self, resource_id: str, resource_data: ResourceData | None = None) -> Template: + """Switch template to active state.""" + return self._resource(resource_id).post("activate", json=resource_data) + + def variants(self, template_id: str) -> TemplateVariantsService: + """Access template variants service. + + Args: + template_id: Template ID. + + Returns: + TemplateVariantsService + """ + return TemplateVariantsService( + http_client=self.http_client, + endpoint_params={"template_id": template_id}, + ) + + +class AsyncTemplatesService( + AsyncDisableMixin[Template], + AsyncManagedResourceMixin[Template], + AsyncCollectionMixin[Template], + AsyncService[Template], + TemplatesServiceConfig, +): + """Async Notifications Templates service.""" + + async def activate( + self, resource_id: str, resource_data: ResourceData | None = None + ) -> Template: + """Switch template to active state.""" + return await self._resource(resource_id).post("activate", json=resource_data) + + def variants(self, template_id: str) -> AsyncTemplateVariantsService: + """Access async template variants service. + + Args: + template_id: Template ID. + + Returns: + AsyncTemplateVariantsService + """ + return AsyncTemplateVariantsService( + http_client=self.http_client, + endpoint_params={"template_id": template_id}, + ) diff --git a/tests/unit/resources/notifications/test_notifications.py b/tests/unit/resources/notifications/test_notifications.py index 994eb8f6..921d1f36 100644 --- a/tests/unit/resources/notifications/test_notifications.py +++ b/tests/unit/resources/notifications/test_notifications.py @@ -18,6 +18,10 @@ AsyncSubscribersService, SubscribersService, ) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) from mpt_api_client.resources.notifications.webhooks import ( AsyncWebhooksService, WebhooksService, @@ -48,6 +52,7 @@ def test_async_notifications_init(async_http_client): ("subscribers", SubscribersService), ("directories", DirectoriesService), ("footers", FootersService), + ("templates", TemplatesService), ("webhooks", WebhooksService), ], ) @@ -69,6 +74,7 @@ def test_notifications_properties(http_client, attr_name, expected): ("subscribers", AsyncSubscribersService), ("directories", AsyncDirectoriesService), ("footers", AsyncFootersService), + ("templates", AsyncTemplatesService), ("webhooks", AsyncWebhooksService), ], ) diff --git a/tests/unit/resources/notifications/test_template_variants.py b/tests/unit/resources/notifications/test_template_variants.py new file mode 100644 index 00000000..62756d2d --- /dev/null +++ b/tests/unit/resources/notifications/test_template_variants.py @@ -0,0 +1,125 @@ +import httpx +import pytest +import respx + +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) + +TEMPLATE_ID = "NTL-1234" +VARIANTS_PATH = f"/public/v1/notifications/templates/{TEMPLATE_ID}/variants" + + +@pytest.fixture +def template_variants_service(http_client): + return TemplateVariantsService( + http_client=http_client, endpoint_params={"template_id": TEMPLATE_ID} + ) + + +@pytest.fixture +def async_template_variants_service(async_http_client): + return AsyncTemplateVariantsService( + http_client=async_http_client, endpoint_params={"template_id": TEMPLATE_ID} + ) + + +@pytest.fixture +def template_variant_data(): + return { + "id": "NTV-1234", + "body": "Hello", + "default": True, + "languageCode": "en-US", + "subject": "Order confirmed", + "status": "Active", + "template": {"id": TEMPLATE_ID, "name": "Order confirmation"}, + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint_contains_template_id(template_variants_service): + result = template_variants_service.build_path() + + assert result == VARIANTS_PATH + + +def test_async_endpoint_contains_template_id(async_template_variants_service): + result = async_template_variants_service.build_path() + + assert result == VARIANTS_PATH + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_mixins_present(template_variants_service, method): + result = hasattr(template_variants_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_async_mixins_present(async_template_variants_service, method): + result = hasattr(async_template_variants_service, method) + + assert result is True + + +def test_get_template_variant(template_variants_service, template_variant_data): + with respx.mock: + mock_route = respx.get(f"https://api.example.com{VARIANTS_PATH}/NTV-1234").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=template_variant_data, + ) + ) + + result = template_variants_service.get("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == template_variant_data + assert result.language_code == "en-US" + assert result.template.id == TEMPLATE_ID + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +def test_template_variant_state_actions(template_variants_service, action): + response_expected_data = {"id": "NTV-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post(f"https://api.example.com{VARIANTS_PATH}/NTV-1234/{action}").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(template_variants_service, action)("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +async def test_async_template_variant_state_actions(async_template_variants_service, action): + response_expected_data = {"id": "NTV-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post(f"https://api.example.com{VARIANTS_PATH}/NTV-1234/{action}").mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_template_variants_service, action)("NTV-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data diff --git a/tests/unit/resources/notifications/test_templates.py b/tests/unit/resources/notifications/test_templates.py new file mode 100644 index 00000000..eb23b476 --- /dev/null +++ b/tests/unit/resources/notifications/test_templates.py @@ -0,0 +1,150 @@ +import httpx +import pytest +import respx + +from mpt_api_client.resources.notifications.template_variants import ( + AsyncTemplateVariantsService, + TemplateVariantsService, +) +from mpt_api_client.resources.notifications.templates import ( + AsyncTemplatesService, + TemplatesService, +) + + +@pytest.fixture +def templates_service(http_client): + return TemplatesService(http_client=http_client) + + +@pytest.fixture +def async_templates_service(async_http_client): + return AsyncTemplatesService(http_client=async_http_client) + + +@pytest.fixture +def template_data(): + return { + "id": "NTL-1234", + "name": "Order confirmation", + "description": "Sent when an order is completed", + "status": "Active", + "type": "Event", + "externalId": "order-confirmation", + "lastUsed": "2024-01-01T00:00:00Z", + "category": {"id": "NCA-1234", "name": "Orders"}, + "defaultVariant": {"id": "NTV-1234", "languageCode": "en-US"}, + "owner": {"id": "ACC-1234", "name": "Account"}, + "variants": [{"id": "NTV-1234", "languageCode": "en-US"}], + "audit": {"created": {"at": "2024-01-01T00:00:00Z"}}, + } + + +def test_endpoint(templates_service): + result = templates_service.build_path() + + assert result == "/public/v1/notifications/templates" + + +def test_async_endpoint(async_templates_service): + result = async_templates_service.build_path() + + assert result == "/public/v1/notifications/templates" + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_mixins_present(templates_service, method): + result = hasattr(templates_service, method) + + assert result is True + + +@pytest.mark.parametrize( + "method", + ["get", "create", "update", "delete", "activate", "disable", "iterate", "fetch_page"], +) +def test_async_mixins_present(async_templates_service, method): + result = hasattr(async_templates_service, method) + + assert result is True + + +def test_get_template(templates_service, template_data): + with respx.mock: + mock_route = respx.get( + "https://api.example.com/public/v1/notifications/templates/NTL-1234" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=template_data, + ) + ) + + result = templates_service.get("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == template_data + assert result.external_id == "order-confirmation" + assert result.default_variant.id == "NTV-1234" + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +def test_template_state_actions(templates_service, action): + response_expected_data = {"id": "NTL-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/notifications/templates/NTL-1234/{action}" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = getattr(templates_service, action)("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +@pytest.mark.parametrize("action", ["activate", "disable"]) +async def test_async_template_state_actions(async_templates_service, action): + response_expected_data = {"id": "NTL-1234", "status": "Active"} + with respx.mock: + mock_route = respx.post( + f"https://api.example.com/public/v1/notifications/templates/NTL-1234/{action}" + ).mock( + return_value=httpx.Response( + status_code=200, + headers={"content-type": "application/json"}, + json=response_expected_data, + ) + ) + + result = await getattr(async_templates_service, action)("NTL-1234") + + assert mock_route.call_count == 1 + assert result.to_dict() == response_expected_data + + +def test_variants_accessor(templates_service): + result = templates_service.variants("NTL-1234") + + assert isinstance(result, TemplateVariantsService) + assert result.http_client is templates_service.http_client + assert result.endpoint_params == {"template_id": "NTL-1234"} + assert result.build_path() == "/public/v1/notifications/templates/NTL-1234/variants" + + +def test_async_variants_accessor(async_templates_service): + result = async_templates_service.variants("NTL-1234") + + assert isinstance(result, AsyncTemplateVariantsService) + assert result.http_client is async_templates_service.http_client + assert result.endpoint_params == {"template_id": "NTL-1234"} + assert result.build_path() == "/public/v1/notifications/templates/NTL-1234/variants"