order-promotion.e2e-spec.ts 67 KB

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