|
|
@@ -101,10 +101,11 @@ export const config: VendureConfig = {
|
|
|
|
|
|
## Creating custom actions
|
|
|
|
|
|
-There are two kinds of PromotionAction:
|
|
|
+There are three kinds of PromotionAction:
|
|
|
|
|
|
- [`PromotionItemAction`]({{< relref "promotion-action" >}}#promotionitemaction) applies a discount on the OrderItem level, i.e. it would be used for a promotion like "50% off USB cables".
|
|
|
- [`PromotionOrderAction`]({{< relref "promotion-action" >}}#promotionorderaction) applies a discount on the Order level, i.e. it would be used for a promotion like "5% off the order total".
|
|
|
+- [`PromotionShippingAction`]({{< relref "promotion-action" >}}#promotionshippingaction) applies a discount on the shipping, i.e. it would be used for a promotion like "free shipping".
|
|
|
|
|
|
Their implementations are similar, with the difference being the arguments passed to the `execute()` function of each.
|
|
|
|
|
|
@@ -157,6 +158,98 @@ export const config: VendureConfig = {
|
|
|
}
|
|
|
```
|
|
|
|
|
|
+## Free gift promotions
|
|
|
+
|
|
|
+Vendure v1.8 introduces a new **side effect API** to PromotionActions, which allow you to define some additional action to be performed when a Promotion becomes active or inactive.
|
|
|
+
|
|
|
+A primary use-case of this API is to add a free gift to the Order. Here's an example of a plugin which implements a "free gift" action:
|
|
|
+
|
|
|
+```TypeScript
|
|
|
+import {
|
|
|
+ ID, idsAreEqual, isGraphQlErrorResult, LanguageCode,
|
|
|
+ Logger, OrderLine, OrderService, PromotionItemAction, VendurePlugin,
|
|
|
+} from '@vendure/core';
|
|
|
+import { createHash } from 'crypto';
|
|
|
+
|
|
|
+let orderService: OrderService;
|
|
|
+export const freeGiftAction = new PromotionItemAction({
|
|
|
+ code: 'free_gift',
|
|
|
+ description: [{ languageCode: LanguageCode.en, value: 'Add free gifts to the order' }],
|
|
|
+ args: {
|
|
|
+ productVariantIds: {
|
|
|
+ type: 'ID',
|
|
|
+ list: true,
|
|
|
+ ui: { component: 'product-selector-form-input' },
|
|
|
+ label: [{ languageCode: LanguageCode.en, value: 'Gift product variants' }],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ init(injector) {
|
|
|
+ orderService = injector.get(OrderService);
|
|
|
+ },
|
|
|
+ execute(ctx, orderItem, orderLine, args) {
|
|
|
+ // This part is responsible for ensuring the variants marked as
|
|
|
+ // "free gifts" have their price reduced to zero.
|
|
|
+ if (lineContainsIds(args.productVariantIds, orderLine)) {
|
|
|
+ const unitPrice = orderLine.unitPrice;
|
|
|
+ return -unitPrice;
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ },
|
|
|
+ // The onActivate function is part of the side effect API, and
|
|
|
+ // allows us to perform some action whenever a Promotion becomes active
|
|
|
+ // due to it's conditions & constraints being satisfied.
|
|
|
+ async onActivate(ctx, order, args, promotion) {
|
|
|
+ for (const id of args.productVariantIds) {
|
|
|
+ if (
|
|
|
+ !order.lines.find(
|
|
|
+ (line) =>
|
|
|
+ idsAreEqual(line.productVariant.id, id) &&
|
|
|
+ line.customFields.freeGiftDescription == null,
|
|
|
+ )
|
|
|
+ ) {
|
|
|
+ // The order does not yet contain this free gift, so add it
|
|
|
+ const result = await orderService.addItemToOrder(ctx, order.id, id, 1, {
|
|
|
+ freeGiftPromotionId: promotion.id.toString(),
|
|
|
+ });
|
|
|
+ if (isGraphQlErrorResult(result)) {
|
|
|
+ Logger.error(`Free gift action error for variantId "${id}": ${result.message}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // The onDeactivate function is the other part of the side effect API and is called
|
|
|
+ // when an active Promotion becomes no longer active. It should reverse any
|
|
|
+ // side effect performed by the onActivate function.
|
|
|
+ async onDeactivate(ctx, order, args, promotion) {
|
|
|
+ const linesWithFreeGift = order.lines.filter(
|
|
|
+ (line) => line.customFields.freeGiftPromotionId === promotion.id.toString(),
|
|
|
+ );
|
|
|
+ for (const line of linesWithFreeGift) {
|
|
|
+ await orderService.removeItemFromOrder(ctx, order.id, line.id);
|
|
|
+ }
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+function lineContainsIds(ids: ID[], line: OrderLine): boolean {
|
|
|
+ return !!ids.find((id) => idsAreEqual(id, line.productVariant.id));
|
|
|
+}
|
|
|
+
|
|
|
+@VendurePlugin({
|
|
|
+ configuration: config => {
|
|
|
+ config.promotionOptions.promotionActions.push(freeGiftAction);
|
|
|
+ config.customFields.OrderItem.push(
|
|
|
+ {
|
|
|
+ name: 'freeGiftPromotionId',
|
|
|
+ type: 'string',
|
|
|
+ public: true,
|
|
|
+ readonly: true,
|
|
|
+ nullable: true,
|
|
|
+ })
|
|
|
+ }
|
|
|
+})
|
|
|
+export class FreeGiftPromotionPlugin {}
|
|
|
+```
|
|
|
+
|
|
|
## Dependency relationships
|
|
|
|
|
|
It is possible to establish dependency relationships between a PromotionAction and one or more PromotionConditions.
|