order-promotion.e2e-spec.ts 74 KB

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