Something as simple as a promo code can be a pretty complicated use case. First of all, this is due to the fact that the promotional code is a logic that (as a rule) is supported by one or several business users, while it also belongs within the domain. This is unconventional, in that sense. There are several ways to deal with this, and what I describe will be just my personal approach.
For argument, suppose you have a simple series of well-known promotional codes, for example:
- X% Off Purchase with or without a minimum purchase
- $ X Off Purchase with or without a minimum purchase
We can make some assumptions:
- Ad code has a start date.
- Ad code has an end date.
Using a promotional code can be difficult. Consider the two scenarios that we have identified. "$ X Off Purchase" is relatively simple, as a fixed amount. However, buying an "X% Off Purchase" is more complicated. If we only had a fixed amount, we could apply the discount to the trolley as soon as any threshold values are reached. If there is a discount on the interest rate, if the user has to add two elements, add a promotional code, and then add another element, the promotion will already be "applied".
Because of this, I personally “attached” the ad code to the cart. What for? The reason is that at the time of verification, I can assume that the trolley will be used to create the order. Until this time, the contents of the basket are liquid. The user’s actions against the basket will change the total value of the basket, as well as the total discount if there is an unfixed discount amount. It can also invalidate the discount if the user removes one or more items from the basket and the total price of the basket drops below the threshold to apply the discount.
So, I would have a few teams that would be involved. In fact, any team that affects the value of the basket can change the discount amount. To this end, I will look for the amount of discounts that will be recounted for teams that:
- Add product to cart
- Remove item from cart.
- Change the number of items in the cart
- Add promotional code to cart
- Change the promotional code attached to the basket.
Since all these operations are against the basket, I would have calculated the discount as part of the promotional code, myself with the participation of the data contained in the basket. It seems that the promotional code will constitute the aggregate, going along this path. Thus, I would call command handlers a domain service that can provide my cart with the necessary information. This domain service will download the promotional code, and I'm going to go through the positions of this basket so that the promotional code will tell me what the calculated discount will be. Then I am going to create an event that contains the new basket value, as well as the adjusted value (discount). Following this path, the logic for calculating discounts based on the positions in the basket is responsible for the promotional code.
Instead, you can put this responsibility in the basket. Personally, however, it seems to me that encapsulating domain logic in a promotional code, in itself, makes more sense. I mentioned that it is likely that you will create an order from the basket. Having a promotional code as an aggregate and containing the logic of applying discounts based on positions, we have the only truth in how we calculate the discount on positions - whether from the point of view of a cart or as members of an order.