| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681 |
- /* tslint:disable:no-non-null-assertion */
- import { pick } from '@vendure/common/lib/pick';
- import {
- atLeastNWithFacets,
- discountOnItemWithFacets,
- minimumOrderAmount,
- orderPercentageDiscount,
- } from '@vendure/core';
- import { createTestEnvironment } from '@vendure/testing';
- import gql from 'graphql-tag';
- import path from 'path';
- import { initialData } from '../../../e2e-common/e2e-initial-data';
- import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
- import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
- import {
- CreatePromotion,
- CreatePromotionInput,
- GetFacetList,
- GetPromoProducts,
- HistoryEntryType,
- } from './graphql/generated-e2e-admin-types';
- import {
- AddItemToOrder,
- AdjustItemQuantity,
- ApplyCouponCode,
- GetActiveOrder,
- GetOrderPromotionsByCode,
- RemoveCouponCode,
- SetCustomerForOrder,
- } from './graphql/generated-e2e-shop-types';
- import { CREATE_PROMOTION, GET_FACET_LIST } from './graphql/shared-definitions';
- import {
- ADD_ITEM_TO_ORDER,
- ADJUST_ITEM_QUANTITY,
- APPLY_COUPON_CODE,
- GET_ACTIVE_ORDER,
- GET_ORDER_PROMOTIONS_BY_CODE,
- REMOVE_COUPON_CODE,
- SET_CUSTOMER,
- } from './graphql/shop-definitions';
- import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
- import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
- describe('Promotions applied to Orders', () => {
- const { server, adminClient, shopClient } = createTestEnvironment({
- ...testConfig,
- paymentOptions: {
- paymentMethodHandlers: [testSuccessfulPaymentMethod],
- },
- });
- const freeOrderAction = {
- code: orderPercentageDiscount.code,
- arguments: [{ name: 'discount', type: 'int', value: '100' }],
- };
- const minOrderAmountCondition = (min: number) => ({
- code: minimumOrderAmount.code,
- arguments: [
- { name: 'amount', type: 'int', value: min.toString() },
- { name: 'taxInclusive', type: 'boolean', value: 'true' },
- ],
- });
- let products: GetPromoProducts.Items[];
- beforeAll(async () => {
- await server.init({
- dataDir: path.join(__dirname, '__data__'),
- initialData,
- productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
- customerCount: 2,
- });
- await adminClient.asSuperAdmin();
- await getProducts();
- await createGlobalPromotions();
- }, TEST_SETUP_TIMEOUT_MS);
- afterAll(async () => {
- await server.destroy();
- });
- describe('coupon codes', () => {
- const TEST_COUPON_CODE = 'TESTCOUPON';
- const EXPIRED_COUPON_CODE = 'EXPIRED';
- let promoFreeWithCoupon: CreatePromotion.CreatePromotion;
- let promoFreeWithExpiredCoupon: CreatePromotion.CreatePromotion;
- beforeAll(async () => {
- promoFreeWithCoupon = await createPromotion({
- enabled: true,
- name: 'Free with test coupon',
- couponCode: TEST_COUPON_CODE,
- conditions: [],
- actions: [freeOrderAction],
- });
- promoFreeWithExpiredCoupon = await createPromotion({
- enabled: true,
- name: 'Expired coupon',
- endsAt: new Date(2010, 0, 0),
- couponCode: EXPIRED_COUPON_CODE,
- conditions: [],
- actions: [freeOrderAction],
- });
- await shopClient.asAnonymousUser();
- const item60 = getVariantBySlug('item-60');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: item60.id,
- quantity: 1,
- });
- });
- afterAll(async () => {
- await deletePromotion(promoFreeWithCoupon.id);
- await deletePromotion(promoFreeWithExpiredCoupon.id);
- });
- it(
- 'applyCouponCode throws with nonexistant code',
- assertThrowsWithMessage(async () => {
- await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
- APPLY_COUPON_CODE,
- {
- couponCode: 'bad code',
- },
- );
- }, 'Coupon code "bad code" is not valid'),
- );
- it(
- 'applyCouponCode throws with expired code',
- assertThrowsWithMessage(async () => {
- await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
- APPLY_COUPON_CODE,
- {
- couponCode: EXPIRED_COUPON_CODE,
- },
- );
- }, `Coupon code "${EXPIRED_COUPON_CODE}" has expired`),
- );
- it('applies a valid coupon code', async () => {
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, {
- couponCode: TEST_COUPON_CODE,
- });
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- expect(applyCouponCode!.adjustments.length).toBe(1);
- expect(applyCouponCode!.adjustments[0].description).toBe('Free with test coupon');
- expect(applyCouponCode!.total).toBe(0);
- });
- it('order history records application', async () => {
- const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
- expect(activeOrder!.history.items).toEqual([
- {
- id: 'T_1',
- type: HistoryEntryType.ORDER_COUPON_APPLIED,
- data: {
- couponCode: TEST_COUPON_CODE,
- promotionId: 'T_3',
- },
- },
- ]);
- });
- it('de-duplicates existing codes', async () => {
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, {
- couponCode: TEST_COUPON_CODE,
- });
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- });
- it('removes a coupon code', async () => {
- const { removeCouponCode } = await shopClient.query<
- RemoveCouponCode.Mutation,
- RemoveCouponCode.Variables
- >(REMOVE_COUPON_CODE, {
- couponCode: TEST_COUPON_CODE,
- });
- expect(removeCouponCode!.adjustments.length).toBe(0);
- expect(removeCouponCode!.total).toBe(6000);
- });
- it('order history records removal', async () => {
- const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
- expect(activeOrder!.history.items).toEqual([
- {
- id: 'T_1',
- type: HistoryEntryType.ORDER_COUPON_APPLIED,
- data: {
- couponCode: TEST_COUPON_CODE,
- promotionId: 'T_3',
- },
- },
- {
- id: 'T_2',
- type: HistoryEntryType.ORDER_COUPON_REMOVED,
- data: {
- couponCode: TEST_COUPON_CODE,
- },
- },
- ]);
- });
- it('does not record removal of coupon code that was not added', async () => {
- const { removeCouponCode } = await shopClient.query<
- RemoveCouponCode.Mutation,
- RemoveCouponCode.Variables
- >(REMOVE_COUPON_CODE, {
- couponCode: 'NOT_THERE',
- });
- expect(removeCouponCode!.history.items).toEqual([
- {
- id: 'T_1',
- type: HistoryEntryType.ORDER_COUPON_APPLIED,
- data: {
- couponCode: TEST_COUPON_CODE,
- promotionId: 'T_3',
- },
- },
- {
- id: 'T_2',
- type: HistoryEntryType.ORDER_COUPON_REMOVED,
- data: {
- couponCode: TEST_COUPON_CODE,
- },
- },
- ]);
- });
- });
- describe('default PromotionConditions', () => {
- beforeEach(async () => {
- await shopClient.asAnonymousUser();
- });
- it('minimumOrderAmount', async () => {
- const promotion = await createPromotion({
- enabled: true,
- name: 'Free if order total greater than 100',
- conditions: [minOrderAmountCondition(10000)],
- actions: [freeOrderAction],
- });
- const item60 = getVariantBySlug('item-60');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: item60.id,
- quantity: 1,
- });
- expect(addItemToOrder!.total).toBe(6000);
- expect(addItemToOrder!.adjustments.length).toBe(0);
- const { adjustOrderLine } = await shopClient.query<
- AdjustItemQuantity.Mutation,
- AdjustItemQuantity.Variables
- >(ADJUST_ITEM_QUANTITY, {
- orderLineId: addItemToOrder!.lines[0].id,
- quantity: 2,
- });
- expect(adjustOrderLine!.total).toBe(0);
- expect(adjustOrderLine!.adjustments[0].description).toBe('Free if order total greater than 100');
- expect(adjustOrderLine!.adjustments[0].amount).toBe(-12000);
- await deletePromotion(promotion.id);
- });
- it('atLeastNWithFacets', async () => {
- const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
- const saleFacetValue = facets.items[0].values[0];
- const promotion = await createPromotion({
- enabled: true,
- name: 'Free if order contains 2 items with Sale facet value',
- conditions: [
- {
- code: atLeastNWithFacets.code,
- arguments: [
- { name: 'minimum', type: 'int', value: '2' },
- { name: 'facets', type: 'facetValueIds', value: `["${saleFacetValue.id}"]` },
- ],
- },
- ],
- actions: [freeOrderAction],
- });
- const itemSale1 = getVariantBySlug('item-sale-1');
- const itemSale12 = getVariantBySlug('item-sale-12');
- const { addItemToOrder: res1 } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: itemSale1.id,
- quantity: 1,
- });
- expect(res1!.total).toBe(120);
- expect(res1!.adjustments.length).toBe(0);
- const { addItemToOrder: res2 } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: itemSale12.id,
- quantity: 1,
- });
- expect(res2!.total).toBe(0);
- expect(res2!.adjustments.length).toBe(1);
- expect(res2!.total).toBe(0);
- expect(res2!.adjustments[0].description).toBe(
- 'Free if order contains 2 items with Sale facet value',
- );
- expect(res2!.adjustments[0].amount).toBe(-1320);
- await deletePromotion(promotion.id);
- });
- });
- describe('default PromotionActions', () => {
- beforeEach(async () => {
- await shopClient.asAnonymousUser();
- });
- it('orderPercentageDiscount', async () => {
- const couponCode = '50%_off_order';
- const promotion = await createPromotion({
- enabled: true,
- name: '50% discount on order',
- couponCode,
- conditions: [],
- actions: [
- {
- code: orderPercentageDiscount.code,
- arguments: [{ name: 'discount', type: 'int', value: '50' }],
- },
- ],
- });
- const item60 = getVariantBySlug('item-60');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: item60.id,
- quantity: 1,
- });
- expect(addItemToOrder!.total).toBe(6000);
- expect(addItemToOrder!.adjustments.length).toBe(0);
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, {
- couponCode,
- });
- expect(applyCouponCode!.adjustments.length).toBe(1);
- expect(applyCouponCode!.adjustments[0].description).toBe('50% discount on order');
- expect(applyCouponCode!.total).toBe(3000);
- await deletePromotion(promotion.id);
- });
- it('discountOnItemWithFacets', async () => {
- const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
- const saleFacetValue = facets.items[0].values[0];
- const couponCode = '50%_off_sale_items';
- const promotion = await createPromotion({
- enabled: true,
- name: '50% off sale items',
- couponCode,
- conditions: [],
- actions: [
- {
- code: discountOnItemWithFacets.code,
- arguments: [
- { name: 'discount', type: 'int', value: '50' },
- { name: 'facets', type: 'facetValueIds', value: `["${saleFacetValue.id}"]` },
- ],
- },
- ],
- });
- await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
- productVariantId: getVariantBySlug('item-12').id,
- quantity: 1,
- });
- await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
- productVariantId: getVariantBySlug('item-sale-12').id,
- quantity: 1,
- });
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: getVariantBySlug('item-sale-1').id,
- quantity: 2,
- });
- expect(addItemToOrder!.adjustments.length).toBe(0);
- expect(addItemToOrder!.total).toBe(2640);
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, {
- couponCode,
- });
- expect(applyCouponCode!.total).toBe(1920);
- await deletePromotion(promotion.id);
- });
- });
- describe('per-customer usage limit', () => {
- const TEST_COUPON_CODE = 'TESTCOUPON';
- let promoWithUsageLimit: CreatePromotion.CreatePromotion;
- beforeAll(async () => {
- promoWithUsageLimit = await createPromotion({
- enabled: true,
- name: 'Free with test coupon',
- couponCode: TEST_COUPON_CODE,
- perCustomerUsageLimit: 1,
- conditions: [],
- actions: [freeOrderAction],
- });
- });
- afterAll(async () => {
- await deletePromotion(promoWithUsageLimit.id);
- });
- async function createNewActiveOrder() {
- const item60 = getVariantBySlug('item-60');
- const { addItemToOrder } = await shopClient.query<
- AddItemToOrder.Mutation,
- AddItemToOrder.Variables
- >(ADD_ITEM_TO_ORDER, {
- productVariantId: item60.id,
- quantity: 1,
- });
- return addItemToOrder;
- }
- describe('guest customer', () => {
- const GUEST_EMAIL_ADDRESS = 'guest@test.com';
- let orderCode: string;
- function addGuestCustomerToOrder() {
- return shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
- SET_CUSTOMER,
- {
- input: {
- emailAddress: GUEST_EMAIL_ADDRESS,
- firstName: 'Guest',
- lastName: 'Customer',
- },
- },
- );
- }
- it('allows initial usage', async () => {
- await shopClient.asAnonymousUser();
- await createNewActiveOrder();
- await addGuestCustomerToOrder();
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
- expect(applyCouponCode!.total).toBe(0);
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- await proceedToArrangingPayment(shopClient);
- const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
- expect(order.state).toBe('PaymentSettled');
- expect(order.active).toBe(false);
- orderCode = order.code;
- });
- it('adds Promotions to Order once payment arranged', async () => {
- const { orderByCode } = await shopClient.query<
- GetOrderPromotionsByCode.Query,
- GetOrderPromotionsByCode.Variables
- >(GET_ORDER_PROMOTIONS_BY_CODE, {
- code: orderCode,
- });
- expect(orderByCode!.promotions.map(pick(['id', 'name']))).toEqual([
- { id: 'T_9', name: 'Free with test coupon' },
- ]);
- });
- it('throws when usage exceeds limit', async () => {
- await shopClient.asAnonymousUser();
- await createNewActiveOrder();
- await addGuestCustomerToOrder();
- try {
- await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
- APPLY_COUPON_CODE,
- { couponCode: TEST_COUPON_CODE },
- );
- fail('should have thrown');
- } catch (err) {
- expect(err.message).toEqual(
- expect.stringContaining('Coupon code cannot be used more than once per customer'),
- );
- }
- });
- it('removes couponCode from order when adding customer after code applied', async () => {
- await shopClient.asAnonymousUser();
- await createNewActiveOrder();
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
- expect(applyCouponCode!.total).toBe(0);
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- await addGuestCustomerToOrder();
- const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
- expect(activeOrder!.couponCodes).toEqual([]);
- expect(activeOrder!.total).toBe(6000);
- });
- });
- describe('signed-in customer', () => {
- function logInAsRegisteredCustomer() {
- return shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
- }
- it('allows initial usage', async () => {
- await logInAsRegisteredCustomer();
- await createNewActiveOrder();
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
- expect(applyCouponCode!.total).toBe(0);
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- await proceedToArrangingPayment(shopClient);
- const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
- expect(order.state).toBe('PaymentSettled');
- expect(order.active).toBe(false);
- });
- it('throws when usage exceeds limit', async () => {
- await logInAsRegisteredCustomer();
- await createNewActiveOrder();
- try {
- await shopClient.query<ApplyCouponCode.Mutation, ApplyCouponCode.Variables>(
- APPLY_COUPON_CODE,
- { couponCode: TEST_COUPON_CODE },
- );
- fail('should have thrown');
- } catch (err) {
- expect(err.message).toEqual(
- expect.stringContaining('Coupon code cannot be used more than once per customer'),
- );
- }
- });
- it('removes couponCode from order when logging in after code applied', async () => {
- await shopClient.asAnonymousUser();
- await createNewActiveOrder();
- const { applyCouponCode } = await shopClient.query<
- ApplyCouponCode.Mutation,
- ApplyCouponCode.Variables
- >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
- expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
- expect(applyCouponCode!.total).toBe(0);
- await logInAsRegisteredCustomer();
- const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
- expect(activeOrder!.total).toBe(6000);
- expect(activeOrder!.couponCodes).toEqual([]);
- });
- });
- });
- async function getProducts() {
- const result = await adminClient.query<GetPromoProducts.Query>(GET_PROMO_PRODUCTS, {
- options: {
- take: 10,
- skip: 0,
- },
- });
- products = result.products.items;
- }
- async function createGlobalPromotions() {
- const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
- const saleFacetValue = facets.items[0].values[0];
- await createPromotion({
- enabled: true,
- name: 'Promo not yet started',
- startsAt: new Date(2199, 0, 0),
- conditions: [minOrderAmountCondition(100)],
- actions: [freeOrderAction],
- });
- const deletedPromotion = await createPromotion({
- enabled: true,
- name: 'Deleted promotion',
- conditions: [minOrderAmountCondition(100)],
- actions: [freeOrderAction],
- });
- await deletePromotion(deletedPromotion.id);
- }
- async function createPromotion(input: CreatePromotionInput): Promise<CreatePromotion.CreatePromotion> {
- const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
- CREATE_PROMOTION,
- {
- input,
- },
- );
- return result.createPromotion;
- }
- function getVariantBySlug(
- slug: 'item-1' | 'item-12' | 'item-60' | 'item-sale-1' | 'item-sale-12',
- ): GetPromoProducts.Variants {
- return products.find(p => p.slug === slug)!.variants[0];
- }
- async function deletePromotion(promotionId: string) {
- await adminClient.query(gql`
- mutation DeletePromotionAdHoc1 {
- deletePromotion(id: "${promotionId}") {
- result
- }
- }
- `);
- }
- });
- export const GET_PROMO_PRODUCTS = gql`
- query GetPromoProducts {
- products {
- items {
- id
- slug
- variants {
- id
- price
- priceWithTax
- sku
- facetValues {
- id
- code
- }
- }
- }
- }
- }
- `;
|