dinoperovic / django-salesman

Headless e-commerce framework for Django and Wagtail.
https://django-salesman.rtfd.io
BSD 3-Clause "New" or "Revised" License
384 stars 46 forks source link

Store custom data on Order #29

Open Timusan opened 1 year ago

Timusan commented 1 year ago

Currently, when overriding and expanding your Order model with custom fields (let's say I want to store first_name and last_name on my order besides the default email), everything has to go through the extras object during checkout. I was wondering if there is (or could be in the future) a more robust way of doing this, as it currently feels like twisting the frameworks arm a bit. Let me clarify.

Say I expand my order model with these fields:

from salesman.orders.models import BaseOrder

class Order(BaseOrder):
    first_name = TextField(_("First name"), null=True, blank=True)
    last_name = TextField(_("Last name"), null=True, blank=True)

And, after creating a basket I initiate a checkout with this JSON:

{
  "email": "tim.vanderlinden@example.com",
  "first_name": "Tim",
  "last_name": "van der Linden",
  "payment_method": "pay-later",
}

To get the extra fields first_name and last_name stored on my order I need to override the populate_from_basket() method on the Order and add in these two fields:

@transaction.atomic
    def populate_from_basket(
        self,
        basket: BaseBasket,
        request: HttpRequest,
        **kwargs: Any,
    ) -> None:
        """
        Populate order with items from basket.

        Args:
            basket (Basket): Basket instance
            request (HttpRequest): Django request
        """
        from salesman.basket.serializers import ExtraRowsField

        if not hasattr(basket, "total"):
            basket.update(request)

        self.user = basket.user
        self.email = basket.extra.pop("email", "")

        self.first_name = basket.extra.pop("first_name", "") # <- added in
        self.last_name = basket.extra.pop("last_name", "")  # <- added in

        ...

As you can see I need to pop them from the extras object of the basket to store them on the final Order.

I understand that the checkout itself is not a model and is more of a process, your Basket simply gets "transformed" into an Order with the above method. But still it feels strange to pull all of our custom data we wish to capture from the extras object of the basket.

Is this the intended way? Or should the Basket model be overridden as well and contain mirror fields of the data we want to store on the final Order (eg. a custom Basket model which would also contain a first_name and last_name field)?

dinoperovic commented 1 year ago

Hi @Timusan, this is how I initially designed it. The basket and orders intentionally have the same "extra" concept of storing data so that it can be easily extended (and validated using the SALESMAN_EXTRA_VALIDATOR) without having to override the models.

The basket should store all data that will end up in the order, and vice versa. This makes it so that we can theoretically re-create baskets from orders as well.

I could introduce an additional method for assigning "extra" data during the populate_from_basket, which would make it easier to override for such cases.

As you've mentioned in the #30, adding SALESMAN_CHECKOUT_SERIALIZER option would make it easier to implement the case where you really don't want to use the "extra" dict -- and enable some other customisations. I would like to add that!