diff --git a/e2e_config.test.json b/e2e_config.test.json index 31a819fd..cb6df82f 100644 --- a/e2e_config.test.json +++ b/e2e_config.test.json @@ -3,5 +3,7 @@ "accounts.seller.id": "SEL-7310-3075", "catalog.product.parameter_group.id": "PGR-7255-3950-0001", "catalog.product.parameter.id": "PAR-7255-3950-0016", - "accounts.account.id": "ACC-9042-0088" + "accounts.account.id": "ACC-9042-0088", + "accounts.buyer.account.id": "ACC-1086-6867", + "accounts.buyer.id": "BUY-1591-2112" } diff --git a/mpt_api_client/resources/accounts/buyers.py b/mpt_api_client/resources/accounts/buyers.py index 0e40c142..d9695b9e 100644 --- a/mpt_api_client/resources/accounts/buyers.py +++ b/mpt_api_client/resources/accounts/buyers.py @@ -1,10 +1,19 @@ +from typing import override + from mpt_api_client.http import AsyncService, Service from mpt_api_client.http.mixins import ( AsyncCollectionMixin, - AsyncManagedResourceMixin, + AsyncCreateWithIconMixin, + AsyncDeleteMixin, + AsyncGetMixin, + AsyncUpdateWithIconMixin, CollectionMixin, - ManagedResourceMixin, + CreateWithIconMixin, + DeleteMixin, + GetMixin, + UpdateWithIconMixin, ) +from mpt_api_client.http.types import FileTypes from mpt_api_client.models import Model from mpt_api_client.models.model import ResourceData from mpt_api_client.resources.accounts.mixins import ( @@ -30,16 +39,74 @@ class BuyersServiceConfig: class BuyersService( + CreateWithIconMixin[Buyer], + UpdateWithIconMixin[Buyer], + GetMixin[Buyer], + DeleteMixin, ActivatableMixin[Buyer], EnablableMixin[Buyer], ValidateMixin[Buyer], - ManagedResourceMixin[Buyer], CollectionMixin[Buyer], Service[Buyer], BuyersServiceConfig, ): """Buyers Service.""" + @override + def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "buyer", + icon_key: str = "logo", + ) -> Buyer: + """Create a buyer. + + Args: + resource_data (ResourceData): Buyer data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the buyer data. + icon_key: The key for the logo image. + + Returns: + Buyer: Created buyer + """ + return super().create( + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + + @override + def update( + self, + resource_id: str, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "buyer", + icon_key: str = "logo", + ) -> Buyer: + """Update a buyer. + + Args: + resource_id: Resource ID + resource_data (ResourceData): Buyer data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the buyer data. + icon_key: The key for the logo image. + + Returns: + Buyer: Updated buyer + """ + return super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + def synchronize(self, resource_id: str, resource_data: ResourceData | None = None) -> Buyer: """Synchronize a buyer. @@ -60,16 +127,74 @@ def transfer(self, resource_id: str, resource_data: ResourceData | None = None) class AsyncBuyersService( + AsyncCreateWithIconMixin[Buyer], + AsyncUpdateWithIconMixin[Buyer], + AsyncGetMixin[Buyer], + AsyncDeleteMixin, AsyncActivatableMixin[Buyer], AsyncEnablableMixin[Buyer], AsyncValidateMixin[Buyer], - AsyncManagedResourceMixin[Buyer], AsyncCollectionMixin[Buyer], AsyncService[Buyer], BuyersServiceConfig, ): """Async Buyers Service.""" + @override + async def create( + self, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "buyer", + icon_key: str = "logo", + ) -> Buyer: + """Create a buyer. + + Args: + resource_data (ResourceData): Buyer data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the buyer data. + icon_key: The key for the logo image. + + Returns: + Buyer: Created buyer + """ + return await super().create( + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + + @override + async def update( + self, + resource_id: str, + resource_data: ResourceData, + logo: FileTypes, + data_key: str = "buyer", + icon_key: str = "logo", + ) -> Buyer: + """Update a buyer. + + Args: + resource_id: Resource ID + resource_data (ResourceData): Buyer data. + logo: Logo image in jpg, png, GIF, etc. + data_key: The key for the buyer data. + icon_key: The key for the logo image. + + Returns: + Buyer: Updated buyer + """ + return await super().update( + resource_id=resource_id, + resource_data=resource_data, + icon=logo, + data_key=data_key, + icon_key=icon_key, + ) + async def synchronize( self, resource_id: str, resource_data: ResourceData | None = None ) -> Buyer: diff --git a/setup.cfg b/setup.cfg index 8297cbe4..06e1231e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ per-file-ignores = mpt_api_client/http/mixins.py: WPS202 WPS204 WPS235 mpt_api_client/resources/*: WPS215 mpt_api_client/models/model.py: WPS215 WPS110 - mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 + mpt_api_client/resources/accounts/*.py: WPS202 WPS215 WPS214 WPS235 mpt_api_client/resources/billing/*.py: WPS202 WPS204 WPS214 WPS215 mpt_api_client/resources/catalog/*.py: WPS110 WPS214 WPS215 WPS235 mpt_api_client/resources/catalog/products.py: WPS204 WPS214 WPS215 WPS235 @@ -45,8 +45,8 @@ per-file-ignores = tests/unit/http/test_service.py: WPS204 WPS202 tests/unit/http/test_mixins.py: WPS204 WPS202 WPS210 tests/unit/resources/catalog/test_products.py: WPS202 WPS210 + tests/unit/resources/accounts/*.py: WPS204 WPS202 WPS210 tests/unit/resources/*/test_mixins.py: WPS118 WPS202 WPS204 WPS235 - tests/unit/resources/accounts/test_users.py: WPS204 WPS202 WPS210 tests/unit/test_mpt_client.py: WPS235 tests/e2e/accounts/*.py: WPS430 WPS202 tests/e2e/catalog/*.py: WPS421 diff --git a/tests/e2e/accounts/buyers/test_async_buyers.py b/tests/e2e/accounts/buyers/test_async_buyers.py new file mode 100644 index 00000000..c1abdcf5 --- /dev/null +++ b/tests/e2e/accounts/buyers/test_async_buyers.py @@ -0,0 +1,115 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +async def async_created_buyer(async_mpt_ops, buyer, buyer_account_id, account_icon): + new_buyer_request_data = buyer( + name="E2E Created Buyer", + account_id=buyer_account_id, + ) + + new_buyer = await async_mpt_ops.accounts.buyers.create( + new_buyer_request_data, logo=account_icon + ) + + yield new_buyer + + try: + await async_mpt_ops.accounts.buyers.delete(new_buyer.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete buyer: {error.title}") # noqa: WPS421 + + +async def test_get_buyer_by_id(async_mpt_ops, buyer_id): + buyer = await async_mpt_ops.accounts.buyers.get(buyer_id) + assert buyer is not None + + +async def test_list_buyers(async_mpt_ops): + limit = 10 + buyers = await async_mpt_ops.accounts.buyers.fetch_page(limit=limit) + assert len(buyers) > 0 + + +async def test_get_buyer_by_id_not_found(async_mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.buyers.get(invalid_buyer_id) + + +async def test_filter_buyers(async_mpt_ops, buyer_id): + select_fields = ["-address"] + + async_filtered_buyers = ( + async_mpt_ops.accounts.buyers.filter(RQLQuery(id=buyer_id)) + .filter(RQLQuery(name="E2E Seeded Buyer")) + .select(*select_fields) + ) + + buyers = [filtered_buyer async for filtered_buyer in async_filtered_buyers.iterate()] + + assert len(buyers) == 1 + + +def test_create_buyer(async_created_buyer): + assert async_created_buyer is not None + + +async def test_delete_buyer(async_mpt_ops, async_created_buyer): + await async_mpt_ops.accounts.buyers.delete(async_created_buyer.id) + + +async def test_delete_buyer_not_found(async_mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.buyers.delete(invalid_buyer_id) + + +async def test_update_buyer( + async_mpt_ops, buyer, buyer_account_id, account_icon, async_created_buyer +): + updated_buyer_data = buyer(name="E2E Updated Buyer", account_id=buyer_account_id) + + updated_buyer = await async_mpt_ops.accounts.buyers.update( + async_created_buyer.id, updated_buyer_data, logo=account_icon + ) + + assert updated_buyer is not None + + +async def test_update_buyer_not_found( + async_mpt_ops, buyer, buyer_account_id, account_icon, invalid_buyer_id +): + updated_buyer_data = buyer(name="Nonexistent Buyer", account_id=buyer_account_id) + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.buyers.update( + invalid_buyer_id, updated_buyer_data, logo=account_icon + ) + + +async def test_buyer_disable(async_mpt_ops, async_created_buyer): + disabled_buyer = await async_mpt_ops.accounts.buyers.disable(async_created_buyer.id) + + assert disabled_buyer is not None + + +async def test_buyer_disable_not_found(async_mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.buyers.disable(invalid_buyer_id) + + +async def test_buyer_enable(async_mpt_ops, async_created_buyer): + await async_mpt_ops.accounts.buyers.disable(async_created_buyer.id) + + enabled_buyer = await async_mpt_ops.accounts.buyers.enable(async_created_buyer.id) + + assert enabled_buyer is not None + + +async def test_buyer_enable_not_found(async_mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + await async_mpt_ops.accounts.buyers.enable(invalid_buyer_id) diff --git a/tests/e2e/accounts/buyers/test_sync_buyers.py b/tests/e2e/accounts/buyers/test_sync_buyers.py new file mode 100644 index 00000000..3c7c88fa --- /dev/null +++ b/tests/e2e/accounts/buyers/test_sync_buyers.py @@ -0,0 +1,108 @@ +import pytest + +from mpt_api_client.exceptions import MPTAPIError +from mpt_api_client.rql.query_builder import RQLQuery + +pytestmark = [pytest.mark.flaky] + + +@pytest.fixture +def created_buyer(mpt_ops, buyer, buyer_account_id, account_icon): + new_buyer_request_data = buyer( + name="E2E Created Buyer", + account_id=buyer_account_id, + ) + + new_buyer = mpt_ops.accounts.buyers.create(new_buyer_request_data, logo=account_icon) + + yield new_buyer + + try: + mpt_ops.accounts.buyers.delete(new_buyer.id) + except MPTAPIError as error: + print(f"TEARDOWN - Unable to delete buyer: {error.title}") # noqa: WPS421 + + +def test_get_buyer_by_id(mpt_ops, buyer_id): + buyer = mpt_ops.accounts.buyers.get(buyer_id) + assert buyer is not None + + +def test_list_buyers(mpt_ops): + limit = 10 + buyers = mpt_ops.accounts.buyers.fetch_page(limit=limit) + assert len(buyers) > 0 + + +def test_get_buyer_by_id_not_found(mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.buyers.get(invalid_buyer_id) + + +def test_filter_buyers(mpt_ops, buyer_id): + select_fields = ["-address"] + + filtered_buyers = ( + mpt_ops.accounts.buyers.filter(RQLQuery(id=buyer_id)) + .filter(RQLQuery(name="E2E Seeded Buyer")) + .select(*select_fields) + ) + + buyers = list(filtered_buyers.iterate()) + + assert len(buyers) == 1 + + +def test_create_buyer(created_buyer): + new_buyer = created_buyer + assert new_buyer is not None + + +def test_delete_buyer(mpt_ops, created_buyer): + mpt_ops.accounts.buyers.delete(created_buyer.id) + + +def test_delete_buyer_not_found(mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.buyers.delete(invalid_buyer_id) + + +def test_update_buyer(mpt_ops, buyer, buyer_account_id, account_icon, created_buyer): + updated_buyer_data = buyer(name="E2E Updated Buyer", account_id=buyer_account_id) + + updated_buyer = mpt_ops.accounts.buyers.update( + created_buyer.id, updated_buyer_data, logo=account_icon + ) + + assert updated_buyer is not None + + +def test_update_buyer_not_found(mpt_ops, buyer, buyer_account_id, account_icon, invalid_buyer_id): + updated_buyer_data = buyer(name="Nonexistent Buyer", account_id=buyer_account_id) + + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.buyers.update(invalid_buyer_id, updated_buyer_data, logo=account_icon) + + +def test_buyer_disable(mpt_ops, created_buyer): + disabled_buyer = mpt_ops.accounts.buyers.disable(created_buyer.id) + + assert disabled_buyer is not None + + +def test_buyer_disable_not_found(mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.buyers.disable(invalid_buyer_id) + + +def test_buyer_enable(mpt_ops, created_buyer): + mpt_ops.accounts.buyers.disable(created_buyer.id) + + enabled_buyer = mpt_ops.accounts.buyers.enable(created_buyer.id) + + assert enabled_buyer is not None + + +def test_buyer_enable_not_found(mpt_ops, invalid_buyer_id): + with pytest.raises(MPTAPIError, match=r"404 Not Found"): + mpt_ops.accounts.buyers.enable(invalid_buyer_id) diff --git a/tests/e2e/accounts/conftest.py b/tests/e2e/accounts/conftest.py index 5a451125..f1657e73 100644 --- a/tests/e2e/accounts/conftest.py +++ b/tests/e2e/accounts/conftest.py @@ -62,3 +62,31 @@ def _account( } return _account + + +@pytest.fixture +def buyer(buyer_account_id): + def _buyer( + name="E2E Created Buyer", + account_id: str = buyer_account_id, + ): + return { + "name": name, + "account": { + "id": account_id, + }, + "contact": { + "firstName": "first", + "lastName": "last", + "email": "created.buyer@example.com", + }, + "address": { + "addressLine1": "123 Main St", + "city": "Anytown", + "state": "CA", + "postCode": "12345", + "country": "US", + }, + } + + return _buyer diff --git a/tests/e2e/accounts/logo.png b/tests/e2e/accounts/logo.png new file mode 100644 index 00000000..ab9b8cfa Binary files /dev/null and b/tests/e2e/accounts/logo.png differ diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 5046e2d4..075c1cc1 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -98,3 +98,18 @@ def account_id(e2e_config): @pytest.fixture def invalid_account_id(): return "ACC-0000-0000" + + +@pytest.fixture +def invalid_buyer_id(): + return "BUY-0000-0000" + + +@pytest.fixture +def buyer_id(e2e_config): + return e2e_config["accounts.buyer.id"] + + +@pytest.fixture +def buyer_account_id(e2e_config): + return e2e_config["accounts.buyer.account.id"] diff --git a/tests/unit/resources/accounts/test_buyers.py b/tests/unit/resources/accounts/test_buyers.py index f1991d5a..a05dc723 100644 --- a/tests/unit/resources/accounts/test_buyers.py +++ b/tests/unit/resources/accounts/test_buyers.py @@ -159,3 +159,99 @@ async def test_async_buyers_resource_action_no_data(async_buyers_service, action assert request.content == request_expected_content assert buyers_obj.to_dict() == response_expected_data assert isinstance(buyers_obj, Buyer) + + +def test_buyers_create(buyers_service, tmp_path): # noqa: WPS210 + buyer_data = { + "id": "BUY-0000-0001", + "name": "Test Buyer", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.post(buyers_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=buyer_data) + ) + + buyer = buyers_service.create(buyer_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "POST" + assert request.url.path == "/public/v1/accounts/buyers" + assert buyer.to_dict() == buyer_data + + +def test_buyers_update(buyers_service, tmp_path): # noqa: WPS210 + buyer_id = "BUY-0000-0001" + buyer_data = { + "name": "Updated Test Buyer", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.put(f"{buyers_service.path}/{buyer_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": buyer_id, **buyer_data}) + ) + + buyer = buyers_service.update(buyer_id, buyer_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "PUT" + assert request.url.path == f"/public/v1/accounts/buyers/{buyer_id}" + assert buyer.to_dict() == {"id": buyer_id, **buyer_data} + + +async def test_async_buyers_create(async_buyers_service, tmp_path): # noqa: WPS210 + buyer_data = { + "id": "BUY-0000-0001", + "name": "Test Buyer", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.post(async_buyers_service.path).mock( + return_value=httpx.Response(httpx.codes.CREATED, json=buyer_data) + ) + + buyer = await async_buyers_service.create(buyer_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "POST" + assert request.url.path == "/public/v1/accounts/buyers" + assert buyer.to_dict() == buyer_data + + +async def test_async_buyers_update(async_buyers_service, tmp_path): # noqa: WPS210 + buyer_id = "BUY-0000-0001" + buyer_data = { + "name": "Updated Test Buyer", + } + + logo_path = tmp_path / "logo.png" + logo_path.write_bytes(b"fake-logo-data") + + with logo_path.open("rb") as logo_file, respx.mock: + mock_route = respx.put(f"{async_buyers_service.path}/{buyer_id}").mock( + return_value=httpx.Response(httpx.codes.OK, json={"id": buyer_id, **buyer_data}) + ) + + buyer = await async_buyers_service.update(buyer_id, buyer_data, logo=logo_file) + + request = mock_route.calls[0].request + + assert mock_route.call_count == 1 + assert request.method == "PUT" + assert request.url.path == f"/public/v1/accounts/buyers/{buyer_id}" + assert buyer.to_dict() == {"id": buyer_id, **buyer_data}