Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve automatically discounts validation #59

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
10 changes: 9 additions & 1 deletion src/viur/shop/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,12 @@
SHOP_INSTANCE: ContextVar["Shop"] = ContextVar("ShopInstance")
SHOP_INSTANCE_VI: ContextVar["Shop"] = ContextVar("ShopInstanceVi")

SENTINEL: t.Final[object] = object()

class Sentinel:
def __repr__(self) -> str:
return "<SENTINEL>"

def __bool__(self) -> bool:
return False

SENTINEL: t.Final[Sentinel] = Sentinel()

Check failure on line 21 in src/viur/shop/globals.py

View workflow job for this annotation

GitHub Actions / linter (3.12)

E305: expected 2 blank lines after class or function definition, found 1
18 changes: 12 additions & 6 deletions src/viur/shop/modules/discount.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from viur.core import db, errors
from viur.core.prototypes import List
from viur.core.skeleton import SkeletonInstance

from viur.shop.types import *
from .abstract import ShopModuleAbstract
from ..globals import SHOP_LOGGER
from ..skeletons import DiscountSkel
from ..types.dc_scope import DiscountValidator

logger = SHOP_LOGGER.getChild(__name__)
Expand Down Expand Up @@ -86,7 +88,7 @@ def apply(
for discount_skel in skels:
logger.debug(f'{discount_skel["name"]=} // {discount_skel["description"]=}')
# logger.debug(f"{discount_skel = }")
applicable, dv = self.can_apply(discount_skel, cart_key, code)
applicable, dv = self.can_apply(discount_skel, cart_key=cart_key, code=code)
if applicable:
logger.debug("is applicable")
break
Expand Down Expand Up @@ -172,11 +174,14 @@ def apply(

def can_apply(
self,
skel: SkeletonInstance,
skel: SkeletonInstance_T[DiscountSkel],
*,
cart_key: db.Key | None = None,
article_skel: SkeletonInstance | None = None,
code: str | None = None,
as_automatically: bool = False,
) -> tuple[bool, DiscountValidator | None]:
logger.debug(f"--- Calling can_apply() ---")
logger.debug(f'{skel["name"] = } // {skel["description"] = }')
# logger.debug(f"{skel = }")

Expand All @@ -188,15 +193,16 @@ def can_apply(
raise errors.NotFound

if not as_automatically and skel["activate_automatically"]:
logger.info(f"is activate_automatically")
logger.info(f"looking for as_automatically")
return False, None

dv = DiscountValidator()(cart_skel=cart, discount_skel=skel, code=code)
dv = DiscountValidator()(cart_skel=cart, article_skel=article_skel, discount_skel=skel, code=code)
# logger.debug(f"{dv.is_fulfilled=} | {dv=}")
return dv.is_fulfilled, dv

@property
@functools.cache
def current_automatically_discounts(self) -> list[SkeletonInstance]:
def current_automatically_discounts(self) -> list[SkeletonInstance_T[DiscountSkel]]:
query = self.viewSkel().all().filter("activate_automatically =", True)
discounts = []
for skel in query.fetch(100):
Expand All @@ -205,7 +211,7 @@ def current_automatically_discounts(self) -> list[SkeletonInstance]:
logger.debug(f'Skipping discount {skel["key"]} {skel["name"]}')
continue
discounts.append(skel)
logger.debug(f'current {discounts=}')
logger.debug(f'current_automatically_discounts {discounts=}')
return discounts

def remove(
Expand Down
7 changes: 6 additions & 1 deletion src/viur/shop/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from .events import EVENT_SERVICE, Event, EventService, on_event
from .hooks import HOOK_SERVICE, Hook
from .hooks import Customization, HOOK_SERVICE, Hook, HookService

__all__ = [
# .event
"EVENT_SERVICE",
"Event",
"EventService",
"on_event",
# .hooks
"Customization",
"HOOK_SERVICE",
"Hook",
"HookService",
]
33 changes: 19 additions & 14 deletions src/viur/shop/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import typing as t

from viur.core.skeleton import Skeleton as _Skeleton, SkeletonInstance as _SkeletonInstance

SkeletonCls_co = t.TypeVar("SkeletonCls_co", bound=t.Type[_Skeleton], covariant=True)


class SkeletonInstance_T(_SkeletonInstance, t.Generic[SkeletonCls_co]):
"""This types trys to say to which SkeletonCls a SkeletonInstance belongs

or in other words, it does what the viur-core failed to do.
"""
...


del _Skeleton, _SkeletonInstance

from .data import ClientError, Supplier # noqa
from .dc_scope import (ConditionValidator, DiscountConditionScope, DiscountValidator) # noqa
from .dc_scope import ( # noqa
DiscountConditionScope,
ConditionValidator,
DiscountValidator,
)
from .enums import ( # noqa
AddressType,
ApplicationDomain,
Expand Down Expand Up @@ -32,16 +50,3 @@
from .price import Price # noqa
from .response import ExtendedCustomJsonEncoder, JsonResponse # noqa
from .results import (OrderViewResult, PaymentProviderResult, StatusError) # noqa

SkeletonCls_co = t.TypeVar("SkeletonCls_co", bound=t.Type[_Skeleton], covariant=True)


class SkeletonInstance_T(_SkeletonInstance, t.Generic[SkeletonCls_co]):
"""This types trys to say to which SkeletonCls a SkeletonInstance belongs

or in other words, it does what the viur-core failed to do.
"""
...


del _Skeleton, _SkeletonInstance
60 changes: 39 additions & 21 deletions src/viur/shop/types/dc_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
import typing as t # noqa

from viur.core import current, utils
from viur.core.skeleton import Skeleton, SkeletonInstance, skeletonByKind
from .exceptions import InvalidStateError
from ..globals import SENTINEL, SHOP_INSTANCE, SHOP_LOGGER
from viur.core.skeleton import Skeleton, skeletonByKind

from .enums import *
from .exceptions import InvalidStateError
from ..globals import SENTINEL, SHOP_INSTANCE, SHOP_LOGGER, Sentinel
from ..types import SkeletonInstance_T

if t.TYPE_CHECKING:
from ..skeletons import ArticleAbstractSkel, CartNodeSkel, DiscountConditionSkel, DiscountSkel

logger = SHOP_LOGGER.getChild(__name__)

Expand All @@ -17,12 +22,14 @@ class DiscountConditionScope:
def __init__(
self,
*,
cart_skel=SENTINEL,
discount_skel=SENTINEL,
condition_skel=SENTINEL,
code=SENTINEL,
cart_skel: SkeletonInstance_T["CartNodeSkel"] | None | Sentinel = SENTINEL,
article_skel: SkeletonInstance_T["ArticleAbstractSkel"] | None | Sentinel = SENTINEL,
discount_skel: SkeletonInstance_T["DiscountSkel"] | None | Sentinel = SENTINEL,
code: str | None | Sentinel = SENTINEL,
condition_skel: SkeletonInstance_T["DiscountConditionSkel"],
):
self.cart_skel = cart_skel
self.article_skel = article_skel
self.discount_skel = discount_skel
self.condition_skel = condition_skel
self.code = code
Expand Down Expand Up @@ -61,16 +68,18 @@ def __init__(self):
self._is_fulfilled = None
self.scope_instances = []
self.cart_skel = None
self.article_skel = None
self.discount_skel = None
self.condition_skel = None

def __call__(
self,
*,
cart_skel=SENTINEL,
discount_skel=SENTINEL,
condition_skel=SENTINEL,
code=SENTINEL,
cart_skel: SkeletonInstance_T["CartNodeSkel"] | None | Sentinel = SENTINEL,
article_skel: SkeletonInstance_T["ArticleAbstractSkel"] | None | Sentinel = SENTINEL,
discount_skel: SkeletonInstance_T["DiscountSkel"] | None | Sentinel = SENTINEL,
code: str | None | Sentinel = SENTINEL,
condition_skel: SkeletonInstance_T["DiscountConditionSkel"],
) -> t.Self:
self.cart_skel = cart_skel
self.discount_skel = discount_skel
Expand All @@ -79,6 +88,7 @@ def __call__(
for Scope in ConditionValidator.scopes:
scope = Scope(
cart_skel=cart_skel,
article_skel=article_skel,
discount_skel=discount_skel,
condition_skel=condition_skel,
code=code,
Expand Down Expand Up @@ -115,32 +125,40 @@ def __init__(self):
self._is_fulfilled = None
self.condition_validator_instances: list[ConditionValidator] = []
self.cart_skel = None
self.article_skel = None
self.discount_skel = None
self.condition_skels = []

def __call__(
self,
*,
cart_skel=SENTINEL,
discount_skel=SENTINEL,
code=SENTINEL,
cart_skel: SkeletonInstance_T["CartNodeSkel"] | None | Sentinel = SENTINEL,
article_skel: SkeletonInstance_T["ArticleAbstractSkel"] | None | Sentinel = SENTINEL,
discount_skel: SkeletonInstance_T["DiscountSkel"] | None | Sentinel = SENTINEL,
code: str | None | Sentinel = SENTINEL,
) -> t.Self:
self.cart_skel = cart_skel
self.article_skel = article_skel
self.discount_skel = discount_skel
self.code = discount_skel

# We need the full skel with all bones (otherwise the refSkel would be to large)
condition_skel_cls: t.Type[Skeleton] = skeletonByKind(discount_skel.condition.kind)
for condition in discount_skel["condition"]:
condition_skel: SkeletonInstance = condition_skel_cls() # noqa
condition_skel: SkeletonInstance_T[DiscountConditionSkel] = condition_skel_cls() # noqa
if not condition_skel.fromDB(condition["dest"]["key"]):
logger.warning(f'Broken relation {condition=} in {discount_skel["key"]}?!')
raise InvalidStateError(f'Broken relation {condition=} in {discount_skel["key"]}?!')
self.condition_skels.append(None) # TODO
self.condition_validator_instances.append(None) # TODO
continue
cv = ConditionValidator()(cart_skel=cart_skel, discount_skel=discount_skel, condition_skel=condition_skel,
code=code)
cv = ConditionValidator()(
cart_skel=cart_skel,
article_skel=article_skel,
discount_skel=discount_skel,
condition_skel=condition_skel,
code=code,
)
self.condition_skels.append(condition_skel)
self.condition_validator_instances.append(cv)

Expand Down Expand Up @@ -297,17 +315,17 @@ def __call__(self) -> bool:
raise NotImplementedError


# @ConditionValidator.register TODO
@ConditionValidator.register
class ScopeCombinableLowPrice(DiscountConditionScope):
def precondition(self) -> bool:
# logger.debug(f"ScopeCombinableLowPrice :: {self.cart_skel=} | {self.article_skel=}")
return (
self.condition_skel["scope_combinable_low_price"] is not None
# and self.cart_skel is not None
and self.article_skel
)

def __call__(self) -> bool:
article_skel = ... # FIXME: how we get this?
return not article_skel["shop_is_low_price"] or self.condition_skel["scope_combinable_low_price"]
return not self.article_skel["shop_is_low_price"] or self.condition_skel["scope_combinable_low_price"]


@ConditionValidator.register
Expand Down
12 changes: 10 additions & 2 deletions src/viur/shop/types/price.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from ..globals import SHOP_INSTANCE, SHOP_LOGGER
from ..types import ConfigurationError

if t.TYPE_CHECKING:
from ..modules import Discount

logger = SHOP_LOGGER.getChild(__name__)


Expand Down Expand Up @@ -44,7 +47,6 @@ def __init__(self, src_object):
raise TypeError(f"Unsupported type {type(src_object)}")

# logger.debug(f"{self.article_skel = }")
# logger.debug(f"{self.article_skel.shop_current_discount = }")

if (best_discount := self.shop_current_discount(self.article_skel)) is not None:
price, skel = best_discount
Expand Down Expand Up @@ -84,14 +86,20 @@ def current(self) -> float:
return toolkit.round_decimal(best_price, 2)
return self.retail

def shop_current_discount(self, skel) -> None | tuple[float, "SkeletonInstance"]:
def shop_current_discount(self, article_skel: SkeletonInstance) -> None | tuple[float, "SkeletonInstance"]:
"""Best permanent discount campaign for article"""
best_discount = None
article_price = self.retail or 0.0 # FIXME: how to handle None prices?
if not article_price:
return None
discount_module: "Discount" = SHOP_INSTANCE.get().discount
for skel in SHOP_INSTANCE.get().discount.current_automatically_discounts:
# TODO: if can apply (article range, lang, ...)
applicable, dv = discount_module.can_apply(skel, article_skel=article_skel, as_automatically=True)
# logger.debug(f"{dv=}")
if not applicable:
logger.debug(f"{skel} is NOT applicable")
continue
price = self.apply_discount(skel, article_price)
if best_discount is None or price < best_discount[0]:
best_discount = price, skel
Expand Down
Loading