Browse Source

feat(core): Implement customer group promotion condition

Relates to #400
Michael Bromley 5 years ago
parent
commit
fd70448bb8

+ 2 - 0
packages/common/src/shared-types.ts

@@ -121,6 +121,7 @@ export type DefaultFormComponentId =
     | 'number-form-input'
     | 'select-form-input'
     | 'product-selector-form-input'
+    | 'customer-group-form-input'
     | 'text-form-input';
 
 /**
@@ -134,6 +135,7 @@ type DefaultFormConfigHash = {
     'currency-form-input': {};
     'facet-value-form-input': {};
     'product-selector-form-input': {};
+    'customer-group-form-input': {};
     'text-form-input': {};
 };
 

+ 7 - 40
packages/core/e2e/customer-group.e2e-spec.ts

@@ -1,14 +1,6 @@
 import { pick } from '@vendure/common/lib/pick';
-import {
-    AccountRegistrationEvent,
-    EventBus,
-    EventBusModule,
-    mergeConfig,
-    VendurePlugin,
-} from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
-import cu from 'i18next-icu/locale-data/cu';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
@@ -28,7 +20,13 @@ import {
     UpdateCustomerGroup,
 } from './graphql/generated-e2e-admin-types';
 import { DeletionResult } from './graphql/generated-e2e-shop-types';
-import { GET_CUSTOMER_HISTORY, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
+import {
+    CREATE_CUSTOMER_GROUP,
+    CUSTOMER_GROUP_FRAGMENT,
+    GET_CUSTOMER_HISTORY,
+    GET_CUSTOMER_LIST,
+    REMOVE_CUSTOMERS_FROM_GROUP,
+} from './graphql/shared-definitions';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { sortById } from './utils/test-order-utils';
 
@@ -285,28 +283,6 @@ describe('CustomerGroup resolver', () => {
     });
 });
 
-export const CUSTOMER_GROUP_FRAGMENT = gql`
-    fragment CustomerGroup on CustomerGroup {
-        id
-        name
-        customers {
-            items {
-                id
-            }
-            totalItems
-        }
-    }
-`;
-
-export const CREATE_CUSTOMER_GROUP = gql`
-    mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
-        createCustomerGroup(input: $input) {
-            ...CustomerGroup
-        }
-    }
-    ${CUSTOMER_GROUP_FRAGMENT}
-`;
-
 export const UPDATE_CUSTOMER_GROUP = gql`
     mutation UpdateCustomerGroup($input: UpdateCustomerGroupInput!) {
         updateCustomerGroup(input: $input) {
@@ -361,15 +337,6 @@ export const ADD_CUSTOMERS_TO_GROUP = gql`
     ${CUSTOMER_GROUP_FRAGMENT}
 `;
 
-export const REMOVE_CUSTOMERS_FROM_GROUP = gql`
-    mutation RemoveCustomersFromGroup($groupId: ID!, $customerIds: [ID!]!) {
-        removeCustomersFromGroup(customerGroupId: $groupId, customerIds: $customerIds) {
-            ...CustomerGroup
-        }
-    }
-    ${CUSTOMER_GROUP_FRAGMENT}
-`;
-
 export const GET_CUSTOMER_WITH_GROUPS = gql`
     query GetCustomerWithGroups($id: ID!) {
         customer(id: $id) {

+ 31 - 0
packages/core/e2e/graphql/shared-definitions.ts

@@ -405,3 +405,34 @@ export const GET_ORDER = gql`
     }
     ${ORDER_WITH_LINES_FRAGMENT}
 `;
+
+export const CUSTOMER_GROUP_FRAGMENT = gql`
+    fragment CustomerGroup on CustomerGroup {
+        id
+        name
+        customers {
+            items {
+                id
+            }
+            totalItems
+        }
+    }
+`;
+
+export const CREATE_CUSTOMER_GROUP = gql`
+    mutation CreateCustomerGroup($input: CreateCustomerGroupInput!) {
+        createCustomerGroup(input: $input) {
+            ...CustomerGroup
+        }
+    }
+    ${CUSTOMER_GROUP_FRAGMENT}
+`;
+
+export const REMOVE_CUSTOMERS_FROM_GROUP = gql`
+    mutation RemoveCustomersFromGroup($groupId: ID!, $customerIds: [ID!]!) {
+        removeCustomersFromGroup(customerGroupId: $groupId, customerIds: $customerIds) {
+            ...CustomerGroup
+        }
+    }
+    ${CUSTOMER_GROUP_FRAGMENT}
+`;

+ 64 - 1
packages/core/e2e/order-promotion.e2e-spec.ts

@@ -3,6 +3,7 @@ import { omit } from '@vendure/common/lib/omit';
 import { pick } from '@vendure/common/lib/pick';
 import {
     containsProducts,
+    customerGroup,
     discountOnItemWithFacets,
     hasFacetValues,
     minimumOrderAmount,
@@ -18,11 +19,13 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import {
+    CreateCustomerGroup,
     CreatePromotion,
     CreatePromotionInput,
     GetFacetList,
     GetPromoProducts,
     HistoryEntryType,
+    RemoveCustomersFromGroup,
 } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrder,
@@ -34,7 +37,12 @@ import {
     RemoveCouponCode,
     SetCustomerForOrder,
 } from './graphql/generated-e2e-shop-types';
-import { CREATE_PROMOTION, GET_FACET_LIST } from './graphql/shared-definitions';
+import {
+    CREATE_CUSTOMER_GROUP,
+    CREATE_PROMOTION,
+    GET_FACET_LIST,
+    REMOVE_CUSTOMERS_FROM_GROUP,
+} from './graphql/shared-definitions';
 import {
     ADD_ITEM_TO_ORDER,
     ADJUST_ITEM_QUANTITY,
@@ -376,6 +384,61 @@ describe('Promotions applied to Orders', () => {
 
             await deletePromotion(promotion.id);
         });
+
+        it('customerGroup', async () => {
+            const { createCustomerGroup } = await adminClient.query<
+                CreateCustomerGroup.Mutation,
+                CreateCustomerGroup.Variables
+            >(CREATE_CUSTOMER_GROUP, {
+                input: { name: 'Test Group', customerIds: ['T_1'] },
+            });
+
+            await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
+
+            const promotion = await createPromotion({
+                enabled: true,
+                name: 'Free for group members',
+                conditions: [
+                    {
+                        code: customerGroup.code,
+                        arguments: [{ name: 'customerGroupId', value: createCustomerGroup.id }],
+                    },
+                ],
+                actions: [freeOrderAction],
+            });
+
+            const { addItemToOrder } = await shopClient.query<
+                AddItemToOrder.Mutation,
+                AddItemToOrder.Variables
+            >(ADD_ITEM_TO_ORDER, {
+                productVariantId: getVariantBySlug('item-60').id,
+                quantity: 1,
+            });
+            expect(addItemToOrder!.total).toBe(0);
+            expect(addItemToOrder!.adjustments.length).toBe(1);
+            expect(addItemToOrder!.adjustments[0].description).toBe('Free for group members');
+            expect(addItemToOrder!.adjustments[0].amount).toBe(-6000);
+
+            await adminClient.query<RemoveCustomersFromGroup.Mutation, RemoveCustomersFromGroup.Variables>(
+                REMOVE_CUSTOMERS_FROM_GROUP,
+                {
+                    groupId: createCustomerGroup.id,
+                    customerIds: ['T_1'],
+                },
+            );
+
+            const { adjustOrderLine } = await shopClient.query<
+                AdjustItemQuantity.Mutation,
+                AdjustItemQuantity.Variables
+            >(ADJUST_ITEM_QUANTITY, {
+                orderLineId: addItemToOrder!.lines[0].id,
+                quantity: 2,
+            });
+            expect(adjustOrderLine!.total).toBe(12000);
+            expect(adjustOrderLine!.adjustments.length).toBe(0);
+
+            await deletePromotion(promotion.id);
+        });
     });
 
     describe('default PromotionActions', () => {

+ 4 - 0
packages/core/src/common/configurable-operation.ts

@@ -46,6 +46,10 @@ export type UiComponentConfig =
     | ({ component: 'boolean-form-input' } & DefaultFormComponentConfig<'boolean-form-input'>)
     | ({ component: 'currency-form-input' } & DefaultFormComponentConfig<'currency-form-input'>)
     | ({ component: 'facet-value-form-input' } & DefaultFormComponentConfig<'facet-value-form-input'>)
+    | ({ component: 'product-selector-form-input' } & DefaultFormComponentConfig<
+          'product-selector-form-input'
+      >)
+    | ({ component: 'customer-group-form-input' } & DefaultFormComponentConfig<'customer-group-form-input'>)
     | { component: string; [prop: string]: any };
 
 const d: UiComponentConfig = {

+ 1 - 0
packages/core/src/common/index.ts

@@ -3,4 +3,5 @@ export * from './finite-state-machine/types';
 export * from './async-queue';
 export * from './error/errors';
 export * from './injector';
+export * from './ttl-cache';
 export * from './utils';

+ 42 - 0
packages/core/src/config/promotion/conditions/customer-group-condition.ts

@@ -0,0 +1,42 @@
+import { LanguageCode } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { TtlCache } from '../../../common/ttl-cache';
+import { idsAreEqual } from '../../../common/utils';
+import { Order } from '../../../entity/order/order.entity';
+import { PromotionCondition } from '../promotion-condition';
+
+let customerService: import('../../../service/services/customer.service').CustomerService;
+
+const fiveMinutes = 5 * 60 * 1000;
+const cache = new TtlCache<ID, ID[]>({ ttl: fiveMinutes });
+
+export const customerGroup = new PromotionCondition({
+    code: 'customer_group',
+    description: [{ languageCode: LanguageCode.en, value: 'Customer is a member of the specified group' }],
+    args: {
+        customerGroupId: {
+            type: 'ID',
+            ui: { component: 'customer-group-form-input' },
+            label: [{ languageCode: LanguageCode.en, value: 'Customer group' }],
+        },
+    },
+    async init(injector) {
+        // Lazily-imported to avoid circular dependency issues.
+        const { CustomerService } = await import('../../../service/services/customer.service');
+        customerService = injector.get(CustomerService);
+    },
+    async check(order: Order, args) {
+        if (!order.customer) {
+            return false;
+        }
+        const customerId = order.customer.id;
+        let groupIds = cache.get(customerId);
+        if (!groupIds) {
+            const groups = await customerService.getCustomerGroups(customerId);
+            groupIds = groups.map(g => g.id);
+            cache.set(customerId, groupIds);
+        }
+        return !!groupIds.find(id => idsAreEqual(id, args.customerGroupId));
+    },
+});

+ 8 - 1
packages/core/src/config/promotion/index.ts

@@ -2,6 +2,7 @@ import { discountOnItemWithFacets } from './actions/facet-values-discount-action
 import { orderPercentageDiscount } from './actions/order-percentage-discount-action';
 import { productsPercentageDiscount } from './actions/product-discount-action';
 import { containsProducts } from './conditions/contains-products-condition';
+import { customerGroup } from './conditions/customer-group-condition';
 import { hasFacetValues } from './conditions/has-facet-values-condition';
 import { minimumOrderAmount } from './conditions/min-order-amount-condition';
 
@@ -13,10 +14,16 @@ export * from './actions/product-discount-action';
 export * from './conditions/has-facet-values-condition';
 export * from './conditions/min-order-amount-condition';
 export * from './conditions/contains-products-condition';
+export * from './conditions/customer-group-condition';
 
 export const defaultPromotionActions = [
     orderPercentageDiscount,
     discountOnItemWithFacets,
     productsPercentageDiscount,
 ];
-export const defaultPromotionConditions = [minimumOrderAmount, hasFacetValues, containsProducts];
+export const defaultPromotionConditions = [
+    minimumOrderAmount,
+    hasFacetValues,
+    containsProducts,
+    customerGroup,
+];