From 4803f64f10ea4b1ec80805b2ad3744b844dbc3c3 Mon Sep 17 00:00:00 2001 From: David Weterings Date: Wed, 15 Jul 2020 11:37:32 +0200 Subject: [PATCH 1/4] Add product testing actions for prices --- src/commercetools/testing/products.py | 80 +++++++++++++++++++++------ src/commercetools/testing/utils.py | 13 ++++- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/commercetools/testing/products.py b/src/commercetools/testing/products.py index d829590a..de9dd668 100644 --- a/src/commercetools/testing/products.py +++ b/src/commercetools/testing/products.py @@ -7,6 +7,7 @@ from marshmallow import fields as schema_fields from commercetools import schemas, types +from commercetools.testing import utils from commercetools.testing.abstract import BaseModel, ServiceBackend from commercetools.testing.utils import ( create_commercetools_response, @@ -258,28 +259,27 @@ def updater(self, obj: dict, action: types.ProductPublishAction): return updater +def convert_draft_price(price_draft: types.PriceDraft, price_id: str=None) -> types.Price: + return types.Price( + id=price_id or str(uuid.uuid4()), + country=price_draft.country, + channel=price_draft.channel, + value=utils._money_to_typed(price_draft.value), + valid_from=price_draft.valid_from, + valid_until=price_draft.valid_until, + discounted=price_draft.discounted, + custom=utils.create_from_draft(price_draft.custom), + tiers=[utils.create_from_draft(tier) for tier in price_draft.tiers], + ) + + def _set_product_prices(): def updater(self, obj: dict, action: types.ProductSetPricesAction): new = copy.deepcopy(obj) target_obj = _get_target_obj(new, getattr(action, "staged", True)) prices = [] for price_draft in action.prices: - price = types.Price( - id=str(uuid.uuid4()), - country=price_draft.country, - channel=price_draft.channel, - value=types.TypedMoney( - fraction_digits=2, - cent_amount=price_draft.value.cent_amount, - currency_code=price_draft.value.currency_code, - type=types.MoneyType.CENT_PRECISION, - ), - valid_from=price_draft.valid_from, - valid_until=price_draft.valid_until, - discounted=price_draft.discounted, - custom=price_draft.custom, - tiers=price_draft.tiers, - ) + price = convert_draft_price(price_draft) prices.append(price) schema = schemas.PriceSchema() @@ -293,6 +293,52 @@ def updater(self, obj: dict, action: types.ProductSetPricesAction): return updater +def _change_price(): + def updater(self, obj: dict, action: types.ProductChangePriceAction): + new = copy.deepcopy(obj) + target_obj = _get_target_obj(new, getattr(action, "staged", True)) + changed_price = convert_draft_price(action.price, action.price_id) + schema = schemas.PriceSchema() + + found_price = True + for variant in get_product_variants(target_obj): + for index, price in enumerate(variant["prices"]): + if price["id"] == action.price_id: + variant["prices"][index] = schema.dump(changed_price) + found_price = True + break + if not found_price: + raise ValueError("Could not find price with id %s" % action.price_id) + if action.staged: + new["masterData"]["hasStagedChanges"] = True + return new + return updater + + +def _add_price(): + def updater(self, obj: dict, action: types.ProductAddPriceAction): + new = copy.deepcopy(obj) + target_obj = _get_target_obj(new, getattr(action, "staged", True)) + new_price = convert_draft_price(action.price) + schema = schemas.PriceSchema() + + found_sku = False + for variant in get_product_variants(target_obj): + if variant["sku"] == action.sku: + if "prices" not in variant: + variant["prices"] = [] + variant["prices"] += schema.dump(new_price) + found_sku = True + break + if not found_sku: + raise ValueError("Could not find sku %s" % action.sku) + if action.staged: + new["masterData"]["hasStagedChanges"] = True + return new + + return updater + + class UploadImageQuerySchema(Schema): staged = schema_fields.Bool() filename = schema_fields.Field() @@ -325,6 +371,8 @@ def urls(self): "setAttribute": _set_attribute_action(), "addVariant": _add_variant_action(), "setPrices": _set_product_prices(), + "changePrice": _change_price(), + "addPrice": _add_price(), "publish": _publish_product_action(), } diff --git a/src/commercetools/testing/utils.py b/src/commercetools/testing/utils.py index 3219bf56..e049a4e9 100644 --- a/src/commercetools/testing/utils.py +++ b/src/commercetools/testing/utils.py @@ -51,7 +51,18 @@ def create_from_draft(draft): return None if isinstance(draft, types.CustomFieldsDraft): - return types.CustomFields(type=draft.type, fields=draft.fields) + return types.CustomFields( + type=types.TypeReference(type_id=draft.type.type_id, id=draft.type.id), + fields=draft.fields + ) + if isinstance(draft, types.PriceTierDraft): + return types.PriceTier( + minimum_quantity=draft.minimum_quantity, + value=_money_to_typed(types.Money( + cent_amount=draft.value.cent_amount, + currency_code=draft.value.currency_code, + )), + ) raise ValueError(f"Unsupported type {draft.__class__}") From e6f629c0422a436c039719320817216c77d1c6b9 Mon Sep 17 00:00:00 2001 From: David Weterings Date: Wed, 15 Jul 2020 12:16:04 +0200 Subject: [PATCH 2/4] Add testing addPrice / changePrice actions --- CHANGES | 3 +- src/commercetools/testing/products.py | 59 ++++++++++++--------- tests/test_service_products.py | 74 +++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 24 deletions(-) diff --git a/CHANGES b/CHANGES index b295d510..0b6d8413 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,7 @@ -8.1.5 (xxxx-xx-xx) +8.1.5 (2020-07-15) ------------------ - Fixed API extensions endpoints +- Testing: add product change/add price actions 8.1.4 (2020-06-11) diff --git a/src/commercetools/testing/products.py b/src/commercetools/testing/products.py index de9dd668..3264149f 100644 --- a/src/commercetools/testing/products.py +++ b/src/commercetools/testing/products.py @@ -1,7 +1,7 @@ import copy import datetime -import typing import uuid +from typing import Optional, List, Union from marshmallow import Schema from marshmallow import fields as schema_fields @@ -23,7 +23,7 @@ class ProductsModel(BaseModel): _unique_values = ["key"] def _create_from_draft( - self, draft: types.ProductDraft, id: typing.Optional[str] = None + self, draft: types.ProductDraft, id: Optional[str] = None ) -> types.Product: object_id = str(uuid.UUID(id) if id is not None else uuid.uuid4()) @@ -64,11 +64,11 @@ def _create_variant_from_draft( self, draft: types.ProductVariantDraft ) -> types.ProductVariant: - assets: typing.Optional[typing.List[types.Asset]] = None + assets: Optional[List[types.Asset]] = None if draft.assets: assets = self._create_assets_from_draft(draft.assets) - prices: typing.Optional[typing.List[types.Price]] = None + prices: Optional[List[types.Price]] = None if draft.prices: prices = self._create_prices_from_draft(draft.prices) @@ -88,11 +88,11 @@ def _create_variant_from_draft( ) def _create_assets_from_draft( - self, drafts: typing.List[types.AssetDraft] - ) -> typing.List[types.Asset]: - assets: typing.List[types.Asset] = [] + self, drafts: List[types.AssetDraft] + ) -> List[types.Asset]: + assets: List[types.Asset] = [] for draft in drafts: - custom: typing.Optional[types.CustomFields] = None + custom: Optional[types.CustomFields] = None if draft.custom: custom = custom_fields_from_draft(self._storage, draft.custom) @@ -108,9 +108,9 @@ def _create_assets_from_draft( return assets def _create_prices_from_draft( - self, drafts: typing.List[types.PriceDraft] - ) -> typing.List[types.Price]: - prices: typing.List[types.Price] = [] + self, drafts: List[types.PriceDraft] + ) -> List[types.Price]: + prices: List[types.Price] = [] for draft in drafts: custom = None if draft.custom: @@ -131,8 +131,8 @@ def _create_prices_from_draft( return prices def _create_price_from_draft( - self, draft: typing.Optional[types.TypedMoneyDraft] - ) -> typing.Optional[types.TypedMoney]: + self, draft: Union[types.Money, Optional[types.TypedMoneyDraft]] + ) -> Optional[types.TypedMoney]: if draft is None: return None @@ -164,9 +164,7 @@ def _get_target_obj(obj: dict, staged: bool): return obj["masterData"]["staged"] -def _get_variant( - data_object: dict, *, id: str = "", sku: str = "" -) -> typing.Optional[dict]: +def _get_variant(data_object: dict, *, id: int = "", sku: str = "") -> Optional[dict]: if not data_object: return None @@ -252,6 +250,7 @@ def updater(self, obj: dict, action: types.ProductPublishAction): # not implemented scopes right now. if new["masterData"].get("staged"): new["masterData"]["current"] = new["masterData"]["staged"] + del new["masterData"]["staged"] new["masterData"]["hasStagedChanges"] = False new["masterData"]["published"] = True return new @@ -259,7 +258,12 @@ def updater(self, obj: dict, action: types.ProductPublishAction): return updater -def convert_draft_price(price_draft: types.PriceDraft, price_id: str=None) -> types.Price: +def convert_draft_price( + price_draft: types.PriceDraft, price_id: str = None +) -> types.Price: + tiers: Optional[List[types.PriceTier]] = None + if price_draft.tiers: + tiers = [utils.create_from_draft(tier) for tier in price_draft.tiers] return types.Price( id=price_id or str(uuid.uuid4()), country=price_draft.country, @@ -269,7 +273,7 @@ def convert_draft_price(price_draft: types.PriceDraft, price_id: str=None) -> ty valid_until=price_draft.valid_until, discounted=price_draft.discounted, custom=utils.create_from_draft(price_draft.custom), - tiers=[utils.create_from_draft(tier) for tier in price_draft.tiers], + tiers=tiers, ) @@ -296,7 +300,10 @@ def updater(self, obj: dict, action: types.ProductSetPricesAction): def _change_price(): def updater(self, obj: dict, action: types.ProductChangePriceAction): new = copy.deepcopy(obj) - target_obj = _get_target_obj(new, getattr(action, "staged", True)) + staged = action.staged + if staged is None: + staged = True + target_obj = _get_target_obj(new, staged) changed_price = convert_draft_price(action.price, action.price_id) schema = schemas.PriceSchema() @@ -309,16 +316,20 @@ def updater(self, obj: dict, action: types.ProductChangePriceAction): break if not found_price: raise ValueError("Could not find price with id %s" % action.price_id) - if action.staged: + if staged: new["masterData"]["hasStagedChanges"] = True return new + return updater def _add_price(): def updater(self, obj: dict, action: types.ProductAddPriceAction): new = copy.deepcopy(obj) - target_obj = _get_target_obj(new, getattr(action, "staged", True)) + staged = action.staged + if staged is None: + staged = True + target_obj = _get_target_obj(new, staged) new_price = convert_draft_price(action.price) schema = schemas.PriceSchema() @@ -327,12 +338,14 @@ def updater(self, obj: dict, action: types.ProductAddPriceAction): if variant["sku"] == action.sku: if "prices" not in variant: variant["prices"] = [] - variant["prices"] += schema.dump(new_price) + elif not variant["prices"]: + variant["prices"] = [] + variant["prices"].append(schema.dump(new_price)) found_sku = True break if not found_sku: raise ValueError("Could not find sku %s" % action.sku) - if action.staged: + if staged: new["masterData"]["hasStagedChanges"] = True return new diff --git a/tests/test_service_products.py b/tests/test_service_products.py index 5581fa46..ec25cdca 100644 --- a/tests/test_service_products.py +++ b/tests/test_service_products.py @@ -197,3 +197,77 @@ def test_product_update(client): ], ) assert product.key == "test-product" + + +def test_product_update_add_change_price_staged(client): + product = client.products.create( + types.ProductDraft( + key="test-product", + master_variant=types.ProductVariantDraft(sku="1", key="1"), + ) + ) + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductAddPriceAction( + sku="1", + price=types.PriceDraft( + value=types.Money(cent_amount=1000, currency_code="GBP") + ), + ) + ], + ) + + assert product.master_data.current is None + assert len(product.master_data.staged.master_variant.prices) == 1 + price = product.master_data.staged.master_variant.prices[0] + assert price.value.cent_amount == 1000 + assert price.value.currency_code == "GBP" + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductChangePriceAction( + price_id=price.id, + price=types.PriceDraft( + value=types.Money(cent_amount=3000, currency_code="EUR") + ), + ) + ], + ) + + assert product.master_data.current is None + assert len(product.master_data.staged.master_variant.prices) == 1 + price = product.master_data.staged.master_variant.prices[0] + assert price.value.cent_amount == 3000 + assert price.value.currency_code == "EUR" + + +def test_product_update_add_price_current(client): + product = client.products.create( + types.ProductDraft( + key="test-product", + master_variant=types.ProductVariantDraft(sku="1", key="1"), + publish=True, + ) + ) + + product = client.products.update_by_id( + id=product.id, + version=product.version, + actions=[ + types.ProductAddPriceAction( + sku="1", + staged=False, + price=types.PriceDraft( + value=types.Money(cent_amount=1000, currency_code="GBP") + ), + ) + ], + ) + + assert product.master_data.staged is None + assert len(product.master_data.current.master_variant.prices) == 1 From 53f57e27a59117d7c2ca7dc405083fcc562c7a47 Mon Sep 17 00:00:00 2001 From: David Weterings Date: Wed, 15 Jul 2020 12:21:19 +0200 Subject: [PATCH 3/4] pin isort to same value everywhere (tox, setup.py) --- setup.py | 2 +- src/commercetools/helpers.py | 2 +- src/commercetools/services/extensions.py | 6 +++++- src/commercetools/testing/products.py | 4 ++-- src/commercetools/testing/utils.py | 12 +++++++----- tox.ini | 2 +- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 025e5bdc..0c9e3599 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "astunparse==1.6.3", "attrs>=18.2.0", "black==18.9b0", - "isort[pyproject]", + "isort[pyproject]==4.2.15", "PyYAML==3.13", ] diff --git a/src/commercetools/helpers.py b/src/commercetools/helpers.py index e1568a10..a47aa80f 100644 --- a/src/commercetools/helpers.py +++ b/src/commercetools/helpers.py @@ -3,8 +3,8 @@ from marshmallow import class_registry, fields, missing from marshmallow.exceptions import StringNotCollectionError, ValidationError from marshmallow.fields import Field -from marshmallow.utils import RAISE, is_collection from marshmallow.utils import missing as missing_ +from marshmallow.utils import RAISE, is_collection from commercetools.exceptions import CommercetoolsError diff --git a/src/commercetools/services/extensions.py b/src/commercetools/services/extensions.py index df54df64..5db3af38 100644 --- a/src/commercetools/services/extensions.py +++ b/src/commercetools/services/extensions.py @@ -43,7 +43,11 @@ def query( def create(self, draft: types.ExtensionDraft) -> types.Extension: return self._client._post( - "extensions", {}, draft, schemas.ExtensionDraftSchema, schemas.ExtensionSchema + "extensions", + {}, + draft, + schemas.ExtensionDraftSchema, + schemas.ExtensionSchema, ) def update_by_id( diff --git a/src/commercetools/testing/products.py b/src/commercetools/testing/products.py index 3264149f..a2b62665 100644 --- a/src/commercetools/testing/products.py +++ b/src/commercetools/testing/products.py @@ -1,10 +1,10 @@ import copy import datetime import uuid -from typing import Optional, List, Union +from typing import List, Optional, Union -from marshmallow import Schema from marshmallow import fields as schema_fields +from marshmallow import Schema from commercetools import schemas, types from commercetools.testing import utils diff --git a/src/commercetools/testing/utils.py b/src/commercetools/testing/utils.py index e049a4e9..3ca14d05 100644 --- a/src/commercetools/testing/utils.py +++ b/src/commercetools/testing/utils.py @@ -53,15 +53,17 @@ def create_from_draft(draft): if isinstance(draft, types.CustomFieldsDraft): return types.CustomFields( type=types.TypeReference(type_id=draft.type.type_id, id=draft.type.id), - fields=draft.fields + fields=draft.fields, ) if isinstance(draft, types.PriceTierDraft): return types.PriceTier( minimum_quantity=draft.minimum_quantity, - value=_money_to_typed(types.Money( - cent_amount=draft.value.cent_amount, - currency_code=draft.value.currency_code, - )), + value=_money_to_typed( + types.Money( + cent_amount=draft.value.cent_amount, + currency_code=draft.value.currency_code, + ) + ), ) raise ValueError(f"Unsupported type {draft.__class__}") diff --git a/tox.ini b/tox.ini index f9cb9982..592734e9 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = basepython = python3.7 deps = black==18.9b0 - isort[toml] + isort[toml]==4.2.15 skip_install = true commands = isort --recursive --check-only src tests From 6f44e543a4d4b8f809b9362a5cba0932ccc2313e Mon Sep 17 00:00:00 2001 From: David Weterings Date: Wed, 15 Jul 2020 12:27:50 +0200 Subject: [PATCH 4/4] try a different isort version --- setup.py | 4 ++-- src/commercetools/helpers.py | 2 +- src/commercetools/testing/products.py | 2 +- tox.ini | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 0c9e3599..2b518241 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ "astunparse==1.6.3", "attrs>=18.2.0", "black==18.9b0", - "isort[pyproject]==4.2.15", + "isort[pyproject]==4.3.21", "PyYAML==3.13", ] @@ -36,7 +36,7 @@ "pytest-cov==2.5.1", "pytest==3.1.3", # Linting - "isort==4.2.15", + "isort==4.3.21", "flake8==3.3.0", "flake8-blind-except==0.1.1", "flake8-debugger==1.4.0", diff --git a/src/commercetools/helpers.py b/src/commercetools/helpers.py index a47aa80f..e1568a10 100644 --- a/src/commercetools/helpers.py +++ b/src/commercetools/helpers.py @@ -3,8 +3,8 @@ from marshmallow import class_registry, fields, missing from marshmallow.exceptions import StringNotCollectionError, ValidationError from marshmallow.fields import Field -from marshmallow.utils import missing as missing_ from marshmallow.utils import RAISE, is_collection +from marshmallow.utils import missing as missing_ from commercetools.exceptions import CommercetoolsError diff --git a/src/commercetools/testing/products.py b/src/commercetools/testing/products.py index a2b62665..206c02bf 100644 --- a/src/commercetools/testing/products.py +++ b/src/commercetools/testing/products.py @@ -3,8 +3,8 @@ import uuid from typing import List, Optional, Union -from marshmallow import fields as schema_fields from marshmallow import Schema +from marshmallow import fields as schema_fields from commercetools import schemas, types from commercetools.testing import utils diff --git a/tox.ini b/tox.ini index 592734e9..8d256050 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ commands = basepython = python3.7 deps = black==18.9b0 - isort[toml]==4.2.15 + isort[toml]==4.3.21 skip_install = true commands = isort --recursive --check-only src tests