Non-standard discounts in the Django project: Birthday discount
Discount system has very important place in the ecommerce project life cycle — it helps to retain existing clients, as well as acquire new ones. So, this time we continue to develop this topic further, but unlike the previous time when we showed how to introduce new offer type for the flash sales, we will be looking into customer loyalty program and using pre-defined discount types in the Django-Oscar. So to speak, Django-Oscar has 4 types of discounts in the conditional offer model:
Let's start sketching out our implementation from adding
date_of_birth field to the user model.
Most of the logic will sit in the custom condition class, which simply checks if user celebrates birthday day:
from django.conf import settings from django.utils.timezone import now from oscar.core.loading import get_model Condition = get_model('offer', 'Condition') class BirthdayCondition(Condition): _description = "User has birthday today." class Meta: proxy = True @property def name(self): return self._description @property def description(self): return self._description def is_satisfied(self, offer, basket, request=None): if not basket.owner: return False current_date = now().date() dob = basket.owner.date_of_birth if dob: return current_date.day == dob.day and current_date.month == dob.month
Also we'd check if the particular condition can be used with the selected offer, which is why we implemented
In this case, we prohibit birthday condition usage with the offer, other then of the user type. If validation does not pass, we raise
from django.conf import settings class ConditionIncompatible(Exception): pass class BirthdayCondition(Condition): def check_compatibility(self, offer): if offer.offer_type != ConditionalOffer.USER: raise ConditionIncompatible( "Birthday condition could be used only with the user type offer." )
Since custom conditions are implemented in the Oscar as proxy models, we need add method
Condition model, load proxy model in it and call given method on its instance.
class Condition(AbstractCondition): def is_satisfied(self, offer, basket, request=None): return super().is_satisfied(offer=offer, basket=basket) def check_compatibility(self, offer): proxy_instance = self.proxy() check_compatibility = getattr(proxy_instance, "check_compatibility", None) if check_compatibility is not None and callable(check_compatibility): proxy_instance.check_compatibility(offer)
In order to perform this validation, we need to tweak condition dashboard form slightly, but since the form does not have offer instance in the context, we'd fix this as well.
Basically, in the dashboard we can edit offer through the step-by-step wizard, which is set of Django views, where offer object is passed through the request session, so we only need to extract the latter from session and pass to the form:
from oscar.core.loading import get_class CoreOfferConditionView = get_class("dashboard.offers.views", "OfferConditionView") class OfferConditionView(CoreOfferConditionView): def get_form_kwargs(self, *args, **kwargs): form_kwargs = super().get_form_kwargs(*args, **kwargs) form_kwargs["session_offer"] = self._fetch_session_offer() return form_kwargs
Now we can customize our dashboard form and incorporate compatibility validation:
from django import forms from django.utils.translation import gettext_lazy as _ from oscar.core.loading import get_class, get_model from apps.offer.models import ConditionIncompatible Condition = get_model('offer', 'Condition') CoreConditionForm = get_class('dashboard.offers.forms', 'ConditionForm') class ConditionForm(CoreConditionForm): def __init__(self, *args, **kwargs): self.session_offer = kwargs.pop("session_offer", None) super().__init__(*args, **kwargs) def clean(self): data = super().clean() custom_condition = data.get("custom_condition", None) if custom_condition: condition = Condition.objects.get(id=custom_condition) try: condition.check_compatibility(offer=self.session_offer) except ConditionIncompatible as e: raise forms.ValidationError(e) return data
Custom condition won't be available for the selection in the dashboard form by default, so we need to manually propagate it:
from oscar.apps.offer.custom import create_condition from apps.offer.conditions import BirthdayCondition create_condition(BirthdayCondition)
The last part would be to introduce user offers aggregation to the offer applicator class:
from oscar.apps.offer.applicator import Applicator as CoreApplicator from oscar.core.loading import get_class, get_model ConditionalOffer = get_model('offer', 'ConditionalOffer') OfferApplications = get_class('offer.results', 'OfferApplications') class Applicator(CoreApplicator): def get_user_offers(self, user): qs = ConditionalOffer.active.filter(offer_type=ConditionalOffer.USER) return qs.select_related('condition', 'benefit')
Finally, after we create an offer with the newly introduced custom condition
BirthdayCondition, it will
be applied automatically if the custom celebrates birthday today (and thus it satisfies the condition):
All project code available in the Github repository — https://github.com/metaclassco/django_oscar_non_standard_dicounts.