Closed michaelbromley closed 5 years ago
Firstly, I looked into creating a DSL (e.g. using Chevrotain) and this seems far too complex for the task at hand within the given time constrains. Perhaps it would be an interesting exploration for a future release.
I am formulating a general-purpose pattern which could cover all types of Adjustment in a way that gives sensible defaults but also allows arbitrary extension by a developer:
Each Adjustment has one or more conditions, which are rules which decide whether or not to apply the Adjustment to the target in question. A condition would have 2 representations: a form with inputs for configuring the condition in the UI, and a predicate function which is evaluated when deciding whether to apply the Adjustment.
We can define a data structure which encompasses both of these representations:
export interface AdjustmentCondition {
name: string;
args: Array<{ name: string; type: 'int' | 'money' | 'string' | 'datetime'; }>;
predicate: (target: OrderItem | Order, args: { [argName: string]: any; }) => boolean;
}
// example
const minimumOrderPriceCondition: AdjustmentCondition = {
name: 'Minimum order price',
args: [
{ name: 'price', type: 'money' }
],
predicate: (order, args) => {
return order.price > args.price;
},
}
There would be a number of commonly-used conditions built-in to the framework (minimum order price, minimum item quantity, date range, voucher code applied etc) but creating custom conditions would be as simple as passing a new AdjustmentCondition
into a customAdjustmentConditions
array in the VendureConfig
object.
Once it has been established that an AdjustmentSource should be applied to the target (i.e. every AdjustmentCondition's predicate evaluates to "true"), then one or more AdjustmentActions will be applied to the target.
Actions would be defined in a similar way to conditions - with configuration defining the arguments and used to determine how the form is generated in the admin ui, and a function which uses those arguments to return an adjustment amount:
export interface AdjustmentAction {
name: string;
args: Array<{ name: string; type: 'percentage' | 'money' }>;
calculate: (target: OrderItem | Order, args: { [argName: string]: any; }) => number;
}
const orderPercentageDiscountAction: AdjustmentAction = {
name: 'Percentage discount on order',
args: [
{ name: 'percentage', type: 'percentage' },
],
calculate: (order, args) => {
return order.price * args.percentage;
},
};
As with conditions, custom actions could be defined in the config object.
Using the above pattern of 1..n conditions and 1..n actions, here is how we could formulate some typical adjustments:
Conditions: OrderItem minimum, 2 Actions: Item discount, buy x get x free
Conditions: Order total greater than, 10000 Actions: Order discount, 10%
Tax adjustments would be handled in much the same way, but we would should additionally have a "tax category" field in the ProductVariant entity which points at a given AdjustmentSource representing that tax category.
In the UK for example, the current VAT rates are zero (0%), reduced (5%) and standard (20%). Here are rates in other EU countries
Taxes will further need to take into account the location of the customer, which will need to be available in the calculate()
function.
Shipping adjustments will have conditions relating to the contents, weight and destination of the Order. The calculate()
function will likely refer to either some custom logic applicable to the carrier, or a price table.
The tax system is now implemented to a degree that it seems like it is a workable design. (https://github.com/vendure-ecommerce/vendure/commit/ff31e03c969ff55778c2681f0b59fc11ec6150e5). Currently taxes are only applied to OrderItems, not Orders as a whole.
I now think shipping might work better as a specialized type of OrderLine, so we can optionally apply taxes to it using the existing tax method.
Promotions are currently implemented to a basic degree, but now the design needs to be refined so that we can be sure it is flexible enough to cover the cases we need to support.
In the comment above I mention "collections" as a means to limit the scope of a promotion. For the Alpha, rather than implementing another type of entity, we can do it with FacetValues.
So, e.g. the admin could create a new Facet named "promotions" with the value "spring offer", and then apply that FacetValue to all Products which are to be included in the "Spring Offer" promotion.
Then there should be a PromotionCondition of order contains at least n with facet values, which accepts as an argument 1) the number n
and 2) an array of facet value ids, which are applied with a logical AND operation (i.e. the product must have all the given facet values assigned to it)
And a corresponding PromotionAction of apply percentage discount on products with facet values
With 96209cd, async conditions and actions are now supported in Promotions, as well as a mechanism for injecting helper methods via the PromotionUtils
object. Also I've made a start on tests for the order calculation with promotions. Therefore I'd call this done, and for specific aspects of the promotions, individual issues can be created (e.g. implementing vouchers).
(Relates to #26)
The price of a ProductVariant is stored as an integer representing the price of the item in cents (or pence etc) without any taxes applied.
When putting together an order, the final price of the OrderItem is therefore subject to:
Furthermore, the overall Order price is the aggregate of each OrderItem as well as:
The price modifications listed above are known as Adjustments.
How Adjustments work
Determining whether to apply to a target
The basic idea is that each AdjustmentSource would have a function which if called for each potential target (e.g. each OrderItem in an Order) and this function should return true if an Adjustment should be applied or false if not.
The hard part will be figuring out how to allow the administrator to write this function. We don't want arbitrary JavaScript to be written and executed. A couple of alternatives are:
Research is needed to figure out the real costs & benefits of each approach, including: