order-promotion.e2e-spec.ts 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563
  1. /* tslint:disable:no-non-null-assertion */
  2. import { omit } from '@vendure/common/lib/omit';
  3. import { pick } from '@vendure/common/lib/pick';
  4. import {
  5. containsProducts,
  6. customerGroup,
  7. defaultShippingCalculator,
  8. defaultShippingEligibilityChecker,
  9. discountOnItemWithFacets,
  10. hasFacetValues,
  11. manualFulfillmentHandler,
  12. minimumOrderAmount,
  13. orderPercentageDiscount,
  14. productsPercentageDiscount,
  15. } from '@vendure/core';
  16. import {
  17. createErrorResultGuard,
  18. createTestEnvironment,
  19. E2E_DEFAULT_CHANNEL_TOKEN,
  20. ErrorResultGuard,
  21. } from '@vendure/testing';
  22. import gql from 'graphql-tag';
  23. import path from 'path';
  24. import { initialData } from '../../../e2e-common/e2e-initial-data';
  25. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  26. import { freeShipping } from '../src/config/promotion/actions/free-shipping-action';
  27. import { orderFixedDiscount } from '../src/config/promotion/actions/order-fixed-discount-action';
  28. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  29. import {
  30. AssignProductsToChannel,
  31. AssignPromotionToChannel,
  32. ChannelFragment,
  33. CreateChannel,
  34. CreateCustomerGroup,
  35. CreatePromotion,
  36. CreatePromotionInput,
  37. CreateShippingMethod,
  38. CurrencyCode,
  39. GetFacetList,
  40. GetProductsWithVariantPrices,
  41. HistoryEntryType,
  42. LanguageCode,
  43. PromotionFragment,
  44. RemoveCustomersFromGroup,
  45. } from './graphql/generated-e2e-admin-types';
  46. import {
  47. AddItemToOrder,
  48. AdjustItemQuantity,
  49. AdjustmentType,
  50. ApplyCouponCode,
  51. ErrorCode,
  52. GetActiveOrder,
  53. GetOrderPromotionsByCode,
  54. RemoveCouponCode,
  55. RemoveItemFromOrder,
  56. SetCustomerForOrder,
  57. SetShippingMethod,
  58. TestOrderFragmentFragment,
  59. TestOrderWithPaymentsFragment,
  60. UpdatedOrderFragment,
  61. } from './graphql/generated-e2e-shop-types';
  62. import {
  63. ASSIGN_PRODUCT_TO_CHANNEL,
  64. ASSIGN_PROMOTIONS_TO_CHANNEL,
  65. CREATE_CHANNEL,
  66. CREATE_CUSTOMER_GROUP,
  67. CREATE_PROMOTION,
  68. CREATE_SHIPPING_METHOD,
  69. GET_FACET_LIST,
  70. GET_PRODUCTS_WITH_VARIANT_PRICES,
  71. REMOVE_CUSTOMERS_FROM_GROUP,
  72. } from './graphql/shared-definitions';
  73. import {
  74. ADD_ITEM_TO_ORDER,
  75. ADJUST_ITEM_QUANTITY,
  76. APPLY_COUPON_CODE,
  77. GET_ACTIVE_ORDER,
  78. GET_ORDER_PROMOTIONS_BY_CODE,
  79. REMOVE_COUPON_CODE,
  80. REMOVE_ITEM_FROM_ORDER,
  81. SET_CUSTOMER,
  82. SET_SHIPPING_METHOD,
  83. } from './graphql/shop-definitions';
  84. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  85. describe('Promotions applied to Orders', () => {
  86. const { server, adminClient, shopClient } = createTestEnvironment({
  87. ...testConfig(),
  88. paymentOptions: {
  89. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  90. },
  91. });
  92. const freeOrderAction = {
  93. code: orderPercentageDiscount.code,
  94. arguments: [{ name: 'discount', value: '100' }],
  95. };
  96. const minOrderAmountCondition = (min: number) => ({
  97. code: minimumOrderAmount.code,
  98. arguments: [
  99. { name: 'amount', value: min.toString() },
  100. { name: 'taxInclusive', value: 'true' },
  101. ],
  102. });
  103. type OrderSuccessResult = UpdatedOrderFragment | TestOrderFragmentFragment;
  104. const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
  105. input => !!input.lines,
  106. );
  107. let products: GetProductsWithVariantPrices.Items[];
  108. beforeAll(async () => {
  109. await server.init({
  110. initialData: {
  111. ...initialData,
  112. paymentMethods: [
  113. {
  114. name: testSuccessfulPaymentMethod.code,
  115. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  116. },
  117. ],
  118. },
  119. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
  120. customerCount: 2,
  121. });
  122. await adminClient.asSuperAdmin();
  123. await getProducts();
  124. await createGlobalPromotions();
  125. }, TEST_SETUP_TIMEOUT_MS);
  126. afterAll(async () => {
  127. await server.destroy();
  128. });
  129. describe('coupon codes', () => {
  130. const TEST_COUPON_CODE = 'TESTCOUPON';
  131. const EXPIRED_COUPON_CODE = 'EXPIRED';
  132. let promoFreeWithCoupon: PromotionFragment;
  133. let promoFreeWithExpiredCoupon: PromotionFragment;
  134. beforeAll(async () => {
  135. promoFreeWithCoupon = await createPromotion({
  136. enabled: true,
  137. name: 'Free with test coupon',
  138. couponCode: TEST_COUPON_CODE,
  139. conditions: [],
  140. actions: [freeOrderAction],
  141. });
  142. promoFreeWithExpiredCoupon = await createPromotion({
  143. enabled: true,
  144. name: 'Expired coupon',
  145. endsAt: new Date(2010, 0, 0),
  146. couponCode: EXPIRED_COUPON_CODE,
  147. conditions: [],
  148. actions: [freeOrderAction],
  149. });
  150. await shopClient.asAnonymousUser();
  151. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  152. productVariantId: getVariantBySlug('item-5000').id,
  153. quantity: 1,
  154. });
  155. });
  156. afterAll(async () => {
  157. await deletePromotion(promoFreeWithCoupon.id);
  158. await deletePromotion(promoFreeWithExpiredCoupon.id);
  159. });
  160. it('applyCouponCode returns error result when code is nonexistant', async () => {
  161. const { applyCouponCode } = await shopClient.query<
  162. ApplyCouponCode.Mutation,
  163. ApplyCouponCode.Variables
  164. >(APPLY_COUPON_CODE, {
  165. couponCode: 'bad code',
  166. });
  167. orderResultGuard.assertErrorResult(applyCouponCode);
  168. expect(applyCouponCode.message).toBe('Coupon code "bad code" is not valid');
  169. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_INVALID_ERROR);
  170. });
  171. it('applyCouponCode returns error when code is expired', async () => {
  172. const { applyCouponCode } = await shopClient.query<
  173. ApplyCouponCode.Mutation,
  174. ApplyCouponCode.Variables
  175. >(APPLY_COUPON_CODE, {
  176. couponCode: EXPIRED_COUPON_CODE,
  177. });
  178. orderResultGuard.assertErrorResult(applyCouponCode);
  179. expect(applyCouponCode.message).toBe(`Coupon code "${EXPIRED_COUPON_CODE}" has expired`);
  180. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_EXPIRED_ERROR);
  181. });
  182. it('applies a valid coupon code', async () => {
  183. const { applyCouponCode } = await shopClient.query<
  184. ApplyCouponCode.Mutation,
  185. ApplyCouponCode.Variables
  186. >(APPLY_COUPON_CODE, {
  187. couponCode: TEST_COUPON_CODE,
  188. });
  189. orderResultGuard.assertSuccess(applyCouponCode);
  190. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  191. expect(applyCouponCode!.discounts.length).toBe(1);
  192. expect(applyCouponCode!.discounts[0].description).toBe('Free with test coupon');
  193. expect(applyCouponCode!.totalWithTax).toBe(0);
  194. });
  195. it('order history records application', async () => {
  196. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  197. expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
  198. {
  199. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  200. data: {
  201. from: 'Created',
  202. to: 'AddingItems',
  203. },
  204. },
  205. {
  206. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  207. data: {
  208. couponCode: TEST_COUPON_CODE,
  209. promotionId: 'T_3',
  210. },
  211. },
  212. ]);
  213. });
  214. it('de-duplicates existing codes', async () => {
  215. const { applyCouponCode } = await shopClient.query<
  216. ApplyCouponCode.Mutation,
  217. ApplyCouponCode.Variables
  218. >(APPLY_COUPON_CODE, {
  219. couponCode: TEST_COUPON_CODE,
  220. });
  221. orderResultGuard.assertSuccess(applyCouponCode);
  222. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  223. });
  224. it('removes a coupon code', async () => {
  225. const { removeCouponCode } = await shopClient.query<
  226. RemoveCouponCode.Mutation,
  227. RemoveCouponCode.Variables
  228. >(REMOVE_COUPON_CODE, {
  229. couponCode: TEST_COUPON_CODE,
  230. });
  231. expect(removeCouponCode!.discounts.length).toBe(0);
  232. expect(removeCouponCode!.totalWithTax).toBe(6000);
  233. });
  234. // https://github.com/vendure-ecommerce/vendure/issues/649
  235. it('discounts array cleared after coupon code removed', async () => {
  236. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  237. expect(activeOrder?.discounts).toEqual([]);
  238. });
  239. it('order history records removal', async () => {
  240. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  241. expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
  242. {
  243. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  244. data: {
  245. from: 'Created',
  246. to: 'AddingItems',
  247. },
  248. },
  249. {
  250. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  251. data: {
  252. couponCode: TEST_COUPON_CODE,
  253. promotionId: 'T_3',
  254. },
  255. },
  256. {
  257. type: HistoryEntryType.ORDER_COUPON_REMOVED,
  258. data: {
  259. couponCode: TEST_COUPON_CODE,
  260. },
  261. },
  262. ]);
  263. });
  264. it('does not record removal of coupon code that was not added', async () => {
  265. const { removeCouponCode } = await shopClient.query<
  266. RemoveCouponCode.Mutation,
  267. RemoveCouponCode.Variables
  268. >(REMOVE_COUPON_CODE, {
  269. couponCode: 'NOT_THERE',
  270. });
  271. expect(removeCouponCode!.history.items.map(i => omit(i, ['id']))).toEqual([
  272. {
  273. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  274. data: {
  275. from: 'Created',
  276. to: 'AddingItems',
  277. },
  278. },
  279. {
  280. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  281. data: {
  282. couponCode: TEST_COUPON_CODE,
  283. promotionId: 'T_3',
  284. },
  285. },
  286. {
  287. type: HistoryEntryType.ORDER_COUPON_REMOVED,
  288. data: {
  289. couponCode: TEST_COUPON_CODE,
  290. },
  291. },
  292. ]);
  293. });
  294. });
  295. describe('default PromotionConditions', () => {
  296. beforeEach(async () => {
  297. await shopClient.asAnonymousUser();
  298. });
  299. it('minimumOrderAmount', async () => {
  300. const promotion = await createPromotion({
  301. enabled: true,
  302. name: 'Free if order total greater than 100',
  303. conditions: [minOrderAmountCondition(10000)],
  304. actions: [freeOrderAction],
  305. });
  306. const { addItemToOrder } = await shopClient.query<
  307. AddItemToOrder.Mutation,
  308. AddItemToOrder.Variables
  309. >(ADD_ITEM_TO_ORDER, {
  310. productVariantId: getVariantBySlug('item-5000').id,
  311. quantity: 1,
  312. });
  313. orderResultGuard.assertSuccess(addItemToOrder);
  314. expect(addItemToOrder!.totalWithTax).toBe(6000);
  315. expect(addItemToOrder!.discounts.length).toBe(0);
  316. const { adjustOrderLine } = await shopClient.query<
  317. AdjustItemQuantity.Mutation,
  318. AdjustItemQuantity.Variables
  319. >(ADJUST_ITEM_QUANTITY, {
  320. orderLineId: addItemToOrder!.lines[0].id,
  321. quantity: 2,
  322. });
  323. orderResultGuard.assertSuccess(adjustOrderLine);
  324. expect(adjustOrderLine!.totalWithTax).toBe(0);
  325. expect(adjustOrderLine!.discounts[0].description).toBe('Free if order total greater than 100');
  326. expect(adjustOrderLine!.discounts[0].amountWithTax).toBe(-12000);
  327. await deletePromotion(promotion.id);
  328. });
  329. it('atLeastNWithFacets', async () => {
  330. const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  331. const saleFacetValue = facets.items[0].values[0];
  332. const promotion = await createPromotion({
  333. enabled: true,
  334. name: 'Free if order contains 2 items with Sale facet value',
  335. conditions: [
  336. {
  337. code: hasFacetValues.code,
  338. arguments: [
  339. { name: 'minimum', value: '2' },
  340. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  341. ],
  342. },
  343. ],
  344. actions: [freeOrderAction],
  345. });
  346. const { addItemToOrder: res1 } = await shopClient.query<
  347. AddItemToOrder.Mutation,
  348. AddItemToOrder.Variables
  349. >(ADD_ITEM_TO_ORDER, {
  350. productVariantId: getVariantBySlug('item-sale-100').id,
  351. quantity: 1,
  352. });
  353. orderResultGuard.assertSuccess(res1);
  354. expect(res1!.totalWithTax).toBe(120);
  355. expect(res1!.discounts.length).toBe(0);
  356. const { addItemToOrder: res2 } = await shopClient.query<
  357. AddItemToOrder.Mutation,
  358. AddItemToOrder.Variables
  359. >(ADD_ITEM_TO_ORDER, {
  360. productVariantId: getVariantBySlug('item-sale-1000').id,
  361. quantity: 1,
  362. });
  363. orderResultGuard.assertSuccess(res2);
  364. expect(res2!.totalWithTax).toBe(0);
  365. expect(res2!.discounts.length).toBe(1);
  366. expect(res2!.totalWithTax).toBe(0);
  367. expect(res2!.discounts[0].description).toBe(
  368. 'Free if order contains 2 items with Sale facet value',
  369. );
  370. expect(res2!.discounts[0].amountWithTax).toBe(-1320);
  371. await deletePromotion(promotion.id);
  372. });
  373. it('containsProducts', async () => {
  374. const promotion = await createPromotion({
  375. enabled: true,
  376. name: 'Free if buying 3 or more offer products',
  377. conditions: [
  378. {
  379. code: containsProducts.code,
  380. arguments: [
  381. { name: 'minimum', value: '3' },
  382. {
  383. name: 'productVariantIds',
  384. value: JSON.stringify([
  385. getVariantBySlug('item-5000').id,
  386. getVariantBySlug('item-1000').id,
  387. ]),
  388. },
  389. ],
  390. },
  391. ],
  392. actions: [freeOrderAction],
  393. });
  394. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  395. productVariantId: getVariantBySlug('item-5000').id,
  396. quantity: 1,
  397. });
  398. const { addItemToOrder } = await shopClient.query<
  399. AddItemToOrder.Mutation,
  400. AddItemToOrder.Variables
  401. >(ADD_ITEM_TO_ORDER, {
  402. productVariantId: getVariantBySlug('item-1000').id,
  403. quantity: 1,
  404. });
  405. orderResultGuard.assertSuccess(addItemToOrder);
  406. expect(addItemToOrder!.totalWithTax).toBe(7200);
  407. expect(addItemToOrder!.discounts.length).toBe(0);
  408. const { adjustOrderLine } = await shopClient.query<
  409. AdjustItemQuantity.Mutation,
  410. AdjustItemQuantity.Variables
  411. >(ADJUST_ITEM_QUANTITY, {
  412. orderLineId: addItemToOrder!.lines[0].id,
  413. quantity: 2,
  414. });
  415. orderResultGuard.assertSuccess(adjustOrderLine);
  416. expect(adjustOrderLine!.total).toBe(0);
  417. expect(adjustOrderLine!.discounts[0].description).toBe('Free if buying 3 or more offer products');
  418. expect(adjustOrderLine!.discounts[0].amountWithTax).toBe(-13200);
  419. await deletePromotion(promotion.id);
  420. });
  421. it('customerGroup', async () => {
  422. const { createCustomerGroup } = await adminClient.query<
  423. CreateCustomerGroup.Mutation,
  424. CreateCustomerGroup.Variables
  425. >(CREATE_CUSTOMER_GROUP, {
  426. input: { name: 'Test Group', customerIds: ['T_1'] },
  427. });
  428. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  429. const promotion = await createPromotion({
  430. enabled: true,
  431. name: 'Free for group members',
  432. conditions: [
  433. {
  434. code: customerGroup.code,
  435. arguments: [{ name: 'customerGroupId', value: createCustomerGroup.id }],
  436. },
  437. ],
  438. actions: [freeOrderAction],
  439. });
  440. const { addItemToOrder } = await shopClient.query<
  441. AddItemToOrder.Mutation,
  442. AddItemToOrder.Variables
  443. >(ADD_ITEM_TO_ORDER, {
  444. productVariantId: getVariantBySlug('item-5000').id,
  445. quantity: 1,
  446. });
  447. orderResultGuard.assertSuccess(addItemToOrder);
  448. expect(addItemToOrder!.totalWithTax).toBe(0);
  449. expect(addItemToOrder!.discounts.length).toBe(1);
  450. expect(addItemToOrder!.discounts[0].description).toBe('Free for group members');
  451. expect(addItemToOrder!.discounts[0].amountWithTax).toBe(-6000);
  452. await adminClient.query<RemoveCustomersFromGroup.Mutation, RemoveCustomersFromGroup.Variables>(
  453. REMOVE_CUSTOMERS_FROM_GROUP,
  454. {
  455. groupId: createCustomerGroup.id,
  456. customerIds: ['T_1'],
  457. },
  458. );
  459. const { adjustOrderLine } = await shopClient.query<
  460. AdjustItemQuantity.Mutation,
  461. AdjustItemQuantity.Variables
  462. >(ADJUST_ITEM_QUANTITY, {
  463. orderLineId: addItemToOrder!.lines[0].id,
  464. quantity: 2,
  465. });
  466. orderResultGuard.assertSuccess(adjustOrderLine);
  467. expect(adjustOrderLine!.totalWithTax).toBe(12000);
  468. expect(adjustOrderLine!.discounts.length).toBe(0);
  469. await deletePromotion(promotion.id);
  470. });
  471. });
  472. describe('default PromotionActions', () => {
  473. const TAX_INCLUDED_CHANNEL_TOKEN = 'tax_included_channel';
  474. let taxIncludedChannel: ChannelFragment;
  475. beforeAll(async () => {
  476. // Create a channel where the prices include tax, so we can ensure
  477. // that PromotionActions are working as expected when taxes are included
  478. const { createChannel } = await adminClient.query<
  479. CreateChannel.Mutation,
  480. CreateChannel.Variables
  481. >(CREATE_CHANNEL, {
  482. input: {
  483. code: 'tax-included-channel',
  484. currencyCode: CurrencyCode.GBP,
  485. pricesIncludeTax: true,
  486. defaultTaxZoneId: 'T_1',
  487. defaultShippingZoneId: 'T_1',
  488. defaultLanguageCode: LanguageCode.en,
  489. token: TAX_INCLUDED_CHANNEL_TOKEN,
  490. },
  491. });
  492. taxIncludedChannel = createChannel as ChannelFragment;
  493. await adminClient.query<AssignProductsToChannel.Mutation, AssignProductsToChannel.Variables>(
  494. ASSIGN_PRODUCT_TO_CHANNEL,
  495. {
  496. input: {
  497. channelId: taxIncludedChannel.id,
  498. priceFactor: 1,
  499. productIds: products.map(p => p.id),
  500. },
  501. },
  502. );
  503. });
  504. beforeEach(async () => {
  505. await shopClient.asAnonymousUser();
  506. });
  507. async function assignPromotionToTaxIncludedChannel(promotionId: string | string[]) {
  508. await adminClient.query<AssignPromotionToChannel.Mutation, AssignPromotionToChannel.Variables>(
  509. ASSIGN_PROMOTIONS_TO_CHANNEL,
  510. {
  511. input: {
  512. promotionIds: Array.isArray(promotionId) ? promotionId : [promotionId],
  513. channelId: taxIncludedChannel.id,
  514. },
  515. },
  516. );
  517. }
  518. describe('orderPercentageDiscount', () => {
  519. const couponCode = '50%_off_order';
  520. let promotion: PromotionFragment;
  521. beforeAll(async () => {
  522. promotion = await createPromotion({
  523. enabled: true,
  524. name: '20% discount on order',
  525. couponCode,
  526. conditions: [],
  527. actions: [
  528. {
  529. code: orderPercentageDiscount.code,
  530. arguments: [{ name: 'discount', value: '20' }],
  531. },
  532. ],
  533. });
  534. await assignPromotionToTaxIncludedChannel(promotion.id);
  535. });
  536. afterAll(async () => {
  537. await deletePromotion(promotion.id);
  538. });
  539. it('prices exclude tax', async () => {
  540. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  541. const { addItemToOrder } = await shopClient.query<
  542. AddItemToOrder.Mutation,
  543. AddItemToOrder.Variables
  544. >(ADD_ITEM_TO_ORDER, {
  545. productVariantId: getVariantBySlug('item-5000').id,
  546. quantity: 1,
  547. });
  548. orderResultGuard.assertSuccess(addItemToOrder);
  549. expect(addItemToOrder!.totalWithTax).toBe(6000);
  550. expect(addItemToOrder!.discounts.length).toBe(0);
  551. const { applyCouponCode } = await shopClient.query<
  552. ApplyCouponCode.Mutation,
  553. ApplyCouponCode.Variables
  554. >(APPLY_COUPON_CODE, {
  555. couponCode,
  556. });
  557. orderResultGuard.assertSuccess(applyCouponCode);
  558. expect(applyCouponCode!.discounts.length).toBe(1);
  559. expect(applyCouponCode!.discounts[0].description).toBe('20% discount on order');
  560. expect(applyCouponCode!.totalWithTax).toBe(4800);
  561. });
  562. it('prices include tax', async () => {
  563. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  564. const { addItemToOrder } = await shopClient.query<
  565. AddItemToOrder.Mutation,
  566. AddItemToOrder.Variables
  567. >(ADD_ITEM_TO_ORDER, {
  568. productVariantId: getVariantBySlug('item-5000').id,
  569. quantity: 1,
  570. });
  571. orderResultGuard.assertSuccess(addItemToOrder);
  572. expect(addItemToOrder!.totalWithTax).toBe(6000);
  573. expect(addItemToOrder!.discounts.length).toBe(0);
  574. const { applyCouponCode } = await shopClient.query<
  575. ApplyCouponCode.Mutation,
  576. ApplyCouponCode.Variables
  577. >(APPLY_COUPON_CODE, {
  578. couponCode,
  579. });
  580. orderResultGuard.assertSuccess(applyCouponCode);
  581. expect(applyCouponCode!.discounts.length).toBe(1);
  582. expect(applyCouponCode!.discounts[0].description).toBe('20% discount on order');
  583. expect(applyCouponCode!.totalWithTax).toBe(4800);
  584. });
  585. });
  586. describe('orderFixedDiscount', () => {
  587. const couponCode = '10_off_order';
  588. let promotion: PromotionFragment;
  589. beforeAll(async () => {
  590. promotion = await createPromotion({
  591. enabled: true,
  592. name: '$10 discount on order',
  593. couponCode,
  594. conditions: [],
  595. actions: [
  596. {
  597. code: orderFixedDiscount.code,
  598. arguments: [{ name: 'discount', value: '1000' }],
  599. },
  600. ],
  601. });
  602. await assignPromotionToTaxIncludedChannel(promotion.id);
  603. });
  604. afterAll(async () => {
  605. await deletePromotion(promotion.id);
  606. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  607. });
  608. it('prices exclude tax', async () => {
  609. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  610. const { addItemToOrder } = await shopClient.query<
  611. AddItemToOrder.Mutation,
  612. AddItemToOrder.Variables
  613. >(ADD_ITEM_TO_ORDER, {
  614. productVariantId: getVariantBySlug('item-5000').id,
  615. quantity: 1,
  616. });
  617. orderResultGuard.assertSuccess(addItemToOrder);
  618. expect(addItemToOrder!.total).toBe(5000);
  619. expect(addItemToOrder!.totalWithTax).toBe(6000);
  620. expect(addItemToOrder!.discounts.length).toBe(0);
  621. const { applyCouponCode } = await shopClient.query<
  622. ApplyCouponCode.Mutation,
  623. ApplyCouponCode.Variables
  624. >(APPLY_COUPON_CODE, {
  625. couponCode,
  626. });
  627. orderResultGuard.assertSuccess(applyCouponCode);
  628. expect(applyCouponCode!.discounts.length).toBe(1);
  629. expect(applyCouponCode!.discounts[0].description).toBe('$10 discount on order');
  630. expect(applyCouponCode!.total).toBe(4000);
  631. expect(applyCouponCode!.totalWithTax).toBe(4800);
  632. });
  633. it('prices include tax', async () => {
  634. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  635. const { addItemToOrder } = await shopClient.query<
  636. AddItemToOrder.Mutation,
  637. AddItemToOrder.Variables
  638. >(ADD_ITEM_TO_ORDER, {
  639. productVariantId: getVariantBySlug('item-5000').id,
  640. quantity: 1,
  641. });
  642. orderResultGuard.assertSuccess(addItemToOrder);
  643. expect(addItemToOrder!.totalWithTax).toBe(6000);
  644. expect(addItemToOrder!.discounts.length).toBe(0);
  645. const { applyCouponCode } = await shopClient.query<
  646. ApplyCouponCode.Mutation,
  647. ApplyCouponCode.Variables
  648. >(APPLY_COUPON_CODE, {
  649. couponCode,
  650. });
  651. orderResultGuard.assertSuccess(applyCouponCode);
  652. expect(applyCouponCode!.discounts.length).toBe(1);
  653. expect(applyCouponCode!.discounts[0].description).toBe('$10 discount on order');
  654. expect(applyCouponCode!.totalWithTax).toBe(5000);
  655. });
  656. });
  657. describe('discountOnItemWithFacets', () => {
  658. const couponCode = '50%_off_sale_items';
  659. let promotion: PromotionFragment;
  660. function getItemSale1Line<
  661. T extends Array<
  662. UpdatedOrderFragment['lines'][number] | TestOrderFragmentFragment['lines'][number]
  663. >,
  664. >(lines: T): T[number] {
  665. return lines.find(l => l.productVariant.id === getVariantBySlug('item-sale-100').id)!;
  666. }
  667. beforeAll(async () => {
  668. const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  669. const saleFacetValue = facets.items[0].values[0];
  670. promotion = await createPromotion({
  671. enabled: true,
  672. name: '50% off sale items',
  673. couponCode,
  674. conditions: [],
  675. actions: [
  676. {
  677. code: discountOnItemWithFacets.code,
  678. arguments: [
  679. { name: 'discount', value: '50' },
  680. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  681. ],
  682. },
  683. ],
  684. });
  685. await assignPromotionToTaxIncludedChannel(promotion.id);
  686. });
  687. afterAll(async () => {
  688. await deletePromotion(promotion.id);
  689. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  690. });
  691. it('prices exclude tax', async () => {
  692. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  693. productVariantId: getVariantBySlug('item-1000').id,
  694. quantity: 1,
  695. });
  696. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  697. productVariantId: getVariantBySlug('item-sale-1000').id,
  698. quantity: 1,
  699. });
  700. const { addItemToOrder } = await shopClient.query<
  701. AddItemToOrder.Mutation,
  702. AddItemToOrder.Variables
  703. >(ADD_ITEM_TO_ORDER, {
  704. productVariantId: getVariantBySlug('item-sale-100').id,
  705. quantity: 2,
  706. });
  707. orderResultGuard.assertSuccess(addItemToOrder);
  708. expect(addItemToOrder!.discounts.length).toBe(0);
  709. expect(getItemSale1Line(addItemToOrder!.lines).discounts.length).toBe(0);
  710. expect(addItemToOrder!.total).toBe(2200);
  711. expect(addItemToOrder!.totalWithTax).toBe(2640);
  712. const { applyCouponCode } = await shopClient.query<
  713. ApplyCouponCode.Mutation,
  714. ApplyCouponCode.Variables
  715. >(APPLY_COUPON_CODE, {
  716. couponCode,
  717. });
  718. orderResultGuard.assertSuccess(applyCouponCode);
  719. expect(applyCouponCode!.total).toBe(1600);
  720. expect(applyCouponCode!.totalWithTax).toBe(1920);
  721. expect(getItemSale1Line(applyCouponCode!.lines).discounts.length).toBe(1); // 1x promotion
  722. const { removeCouponCode } = await shopClient.query<
  723. RemoveCouponCode.Mutation,
  724. RemoveCouponCode.Variables
  725. >(REMOVE_COUPON_CODE, {
  726. couponCode,
  727. });
  728. expect(getItemSale1Line(removeCouponCode!.lines).discounts.length).toBe(0);
  729. expect(removeCouponCode!.total).toBe(2200);
  730. expect(removeCouponCode!.totalWithTax).toBe(2640);
  731. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  732. expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
  733. expect(activeOrder!.total).toBe(2200);
  734. expect(activeOrder!.totalWithTax).toBe(2640);
  735. });
  736. it('prices include tax', async () => {
  737. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  738. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  739. productVariantId: getVariantBySlug('item-1000').id,
  740. quantity: 1,
  741. });
  742. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  743. productVariantId: getVariantBySlug('item-sale-1000').id,
  744. quantity: 1,
  745. });
  746. const { addItemToOrder } = await shopClient.query<
  747. AddItemToOrder.Mutation,
  748. AddItemToOrder.Variables
  749. >(ADD_ITEM_TO_ORDER, {
  750. productVariantId: getVariantBySlug('item-sale-100').id,
  751. quantity: 2,
  752. });
  753. orderResultGuard.assertSuccess(addItemToOrder);
  754. expect(addItemToOrder!.discounts.length).toBe(0);
  755. expect(getItemSale1Line(addItemToOrder!.lines).discounts.length).toBe(0);
  756. expect(addItemToOrder!.total).toBe(2200);
  757. expect(addItemToOrder!.totalWithTax).toBe(2640);
  758. const { applyCouponCode } = await shopClient.query<
  759. ApplyCouponCode.Mutation,
  760. ApplyCouponCode.Variables
  761. >(APPLY_COUPON_CODE, {
  762. couponCode,
  763. });
  764. orderResultGuard.assertSuccess(applyCouponCode);
  765. expect(applyCouponCode!.total).toBe(1600);
  766. expect(applyCouponCode!.totalWithTax).toBe(1920);
  767. expect(getItemSale1Line(applyCouponCode!.lines).discounts.length).toBe(1); // 1x promotion
  768. const { removeCouponCode } = await shopClient.query<
  769. RemoveCouponCode.Mutation,
  770. RemoveCouponCode.Variables
  771. >(REMOVE_COUPON_CODE, {
  772. couponCode,
  773. });
  774. expect(getItemSale1Line(removeCouponCode!.lines).discounts.length).toBe(0);
  775. expect(removeCouponCode!.total).toBe(2200);
  776. expect(removeCouponCode!.totalWithTax).toBe(2640);
  777. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  778. expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
  779. expect(activeOrder!.total).toBe(2200);
  780. expect(activeOrder!.totalWithTax).toBe(2640);
  781. });
  782. });
  783. describe('productsPercentageDiscount', () => {
  784. const couponCode = '50%_off_product';
  785. let promotion: PromotionFragment;
  786. beforeAll(async () => {
  787. promotion = await createPromotion({
  788. enabled: true,
  789. name: '50% off product',
  790. couponCode,
  791. conditions: [],
  792. actions: [
  793. {
  794. code: productsPercentageDiscount.code,
  795. arguments: [
  796. { name: 'discount', value: '50' },
  797. {
  798. name: 'productVariantIds',
  799. value: `["${getVariantBySlug('item-5000').id}"]`,
  800. },
  801. ],
  802. },
  803. ],
  804. });
  805. await assignPromotionToTaxIncludedChannel(promotion.id);
  806. });
  807. afterAll(async () => {
  808. await deletePromotion(promotion.id);
  809. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  810. });
  811. it('prices exclude tax', async () => {
  812. const { addItemToOrder } = await shopClient.query<
  813. AddItemToOrder.Mutation,
  814. AddItemToOrder.Variables
  815. >(ADD_ITEM_TO_ORDER, {
  816. productVariantId: getVariantBySlug('item-5000').id,
  817. quantity: 1,
  818. });
  819. orderResultGuard.assertSuccess(addItemToOrder);
  820. expect(addItemToOrder!.discounts.length).toBe(0);
  821. expect(addItemToOrder!.lines[0].discounts.length).toBe(0);
  822. expect(addItemToOrder!.total).toBe(5000);
  823. expect(addItemToOrder!.totalWithTax).toBe(6000);
  824. const { applyCouponCode } = await shopClient.query<
  825. ApplyCouponCode.Mutation,
  826. ApplyCouponCode.Variables
  827. >(APPLY_COUPON_CODE, {
  828. couponCode,
  829. });
  830. orderResultGuard.assertSuccess(applyCouponCode);
  831. expect(applyCouponCode!.total).toBe(2500);
  832. expect(applyCouponCode!.totalWithTax).toBe(3000);
  833. expect(applyCouponCode!.lines[0].discounts.length).toBe(1); // 1x promotion
  834. });
  835. it('prices include tax', async () => {
  836. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  837. const { addItemToOrder } = await shopClient.query<
  838. AddItemToOrder.Mutation,
  839. AddItemToOrder.Variables
  840. >(ADD_ITEM_TO_ORDER, {
  841. productVariantId: getVariantBySlug('item-5000').id,
  842. quantity: 1,
  843. });
  844. orderResultGuard.assertSuccess(addItemToOrder);
  845. expect(addItemToOrder!.discounts.length).toBe(0);
  846. expect(addItemToOrder!.lines[0].discounts.length).toBe(0);
  847. expect(addItemToOrder!.total).toBe(5000);
  848. expect(addItemToOrder!.totalWithTax).toBe(6000);
  849. const { applyCouponCode } = await shopClient.query<
  850. ApplyCouponCode.Mutation,
  851. ApplyCouponCode.Variables
  852. >(APPLY_COUPON_CODE, {
  853. couponCode,
  854. });
  855. orderResultGuard.assertSuccess(applyCouponCode);
  856. expect(applyCouponCode!.total).toBe(2500);
  857. expect(applyCouponCode!.totalWithTax).toBe(3000);
  858. expect(applyCouponCode!.lines[0].discounts.length).toBe(1); // 1x promotion
  859. });
  860. });
  861. describe('freeShipping', () => {
  862. const couponCode = 'FREE_SHIPPING';
  863. let promotion: PromotionFragment;
  864. // The test shipping method needs to be created in each Channel, since ShippingMethods
  865. // are ChannelAware
  866. async function createTestShippingMethod(channelToken: string) {
  867. adminClient.setChannelToken(channelToken);
  868. const result = await adminClient.query<
  869. CreateShippingMethod.Mutation,
  870. CreateShippingMethod.Variables
  871. >(CREATE_SHIPPING_METHOD, {
  872. input: {
  873. code: 'test-method',
  874. fulfillmentHandler: manualFulfillmentHandler.code,
  875. checker: {
  876. code: defaultShippingEligibilityChecker.code,
  877. arguments: [
  878. {
  879. name: 'orderMinimum',
  880. value: '0',
  881. },
  882. ],
  883. },
  884. calculator: {
  885. code: defaultShippingCalculator.code,
  886. arguments: [
  887. { name: 'rate', value: '345' },
  888. { name: 'includesTax', value: 'auto' },
  889. { name: 'taxRate', value: '20' },
  890. ],
  891. },
  892. translations: [
  893. { languageCode: LanguageCode.en, name: 'test method', description: '' },
  894. ],
  895. },
  896. });
  897. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  898. return result.createShippingMethod;
  899. }
  900. beforeAll(async () => {
  901. promotion = await createPromotion({
  902. enabled: true,
  903. name: 'Free shipping',
  904. couponCode,
  905. conditions: [],
  906. actions: [
  907. {
  908. code: freeShipping.code,
  909. arguments: [],
  910. },
  911. ],
  912. });
  913. await assignPromotionToTaxIncludedChannel(promotion.id);
  914. });
  915. afterAll(async () => {
  916. await deletePromotion(promotion.id);
  917. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  918. });
  919. it('prices exclude tax', async () => {
  920. const { addItemToOrder } = await shopClient.query<
  921. AddItemToOrder.Mutation,
  922. AddItemToOrder.Variables
  923. >(ADD_ITEM_TO_ORDER, {
  924. productVariantId: getVariantBySlug('item-5000').id,
  925. quantity: 1,
  926. });
  927. const method = await createTestShippingMethod(E2E_DEFAULT_CHANNEL_TOKEN);
  928. const { setOrderShippingMethod } = await shopClient.query<
  929. SetShippingMethod.Mutation,
  930. SetShippingMethod.Variables
  931. >(SET_SHIPPING_METHOD, {
  932. id: method.id,
  933. });
  934. orderResultGuard.assertSuccess(setOrderShippingMethod);
  935. expect(setOrderShippingMethod.discounts).toEqual([]);
  936. expect(setOrderShippingMethod.shipping).toBe(345);
  937. expect(setOrderShippingMethod.shippingWithTax).toBe(414);
  938. expect(setOrderShippingMethod.total).toBe(5345);
  939. expect(setOrderShippingMethod.totalWithTax).toBe(6414);
  940. const { applyCouponCode } = await shopClient.query<
  941. ApplyCouponCode.Mutation,
  942. ApplyCouponCode.Variables
  943. >(APPLY_COUPON_CODE, {
  944. couponCode,
  945. });
  946. orderResultGuard.assertSuccess(applyCouponCode);
  947. expect(applyCouponCode.discounts.length).toBe(1);
  948. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  949. expect(applyCouponCode.shipping).toBe(0);
  950. expect(applyCouponCode.shippingWithTax).toBe(0);
  951. expect(applyCouponCode.total).toBe(5000);
  952. expect(applyCouponCode.totalWithTax).toBe(6000);
  953. });
  954. it('prices include tax', async () => {
  955. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  956. const { addItemToOrder } = await shopClient.query<
  957. AddItemToOrder.Mutation,
  958. AddItemToOrder.Variables
  959. >(ADD_ITEM_TO_ORDER, {
  960. productVariantId: getVariantBySlug('item-5000').id,
  961. quantity: 1,
  962. });
  963. const method = await createTestShippingMethod(TAX_INCLUDED_CHANNEL_TOKEN);
  964. const { setOrderShippingMethod } = await shopClient.query<
  965. SetShippingMethod.Mutation,
  966. SetShippingMethod.Variables
  967. >(SET_SHIPPING_METHOD, {
  968. id: method.id,
  969. });
  970. orderResultGuard.assertSuccess(setOrderShippingMethod);
  971. expect(setOrderShippingMethod.discounts).toEqual([]);
  972. expect(setOrderShippingMethod.shipping).toBe(287);
  973. expect(setOrderShippingMethod.shippingWithTax).toBe(345);
  974. expect(setOrderShippingMethod.total).toBe(5287);
  975. expect(setOrderShippingMethod.totalWithTax).toBe(6345);
  976. const { applyCouponCode } = await shopClient.query<
  977. ApplyCouponCode.Mutation,
  978. ApplyCouponCode.Variables
  979. >(APPLY_COUPON_CODE, {
  980. couponCode,
  981. });
  982. orderResultGuard.assertSuccess(applyCouponCode);
  983. expect(applyCouponCode.discounts.length).toBe(1);
  984. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  985. expect(applyCouponCode.shipping).toBe(0);
  986. expect(applyCouponCode.shippingWithTax).toBe(0);
  987. expect(applyCouponCode.total).toBe(5000);
  988. expect(applyCouponCode.totalWithTax).toBe(6000);
  989. });
  990. // https://github.com/vendure-ecommerce/vendure/pull/1150
  991. it('shipping discounts get correctly removed', async () => {
  992. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  993. const { addItemToOrder } = await shopClient.query<
  994. AddItemToOrder.Mutation,
  995. AddItemToOrder.Variables
  996. >(ADD_ITEM_TO_ORDER, {
  997. productVariantId: getVariantBySlug('item-5000').id,
  998. quantity: 1,
  999. });
  1000. const method = await createTestShippingMethod(TAX_INCLUDED_CHANNEL_TOKEN);
  1001. const { setOrderShippingMethod } = await shopClient.query<
  1002. SetShippingMethod.Mutation,
  1003. SetShippingMethod.Variables
  1004. >(SET_SHIPPING_METHOD, {
  1005. id: method.id,
  1006. });
  1007. orderResultGuard.assertSuccess(setOrderShippingMethod);
  1008. expect(setOrderShippingMethod.discounts).toEqual([]);
  1009. expect(setOrderShippingMethod.shippingWithTax).toBe(345);
  1010. const { applyCouponCode } = await shopClient.query<
  1011. ApplyCouponCode.Mutation,
  1012. ApplyCouponCode.Variables
  1013. >(APPLY_COUPON_CODE, {
  1014. couponCode,
  1015. });
  1016. orderResultGuard.assertSuccess(applyCouponCode);
  1017. expect(applyCouponCode.discounts.length).toBe(1);
  1018. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  1019. expect(applyCouponCode.shippingWithTax).toBe(0);
  1020. const { removeCouponCode } = await shopClient.query<
  1021. RemoveCouponCode.Mutation,
  1022. RemoveCouponCode.Variables
  1023. >(REMOVE_COUPON_CODE, {
  1024. couponCode,
  1025. });
  1026. orderResultGuard.assertSuccess(removeCouponCode);
  1027. expect(removeCouponCode.discounts).toEqual([]);
  1028. expect(removeCouponCode.shippingWithTax).toBe(345);
  1029. });
  1030. });
  1031. describe('multiple promotions simultaneously', () => {
  1032. const saleItem50pcOffCoupon = 'CODE1';
  1033. const order15pcOffCoupon = 'CODE2';
  1034. let promotion1: PromotionFragment;
  1035. let promotion2: PromotionFragment;
  1036. beforeAll(async () => {
  1037. const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  1038. const saleFacetValue = facets.items[0].values[0];
  1039. promotion1 = await createPromotion({
  1040. enabled: true,
  1041. name: 'item promo',
  1042. couponCode: saleItem50pcOffCoupon,
  1043. conditions: [],
  1044. actions: [
  1045. {
  1046. code: discountOnItemWithFacets.code,
  1047. arguments: [
  1048. { name: 'discount', value: '50' },
  1049. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  1050. ],
  1051. },
  1052. ],
  1053. });
  1054. promotion2 = await createPromotion({
  1055. enabled: true,
  1056. name: 'order promo',
  1057. couponCode: order15pcOffCoupon,
  1058. conditions: [],
  1059. actions: [
  1060. {
  1061. code: orderPercentageDiscount.code,
  1062. arguments: [{ name: 'discount', value: '15' }],
  1063. },
  1064. ],
  1065. });
  1066. await assignPromotionToTaxIncludedChannel([promotion1.id, promotion2.id]);
  1067. });
  1068. afterAll(async () => {
  1069. await deletePromotion(promotion1.id);
  1070. await deletePromotion(promotion2.id);
  1071. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1072. });
  1073. it('prices exclude tax', async () => {
  1074. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  1075. productVariantId: getVariantBySlug('item-sale-1000').id,
  1076. quantity: 2,
  1077. });
  1078. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  1079. productVariantId: getVariantBySlug('item-5000').id,
  1080. quantity: 1,
  1081. });
  1082. // Apply the OrderItem-level promo
  1083. const { applyCouponCode: apply1 } = await shopClient.query<
  1084. ApplyCouponCode.Mutation,
  1085. ApplyCouponCode.Variables
  1086. >(APPLY_COUPON_CODE, {
  1087. couponCode: saleItem50pcOffCoupon,
  1088. });
  1089. orderResultGuard.assertSuccess(apply1);
  1090. const saleItemLine = apply1.lines.find(
  1091. l => l.productVariant.id === getVariantBySlug('item-sale-1000').id,
  1092. )!;
  1093. expect(saleItemLine.discounts.length).toBe(1); // 1x promotion
  1094. expect(
  1095. saleItemLine.discounts.find(a => a.type === AdjustmentType.PROMOTION)?.description,
  1096. ).toBe('item promo');
  1097. expect(apply1.discounts.length).toBe(1);
  1098. expect(apply1.total).toBe(6000);
  1099. expect(apply1.totalWithTax).toBe(7200);
  1100. // Apply the Order-level promo
  1101. const { applyCouponCode: apply2 } = await shopClient.query<
  1102. ApplyCouponCode.Mutation,
  1103. ApplyCouponCode.Variables
  1104. >(APPLY_COUPON_CODE, {
  1105. couponCode: order15pcOffCoupon,
  1106. });
  1107. orderResultGuard.assertSuccess(apply2);
  1108. expect(apply2.discounts.map(d => d.description).sort()).toEqual([
  1109. 'item promo',
  1110. 'order promo',
  1111. ]);
  1112. expect(apply2.total).toBe(5100);
  1113. expect(apply2.totalWithTax).toBe(6120);
  1114. });
  1115. it('prices include tax', async () => {
  1116. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  1117. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  1118. productVariantId: getVariantBySlug('item-sale-1000').id,
  1119. quantity: 2,
  1120. });
  1121. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  1122. productVariantId: getVariantBySlug('item-5000').id,
  1123. quantity: 1,
  1124. });
  1125. // Apply the OrderItem-level promo
  1126. const { applyCouponCode: apply1 } = await shopClient.query<
  1127. ApplyCouponCode.Mutation,
  1128. ApplyCouponCode.Variables
  1129. >(APPLY_COUPON_CODE, {
  1130. couponCode: saleItem50pcOffCoupon,
  1131. });
  1132. orderResultGuard.assertSuccess(apply1);
  1133. const saleItemLine = apply1.lines.find(
  1134. l => l.productVariant.id === getVariantBySlug('item-sale-1000').id,
  1135. )!;
  1136. expect(saleItemLine.discounts.length).toBe(1); // 1x promotion
  1137. expect(
  1138. saleItemLine.discounts.find(a => a.type === AdjustmentType.PROMOTION)?.description,
  1139. ).toBe('item promo');
  1140. expect(apply1.discounts.length).toBe(1);
  1141. expect(apply1.total).toBe(6000);
  1142. expect(apply1.totalWithTax).toBe(7200);
  1143. // Apply the Order-level promo
  1144. const { applyCouponCode: apply2 } = await shopClient.query<
  1145. ApplyCouponCode.Mutation,
  1146. ApplyCouponCode.Variables
  1147. >(APPLY_COUPON_CODE, {
  1148. couponCode: order15pcOffCoupon,
  1149. });
  1150. orderResultGuard.assertSuccess(apply2);
  1151. expect(apply2.discounts.map(d => d.description).sort()).toEqual([
  1152. 'item promo',
  1153. 'order promo',
  1154. ]);
  1155. expect(apply2.total).toBe(5100);
  1156. expect(apply2.totalWithTax).toBe(6120);
  1157. });
  1158. });
  1159. });
  1160. describe('per-customer usage limit', () => {
  1161. const TEST_COUPON_CODE = 'TESTCOUPON';
  1162. const orderGuard: ErrorResultGuard<TestOrderWithPaymentsFragment> = createErrorResultGuard(
  1163. input => !!input.lines,
  1164. );
  1165. let promoWithUsageLimit: PromotionFragment;
  1166. beforeAll(async () => {
  1167. promoWithUsageLimit = await createPromotion({
  1168. enabled: true,
  1169. name: 'Free with test coupon',
  1170. couponCode: TEST_COUPON_CODE,
  1171. perCustomerUsageLimit: 1,
  1172. conditions: [],
  1173. actions: [freeOrderAction],
  1174. });
  1175. });
  1176. afterAll(async () => {
  1177. await deletePromotion(promoWithUsageLimit.id);
  1178. });
  1179. async function createNewActiveOrder() {
  1180. const { addItemToOrder } = await shopClient.query<
  1181. AddItemToOrder.Mutation,
  1182. AddItemToOrder.Variables
  1183. >(ADD_ITEM_TO_ORDER, {
  1184. productVariantId: getVariantBySlug('item-5000').id,
  1185. quantity: 1,
  1186. });
  1187. return addItemToOrder;
  1188. }
  1189. describe('guest customer', () => {
  1190. const GUEST_EMAIL_ADDRESS = 'guest@test.com';
  1191. let orderCode: string;
  1192. function addGuestCustomerToOrder() {
  1193. return shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  1194. SET_CUSTOMER,
  1195. {
  1196. input: {
  1197. emailAddress: GUEST_EMAIL_ADDRESS,
  1198. firstName: 'Guest',
  1199. lastName: 'Customer',
  1200. },
  1201. },
  1202. );
  1203. }
  1204. it('allows initial usage', async () => {
  1205. await shopClient.asAnonymousUser();
  1206. await createNewActiveOrder();
  1207. await addGuestCustomerToOrder();
  1208. const { applyCouponCode } = await shopClient.query<
  1209. ApplyCouponCode.Mutation,
  1210. ApplyCouponCode.Variables
  1211. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1212. orderResultGuard.assertSuccess(applyCouponCode);
  1213. expect(applyCouponCode!.totalWithTax).toBe(0);
  1214. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  1215. await proceedToArrangingPayment(shopClient);
  1216. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1217. orderGuard.assertSuccess(order);
  1218. expect(order.state).toBe('PaymentSettled');
  1219. expect(order.active).toBe(false);
  1220. orderCode = order.code;
  1221. });
  1222. it('adds Promotions to Order once payment arranged', async () => {
  1223. const { orderByCode } = await shopClient.query<
  1224. GetOrderPromotionsByCode.Query,
  1225. GetOrderPromotionsByCode.Variables
  1226. >(GET_ORDER_PROMOTIONS_BY_CODE, {
  1227. code: orderCode,
  1228. });
  1229. expect(orderByCode!.promotions.map(pick(['name']))).toEqual([
  1230. { name: 'Free with test coupon' },
  1231. ]);
  1232. });
  1233. it('returns error result when usage exceeds limit', async () => {
  1234. await shopClient.asAnonymousUser();
  1235. await createNewActiveOrder();
  1236. await addGuestCustomerToOrder();
  1237. const { applyCouponCode } = await shopClient.query<
  1238. ApplyCouponCode.Mutation,
  1239. ApplyCouponCode.Variables
  1240. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1241. orderResultGuard.assertErrorResult(applyCouponCode);
  1242. expect(applyCouponCode.message).toEqual(
  1243. 'Coupon code cannot be used more than once per customer',
  1244. );
  1245. });
  1246. it('removes couponCode from order when adding customer after code applied', async () => {
  1247. await shopClient.asAnonymousUser();
  1248. await createNewActiveOrder();
  1249. const { applyCouponCode } = await shopClient.query<
  1250. ApplyCouponCode.Mutation,
  1251. ApplyCouponCode.Variables
  1252. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1253. orderResultGuard.assertSuccess(applyCouponCode);
  1254. expect(applyCouponCode!.totalWithTax).toBe(0);
  1255. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  1256. await addGuestCustomerToOrder();
  1257. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1258. expect(activeOrder!.couponCodes).toEqual([]);
  1259. expect(activeOrder!.totalWithTax).toBe(6000);
  1260. });
  1261. });
  1262. describe('signed-in customer', () => {
  1263. function logInAsRegisteredCustomer() {
  1264. return shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1265. }
  1266. it('allows initial usage', async () => {
  1267. await logInAsRegisteredCustomer();
  1268. await createNewActiveOrder();
  1269. const { applyCouponCode } = await shopClient.query<
  1270. ApplyCouponCode.Mutation,
  1271. ApplyCouponCode.Variables
  1272. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1273. orderResultGuard.assertSuccess(applyCouponCode);
  1274. expect(applyCouponCode!.totalWithTax).toBe(0);
  1275. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  1276. await proceedToArrangingPayment(shopClient);
  1277. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1278. orderGuard.assertSuccess(order);
  1279. expect(order.state).toBe('PaymentSettled');
  1280. expect(order.active).toBe(false);
  1281. });
  1282. it('returns error result when usage exceeds limit', async () => {
  1283. await logInAsRegisteredCustomer();
  1284. await createNewActiveOrder();
  1285. const { applyCouponCode } = await shopClient.query<
  1286. ApplyCouponCode.Mutation,
  1287. ApplyCouponCode.Variables
  1288. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1289. orderResultGuard.assertErrorResult(applyCouponCode);
  1290. expect(applyCouponCode.message).toEqual(
  1291. 'Coupon code cannot be used more than once per customer',
  1292. );
  1293. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_LIMIT_ERROR);
  1294. });
  1295. it('removes couponCode from order when logging in after code applied', async () => {
  1296. await shopClient.asAnonymousUser();
  1297. await createNewActiveOrder();
  1298. const { applyCouponCode } = await shopClient.query<
  1299. ApplyCouponCode.Mutation,
  1300. ApplyCouponCode.Variables
  1301. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1302. orderResultGuard.assertSuccess(applyCouponCode);
  1303. expect(applyCouponCode!.couponCodes).toEqual([TEST_COUPON_CODE]);
  1304. expect(applyCouponCode!.totalWithTax).toBe(0);
  1305. await logInAsRegisteredCustomer();
  1306. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1307. expect(activeOrder!.totalWithTax).toBe(6000);
  1308. expect(activeOrder!.couponCodes).toEqual([]);
  1309. });
  1310. });
  1311. });
  1312. // https://github.com/vendure-ecommerce/vendure/issues/710
  1313. it('removes order-level discount made invalid by removing OrderLine', async () => {
  1314. const promotion = await createPromotion({
  1315. enabled: true,
  1316. name: 'Test Promo',
  1317. conditions: [minOrderAmountCondition(10000)],
  1318. actions: [
  1319. {
  1320. code: orderFixedDiscount.code,
  1321. arguments: [{ name: 'discount', value: '1000' }],
  1322. },
  1323. ],
  1324. });
  1325. await shopClient.asAnonymousUser();
  1326. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  1327. productVariantId: getVariantBySlug('item-1000').id,
  1328. quantity: 8,
  1329. });
  1330. const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
  1331. ADD_ITEM_TO_ORDER,
  1332. {
  1333. productVariantId: getVariantBySlug('item-5000').id,
  1334. quantity: 1,
  1335. },
  1336. );
  1337. orderResultGuard.assertSuccess(addItemToOrder);
  1338. expect(addItemToOrder!.discounts.length).toBe(1);
  1339. expect(addItemToOrder!.discounts[0].description).toBe('Test Promo');
  1340. const { activeOrder: check1 } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1341. expect(check1!.discounts.length).toBe(1);
  1342. expect(check1!.discounts[0].description).toBe('Test Promo');
  1343. const { removeOrderLine } = await shopClient.query<
  1344. RemoveItemFromOrder.Mutation,
  1345. RemoveItemFromOrder.Variables
  1346. >(REMOVE_ITEM_FROM_ORDER, {
  1347. orderLineId: addItemToOrder.lines[1].id,
  1348. });
  1349. orderResultGuard.assertSuccess(removeOrderLine);
  1350. expect(removeOrderLine.discounts.length).toBe(0);
  1351. const { activeOrder: check2 } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1352. expect(check2!.discounts.length).toBe(0);
  1353. });
  1354. async function getProducts() {
  1355. const result = await adminClient.query<GetProductsWithVariantPrices.Query>(
  1356. GET_PRODUCTS_WITH_VARIANT_PRICES,
  1357. {
  1358. options: {
  1359. take: 10,
  1360. skip: 0,
  1361. },
  1362. },
  1363. );
  1364. products = result.products.items;
  1365. }
  1366. async function createGlobalPromotions() {
  1367. const { facets } = await adminClient.query<GetFacetList.Query>(GET_FACET_LIST);
  1368. const saleFacetValue = facets.items[0].values[0];
  1369. await createPromotion({
  1370. enabled: true,
  1371. name: 'Promo not yet started',
  1372. startsAt: new Date(2199, 0, 0),
  1373. conditions: [minOrderAmountCondition(100)],
  1374. actions: [freeOrderAction],
  1375. });
  1376. const deletedPromotion = await createPromotion({
  1377. enabled: true,
  1378. name: 'Deleted promotion',
  1379. conditions: [minOrderAmountCondition(100)],
  1380. actions: [freeOrderAction],
  1381. });
  1382. await deletePromotion(deletedPromotion.id);
  1383. }
  1384. async function createPromotion(input: CreatePromotionInput): Promise<PromotionFragment> {
  1385. const result = await adminClient.query<CreatePromotion.Mutation, CreatePromotion.Variables>(
  1386. CREATE_PROMOTION,
  1387. {
  1388. input,
  1389. },
  1390. );
  1391. return result.createPromotion as PromotionFragment;
  1392. }
  1393. function getVariantBySlug(
  1394. slug: 'item-100' | 'item-1000' | 'item-5000' | 'item-sale-100' | 'item-sale-1000',
  1395. ): GetProductsWithVariantPrices.Variants {
  1396. return products.find(p => p.slug === slug)!.variants[0];
  1397. }
  1398. async function deletePromotion(promotionId: string) {
  1399. await adminClient.query(gql`
  1400. mutation DeletePromotionAdHoc1 {
  1401. deletePromotion(id: "${promotionId}") {
  1402. result
  1403. }
  1404. }
  1405. `);
  1406. }
  1407. });