order-promotion.e2e-spec.ts 94 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { omit } from '@vendure/common/lib/omit';
  3. import { pick } from '@vendure/common/lib/pick';
  4. import {
  5. containsProducts,
  6. customerGroup,
  7. defaultShippingCalculator,
  8. defaultShippingEligibilityChecker,
  9. discountOnItemWithFacets,
  10. hasFacetValues,
  11. manualFulfillmentHandler,
  12. mergeConfig,
  13. minimumOrderAmount,
  14. orderPercentageDiscount,
  15. productsPercentageDiscount,
  16. } from '@vendure/core';
  17. import {
  18. createErrorResultGuard,
  19. createTestEnvironment,
  20. E2E_DEFAULT_CHANNEL_TOKEN,
  21. ErrorResultGuard,
  22. } from '@vendure/testing';
  23. import gql from 'graphql-tag';
  24. import path from 'path';
  25. import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
  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 { orderLineFixedDiscount } from '../src/config/promotion/actions/order-line-fixed-discount-action';
  31. import { TestMoneyStrategy } from './fixtures/test-money-strategy';
  32. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  33. import { CurrencyCode, HistoryEntryType, LanguageCode } from './graphql/generated-e2e-admin-types';
  34. import * as Codegen from './graphql/generated-e2e-admin-types';
  35. import { AdjustmentType, ErrorCode } from './graphql/generated-e2e-shop-types';
  36. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  37. import {
  38. ASSIGN_PRODUCT_TO_CHANNEL,
  39. ASSIGN_PROMOTIONS_TO_CHANNEL,
  40. CANCEL_ORDER,
  41. CREATE_CHANNEL,
  42. CREATE_CUSTOMER_GROUP,
  43. CREATE_PROMOTION,
  44. CREATE_SHIPPING_METHOD,
  45. GET_FACET_LIST,
  46. GET_PRODUCTS_WITH_VARIANT_PRICES,
  47. REMOVE_CUSTOMERS_FROM_GROUP,
  48. } from './graphql/shared-definitions';
  49. import {
  50. ADD_ITEM_TO_ORDER,
  51. ADJUST_ITEM_QUANTITY,
  52. APPLY_COUPON_CODE,
  53. GET_ACTIVE_ORDER,
  54. GET_ORDER_PROMOTIONS_BY_CODE,
  55. REMOVE_COUPON_CODE,
  56. REMOVE_ITEM_FROM_ORDER,
  57. SET_CUSTOMER,
  58. SET_SHIPPING_METHOD,
  59. } from './graphql/shop-definitions';
  60. import { addPaymentToOrder, proceedToArrangingPayment } from './utils/test-order-utils';
  61. describe('Promotions applied to Orders', () => {
  62. const { server, adminClient, shopClient } = createTestEnvironment(
  63. mergeConfig(testConfig(), {
  64. dbConnectionOptions: { logging: true },
  65. paymentOptions: {
  66. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  67. },
  68. entityOptions: {
  69. moneyStrategy: new TestMoneyStrategy(),
  70. },
  71. }),
  72. );
  73. const freeOrderAction = {
  74. code: orderPercentageDiscount.code,
  75. arguments: [{ name: 'discount', value: '100' }],
  76. };
  77. const minOrderAmountCondition = (min: number) => ({
  78. code: minimumOrderAmount.code,
  79. arguments: [
  80. { name: 'amount', value: min.toString() },
  81. { name: 'taxInclusive', value: 'true' },
  82. ],
  83. });
  84. type OrderSuccessResult = CodegenShop.UpdatedOrderFragment | CodegenShop.TestOrderFragmentFragment;
  85. const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
  86. input => !!input.lines,
  87. );
  88. let products: Codegen.GetProductsWithVariantPricesQuery['products']['items'];
  89. beforeAll(async () => {
  90. await server.init({
  91. initialData: {
  92. ...initialData,
  93. paymentMethods: [
  94. {
  95. name: testSuccessfulPaymentMethod.code,
  96. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  97. },
  98. ],
  99. },
  100. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-promotions.csv'),
  101. customerCount: 2,
  102. });
  103. await adminClient.asSuperAdmin();
  104. await getProducts();
  105. await createGlobalPromotions();
  106. }, TEST_SETUP_TIMEOUT_MS);
  107. afterAll(async () => {
  108. await server.destroy();
  109. });
  110. describe('coupon codes', () => {
  111. const TEST_COUPON_CODE = 'TESTCOUPON';
  112. const EXPIRED_COUPON_CODE = 'EXPIRED';
  113. let promoFreeWithCoupon: Codegen.PromotionFragment;
  114. let promoFreeWithExpiredCoupon: Codegen.PromotionFragment;
  115. beforeAll(async () => {
  116. promoFreeWithCoupon = await createPromotion({
  117. enabled: true,
  118. name: 'Free with test coupon',
  119. couponCode: TEST_COUPON_CODE,
  120. conditions: [],
  121. actions: [freeOrderAction],
  122. });
  123. promoFreeWithExpiredCoupon = await createPromotion({
  124. enabled: true,
  125. name: 'Expired coupon',
  126. endsAt: new Date(2010, 0, 0),
  127. couponCode: EXPIRED_COUPON_CODE,
  128. conditions: [],
  129. actions: [freeOrderAction],
  130. });
  131. await shopClient.asAnonymousUser();
  132. await shopClient.query<
  133. CodegenShop.AddItemToOrderMutation,
  134. CodegenShop.AddItemToOrderMutationVariables
  135. >(ADD_ITEM_TO_ORDER, {
  136. productVariantId: getVariantBySlug('item-5000').id,
  137. quantity: 1,
  138. });
  139. });
  140. afterAll(async () => {
  141. await deletePromotion(promoFreeWithCoupon.id);
  142. await deletePromotion(promoFreeWithExpiredCoupon.id);
  143. });
  144. it('applyCouponCode returns error result when code is nonexistant', async () => {
  145. const { applyCouponCode } = await shopClient.query<
  146. CodegenShop.ApplyCouponCodeMutation,
  147. CodegenShop.ApplyCouponCodeMutationVariables
  148. >(APPLY_COUPON_CODE, {
  149. couponCode: 'bad code',
  150. });
  151. orderResultGuard.assertErrorResult(applyCouponCode);
  152. expect(applyCouponCode.message).toBe('Coupon code "bad code" is not valid');
  153. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_INVALID_ERROR);
  154. });
  155. it('applyCouponCode returns error when code is expired', async () => {
  156. const { applyCouponCode } = await shopClient.query<
  157. CodegenShop.ApplyCouponCodeMutation,
  158. CodegenShop.ApplyCouponCodeMutationVariables
  159. >(APPLY_COUPON_CODE, {
  160. couponCode: EXPIRED_COUPON_CODE,
  161. });
  162. orderResultGuard.assertErrorResult(applyCouponCode);
  163. expect(applyCouponCode.message).toBe(`Coupon code "${EXPIRED_COUPON_CODE}" has expired`);
  164. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_EXPIRED_ERROR);
  165. });
  166. it('coupon code application is case-sensitive', async () => {
  167. const { applyCouponCode } = await shopClient.query<
  168. CodegenShop.ApplyCouponCodeMutation,
  169. CodegenShop.ApplyCouponCodeMutationVariables
  170. >(APPLY_COUPON_CODE, {
  171. couponCode: TEST_COUPON_CODE.toLowerCase(),
  172. });
  173. orderResultGuard.assertErrorResult(applyCouponCode);
  174. expect(applyCouponCode.message).toBe(
  175. `Coupon code "${TEST_COUPON_CODE.toLowerCase()}" is not valid`,
  176. );
  177. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_INVALID_ERROR);
  178. });
  179. it('applies a valid coupon code', async () => {
  180. const { applyCouponCode } = await shopClient.query<
  181. CodegenShop.ApplyCouponCodeMutation,
  182. CodegenShop.ApplyCouponCodeMutationVariables
  183. >(APPLY_COUPON_CODE, {
  184. couponCode: TEST_COUPON_CODE,
  185. });
  186. orderResultGuard.assertSuccess(applyCouponCode);
  187. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  188. expect(applyCouponCode.discounts.length).toBe(1);
  189. expect(applyCouponCode.discounts[0].description).toBe('Free with test coupon');
  190. expect(applyCouponCode.totalWithTax).toBe(0);
  191. });
  192. it('order history records application', async () => {
  193. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  194. expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
  195. {
  196. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  197. data: {
  198. from: 'Created',
  199. to: 'AddingItems',
  200. },
  201. },
  202. {
  203. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  204. data: {
  205. couponCode: TEST_COUPON_CODE,
  206. promotionId: 'T_3',
  207. },
  208. },
  209. ]);
  210. });
  211. it('de-duplicates existing codes', async () => {
  212. const { applyCouponCode } = await shopClient.query<
  213. CodegenShop.ApplyCouponCodeMutation,
  214. CodegenShop.ApplyCouponCodeMutationVariables
  215. >(APPLY_COUPON_CODE, {
  216. couponCode: TEST_COUPON_CODE,
  217. });
  218. orderResultGuard.assertSuccess(applyCouponCode);
  219. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  220. });
  221. it('removes a coupon code', async () => {
  222. const { removeCouponCode } = await shopClient.query<
  223. CodegenShop.RemoveCouponCodeMutation,
  224. CodegenShop.RemoveCouponCodeMutationVariables
  225. >(REMOVE_COUPON_CODE, {
  226. couponCode: TEST_COUPON_CODE,
  227. });
  228. expect(removeCouponCode!.discounts.length).toBe(0);
  229. expect(removeCouponCode!.totalWithTax).toBe(6000);
  230. });
  231. // https://github.com/vendure-ecommerce/vendure/issues/649
  232. it('discounts array cleared after coupon code removed', async () => {
  233. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  234. expect(activeOrder?.discounts).toEqual([]);
  235. });
  236. it('order history records removal', async () => {
  237. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  238. expect(activeOrder!.history.items.map(i => omit(i, ['id']))).toEqual([
  239. {
  240. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  241. data: {
  242. from: 'Created',
  243. to: 'AddingItems',
  244. },
  245. },
  246. {
  247. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  248. data: {
  249. couponCode: TEST_COUPON_CODE,
  250. promotionId: 'T_3',
  251. },
  252. },
  253. {
  254. type: HistoryEntryType.ORDER_COUPON_REMOVED,
  255. data: {
  256. couponCode: TEST_COUPON_CODE,
  257. },
  258. },
  259. ]);
  260. });
  261. it('does not record removal of coupon code that was not added', async () => {
  262. const { removeCouponCode } = await shopClient.query<
  263. CodegenShop.RemoveCouponCodeMutation,
  264. CodegenShop.RemoveCouponCodeMutationVariables
  265. >(REMOVE_COUPON_CODE, {
  266. couponCode: 'NOT_THERE',
  267. });
  268. expect(removeCouponCode!.history.items.map(i => omit(i, ['id']))).toEqual([
  269. {
  270. type: HistoryEntryType.ORDER_STATE_TRANSITION,
  271. data: {
  272. from: 'Created',
  273. to: 'AddingItems',
  274. },
  275. },
  276. {
  277. type: HistoryEntryType.ORDER_COUPON_APPLIED,
  278. data: {
  279. couponCode: TEST_COUPON_CODE,
  280. promotionId: 'T_3',
  281. },
  282. },
  283. {
  284. type: HistoryEntryType.ORDER_COUPON_REMOVED,
  285. data: {
  286. couponCode: TEST_COUPON_CODE,
  287. },
  288. },
  289. ]);
  290. });
  291. describe('coupon codes in other channels', () => {
  292. const OTHER_CHANNEL_TOKEN = 'other-channel';
  293. const OTHER_CHANNEL_COUPON_CODE = 'OTHER_CHANNEL_CODE';
  294. beforeAll(async () => {
  295. const { createChannel } = await adminClient.query<
  296. Codegen.CreateChannelMutation,
  297. Codegen.CreateChannelMutationVariables
  298. >(CREATE_CHANNEL, {
  299. input: {
  300. code: 'other-channel',
  301. currencyCode: CurrencyCode.GBP,
  302. pricesIncludeTax: false,
  303. defaultTaxZoneId: 'T_1',
  304. defaultShippingZoneId: 'T_1',
  305. defaultLanguageCode: LanguageCode.en,
  306. token: OTHER_CHANNEL_TOKEN,
  307. },
  308. });
  309. await createPromotion({
  310. enabled: true,
  311. name: 'Other Channel Promo',
  312. couponCode: OTHER_CHANNEL_COUPON_CODE,
  313. conditions: [],
  314. actions: [freeOrderAction],
  315. });
  316. });
  317. afterAll(() => {
  318. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  319. });
  320. // https://github.com/vendure-ecommerce/vendure/issues/1692
  321. it('does not allow a couponCode from another channel', async () => {
  322. shopClient.setChannelToken(OTHER_CHANNEL_TOKEN);
  323. const { applyCouponCode } = await shopClient.query<
  324. ApplyCouponCode.Mutation,
  325. ApplyCouponCode.Variables
  326. >(APPLY_COUPON_CODE, {
  327. couponCode: OTHER_CHANNEL_COUPON_CODE,
  328. });
  329. orderResultGuard.assertErrorResult(applyCouponCode);
  330. expect(applyCouponCode.errorCode).toEqual('COUPON_CODE_INVALID_ERROR');
  331. });
  332. });
  333. });
  334. describe('default PromotionConditions', () => {
  335. beforeEach(async () => {
  336. await shopClient.asAnonymousUser();
  337. });
  338. it('minimumOrderAmount', async () => {
  339. const promotion = await createPromotion({
  340. enabled: true,
  341. name: 'Free if order total greater than 100',
  342. conditions: [minOrderAmountCondition(10000)],
  343. actions: [freeOrderAction],
  344. });
  345. const { addItemToOrder } = await shopClient.query<
  346. CodegenShop.AddItemToOrderMutation,
  347. CodegenShop.AddItemToOrderMutationVariables
  348. >(ADD_ITEM_TO_ORDER, {
  349. productVariantId: getVariantBySlug('item-5000').id,
  350. quantity: 1,
  351. });
  352. orderResultGuard.assertSuccess(addItemToOrder);
  353. expect(addItemToOrder.totalWithTax).toBe(6000);
  354. expect(addItemToOrder.discounts.length).toBe(0);
  355. const { adjustOrderLine } = await shopClient.query<
  356. CodegenShop.AdjustItemQuantityMutation,
  357. CodegenShop.AdjustItemQuantityMutationVariables
  358. >(ADJUST_ITEM_QUANTITY, {
  359. orderLineId: addItemToOrder.lines[0].id,
  360. quantity: 2,
  361. });
  362. orderResultGuard.assertSuccess(adjustOrderLine);
  363. expect(adjustOrderLine.totalWithTax).toBe(0);
  364. expect(adjustOrderLine.discounts[0].description).toBe('Free if order total greater than 100');
  365. expect(adjustOrderLine.discounts[0].amountWithTax).toBe(-12000);
  366. await deletePromotion(promotion.id);
  367. });
  368. it('atLeastNWithFacets', async () => {
  369. const { facets } = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  370. const saleFacetValue = facets.items[0].values[0];
  371. const promotion = await createPromotion({
  372. enabled: true,
  373. name: 'Free if order contains 2 items with Sale facet value',
  374. conditions: [
  375. {
  376. code: hasFacetValues.code,
  377. arguments: [
  378. { name: 'minimum', value: '2' },
  379. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  380. ],
  381. },
  382. ],
  383. actions: [freeOrderAction],
  384. });
  385. const { addItemToOrder: res1 } = await shopClient.query<
  386. CodegenShop.AddItemToOrderMutation,
  387. CodegenShop.AddItemToOrderMutationVariables
  388. >(ADD_ITEM_TO_ORDER, {
  389. productVariantId: getVariantBySlug('item-sale-100').id,
  390. quantity: 1,
  391. });
  392. orderResultGuard.assertSuccess(res1);
  393. expect(res1.totalWithTax).toBe(120);
  394. expect(res1.discounts.length).toBe(0);
  395. const { addItemToOrder: res2 } = await shopClient.query<
  396. CodegenShop.AddItemToOrderMutation,
  397. CodegenShop.AddItemToOrderMutationVariables
  398. >(ADD_ITEM_TO_ORDER, {
  399. productVariantId: getVariantBySlug('item-sale-1000').id,
  400. quantity: 1,
  401. });
  402. orderResultGuard.assertSuccess(res2);
  403. expect(res2.totalWithTax).toBe(0);
  404. expect(res2.discounts.length).toBe(1);
  405. expect(res2.totalWithTax).toBe(0);
  406. expect(res2.discounts[0].description).toBe(
  407. 'Free if order contains 2 items with Sale facet value',
  408. );
  409. expect(res2.discounts[0].amountWithTax).toBe(-1320);
  410. await deletePromotion(promotion.id);
  411. });
  412. it('containsProducts', async () => {
  413. const item5000 = getVariantBySlug('item-5000')!;
  414. const item1000 = getVariantBySlug('item-1000')!;
  415. const promotion = await createPromotion({
  416. enabled: true,
  417. name: 'Free if buying 3 or more offer products',
  418. conditions: [
  419. {
  420. code: containsProducts.code,
  421. arguments: [
  422. { name: 'minimum', value: '3' },
  423. {
  424. name: 'productVariantIds',
  425. value: JSON.stringify([item5000.id, item1000.id]),
  426. },
  427. ],
  428. },
  429. ],
  430. actions: [freeOrderAction],
  431. });
  432. await shopClient.query<
  433. CodegenShop.AddItemToOrderMutation,
  434. CodegenShop.AddItemToOrderMutationVariables
  435. >(ADD_ITEM_TO_ORDER, {
  436. productVariantId: item5000.id,
  437. quantity: 1,
  438. });
  439. const { addItemToOrder } = await shopClient.query<
  440. CodegenShop.AddItemToOrderMutation,
  441. CodegenShop.AddItemToOrderMutationVariables
  442. >(ADD_ITEM_TO_ORDER, {
  443. productVariantId: item1000.id,
  444. quantity: 1,
  445. });
  446. orderResultGuard.assertSuccess(addItemToOrder);
  447. expect(addItemToOrder.totalWithTax).toBe(7200);
  448. expect(addItemToOrder.discounts.length).toBe(0);
  449. const { adjustOrderLine } = await shopClient.query<
  450. CodegenShop.AdjustItemQuantityMutation,
  451. CodegenShop.AdjustItemQuantityMutationVariables
  452. >(ADJUST_ITEM_QUANTITY, {
  453. orderLineId: addItemToOrder.lines.find(l => l.productVariant.id === item5000.id)!.id,
  454. quantity: 2,
  455. });
  456. orderResultGuard.assertSuccess(adjustOrderLine);
  457. expect(adjustOrderLine.total).toBe(0);
  458. expect(adjustOrderLine.discounts[0].description).toBe('Free if buying 3 or more offer products');
  459. expect(adjustOrderLine.discounts[0].amountWithTax).toBe(-13200);
  460. await deletePromotion(promotion.id);
  461. });
  462. it('customerGroup', async () => {
  463. const { createCustomerGroup } = await adminClient.query<
  464. Codegen.CreateCustomerGroupMutation,
  465. Codegen.CreateCustomerGroupMutationVariables
  466. >(CREATE_CUSTOMER_GROUP, {
  467. input: { name: 'Test Group', customerIds: ['T_1'] },
  468. });
  469. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  470. const promotion = await createPromotion({
  471. enabled: true,
  472. name: 'Free for group members',
  473. conditions: [
  474. {
  475. code: customerGroup.code,
  476. arguments: [{ name: 'customerGroupId', value: createCustomerGroup.id }],
  477. },
  478. ],
  479. actions: [freeOrderAction],
  480. });
  481. const { addItemToOrder } = await shopClient.query<
  482. CodegenShop.AddItemToOrderMutation,
  483. CodegenShop.AddItemToOrderMutationVariables
  484. >(ADD_ITEM_TO_ORDER, {
  485. productVariantId: getVariantBySlug('item-5000').id,
  486. quantity: 1,
  487. });
  488. orderResultGuard.assertSuccess(addItemToOrder);
  489. expect(addItemToOrder.totalWithTax).toBe(0);
  490. expect(addItemToOrder.discounts.length).toBe(1);
  491. expect(addItemToOrder.discounts[0].description).toBe('Free for group members');
  492. expect(addItemToOrder.discounts[0].amountWithTax).toBe(-6000);
  493. await adminClient.query<
  494. Codegen.RemoveCustomersFromGroupMutation,
  495. Codegen.RemoveCustomersFromGroupMutationVariables
  496. >(REMOVE_CUSTOMERS_FROM_GROUP, {
  497. groupId: createCustomerGroup.id,
  498. customerIds: ['T_1'],
  499. });
  500. const { adjustOrderLine } = await shopClient.query<
  501. CodegenShop.AdjustItemQuantityMutation,
  502. CodegenShop.AdjustItemQuantityMutationVariables
  503. >(ADJUST_ITEM_QUANTITY, {
  504. orderLineId: addItemToOrder.lines[0].id,
  505. quantity: 2,
  506. });
  507. orderResultGuard.assertSuccess(adjustOrderLine);
  508. expect(adjustOrderLine.totalWithTax).toBe(12000);
  509. expect(adjustOrderLine.discounts.length).toBe(0);
  510. await deletePromotion(promotion.id);
  511. });
  512. });
  513. describe('default PromotionActions', () => {
  514. const TAX_INCLUDED_CHANNEL_TOKEN = 'tax_included_channel';
  515. let taxIncludedChannel: Codegen.ChannelFragment;
  516. beforeAll(async () => {
  517. // Create a channel where the prices include tax, so we can ensure
  518. // that PromotionActions are working as expected when taxes are included
  519. const { createChannel } = await adminClient.query<
  520. Codegen.CreateChannelMutation,
  521. Codegen.CreateChannelMutationVariables
  522. >(CREATE_CHANNEL, {
  523. input: {
  524. code: 'tax-included-channel',
  525. currencyCode: CurrencyCode.GBP,
  526. pricesIncludeTax: true,
  527. defaultTaxZoneId: 'T_1',
  528. defaultShippingZoneId: 'T_1',
  529. defaultLanguageCode: LanguageCode.en,
  530. token: TAX_INCLUDED_CHANNEL_TOKEN,
  531. },
  532. });
  533. taxIncludedChannel = createChannel as Codegen.ChannelFragment;
  534. await adminClient.query<
  535. Codegen.AssignProductsToChannelMutation,
  536. Codegen.AssignProductsToChannelMutationVariables
  537. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  538. input: {
  539. channelId: taxIncludedChannel.id,
  540. priceFactor: 1,
  541. productIds: products.map(p => p.id),
  542. },
  543. });
  544. });
  545. beforeEach(async () => {
  546. await shopClient.asAnonymousUser();
  547. });
  548. async function assignPromotionToTaxIncludedChannel(promotionId: string | string[]) {
  549. await adminClient.query<
  550. Codegen.AssignPromotionToChannelMutation,
  551. Codegen.AssignPromotionToChannelMutationVariables
  552. >(ASSIGN_PROMOTIONS_TO_CHANNEL, {
  553. input: {
  554. promotionIds: Array.isArray(promotionId) ? promotionId : [promotionId],
  555. channelId: taxIncludedChannel.id,
  556. },
  557. });
  558. }
  559. describe('orderPercentageDiscount', () => {
  560. const couponCode = '50%_off_order';
  561. let promotion: Codegen.PromotionFragment;
  562. beforeAll(async () => {
  563. promotion = await createPromotion({
  564. enabled: true,
  565. name: '20% discount on order',
  566. couponCode,
  567. conditions: [],
  568. actions: [
  569. {
  570. code: orderPercentageDiscount.code,
  571. arguments: [{ name: 'discount', value: '20' }],
  572. },
  573. ],
  574. });
  575. await assignPromotionToTaxIncludedChannel(promotion.id);
  576. });
  577. afterAll(async () => {
  578. await deletePromotion(promotion.id);
  579. });
  580. it('prices exclude tax', async () => {
  581. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  582. const { addItemToOrder } = await shopClient.query<
  583. CodegenShop.AddItemToOrderMutation,
  584. CodegenShop.AddItemToOrderMutationVariables
  585. >(ADD_ITEM_TO_ORDER, {
  586. productVariantId: getVariantBySlug('item-5000').id,
  587. quantity: 1,
  588. });
  589. orderResultGuard.assertSuccess(addItemToOrder);
  590. expect(addItemToOrder.totalWithTax).toBe(6000);
  591. expect(addItemToOrder.discounts.length).toBe(0);
  592. const { applyCouponCode } = await shopClient.query<
  593. CodegenShop.ApplyCouponCodeMutation,
  594. CodegenShop.ApplyCouponCodeMutationVariables
  595. >(APPLY_COUPON_CODE, {
  596. couponCode,
  597. });
  598. orderResultGuard.assertSuccess(applyCouponCode);
  599. expect(applyCouponCode.discounts.length).toBe(1);
  600. expect(applyCouponCode.discounts[0].description).toBe('20% discount on order');
  601. expect(applyCouponCode.totalWithTax).toBe(4800);
  602. });
  603. it('prices include tax', async () => {
  604. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  605. const { addItemToOrder } = await shopClient.query<
  606. CodegenShop.AddItemToOrderMutation,
  607. CodegenShop.AddItemToOrderMutationVariables
  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. CodegenShop.ApplyCouponCodeMutation,
  617. CodegenShop.ApplyCouponCodeMutationVariables
  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. // https://github.com/vendure-ecommerce/vendure/issues/1773
  627. it('decimal percentage', async () => {
  628. const decimalPercentageCouponCode = 'DPCC';
  629. await createPromotion({
  630. enabled: true,
  631. name: '10.5% discount on order',
  632. couponCode: decimalPercentageCouponCode,
  633. conditions: [],
  634. actions: [
  635. {
  636. code: orderPercentageDiscount.code,
  637. arguments: [{ name: 'discount', value: '10.5' }],
  638. },
  639. ],
  640. });
  641. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  642. const { addItemToOrder } = await shopClient.query<
  643. AddItemToOrder.Mutation,
  644. AddItemToOrder.Variables
  645. >(ADD_ITEM_TO_ORDER, {
  646. productVariantId: getVariantBySlug('item-5000').id,
  647. quantity: 1,
  648. });
  649. orderResultGuard.assertSuccess(addItemToOrder);
  650. expect(addItemToOrder.totalWithTax).toBe(6000);
  651. expect(addItemToOrder.discounts.length).toBe(0);
  652. const { applyCouponCode } = await shopClient.query<
  653. ApplyCouponCode.Mutation,
  654. ApplyCouponCode.Variables
  655. >(APPLY_COUPON_CODE, {
  656. couponCode: decimalPercentageCouponCode,
  657. });
  658. orderResultGuard.assertSuccess(applyCouponCode);
  659. expect(applyCouponCode.discounts.length).toBe(1);
  660. expect(applyCouponCode.discounts[0].description).toBe('10.5% discount on order');
  661. expect(applyCouponCode.totalWithTax).toBe(5370);
  662. });
  663. });
  664. describe('orderFixedDiscount', () => {
  665. const couponCode = '10_off_order';
  666. let promotion: Codegen.PromotionFragment;
  667. beforeAll(async () => {
  668. promotion = await createPromotion({
  669. enabled: true,
  670. name: '$10 discount on order',
  671. couponCode,
  672. conditions: [],
  673. actions: [
  674. {
  675. code: orderFixedDiscount.code,
  676. arguments: [{ name: 'discount', value: '1000' }],
  677. },
  678. ],
  679. });
  680. await assignPromotionToTaxIncludedChannel(promotion.id);
  681. });
  682. afterAll(async () => {
  683. await deletePromotion(promotion.id);
  684. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  685. });
  686. it('prices exclude tax', async () => {
  687. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  688. const { addItemToOrder } = await shopClient.query<
  689. CodegenShop.AddItemToOrderMutation,
  690. CodegenShop.AddItemToOrderMutationVariables
  691. >(ADD_ITEM_TO_ORDER, {
  692. productVariantId: getVariantBySlug('item-5000').id,
  693. quantity: 1,
  694. });
  695. orderResultGuard.assertSuccess(addItemToOrder);
  696. expect(addItemToOrder.total).toBe(5000);
  697. expect(addItemToOrder.totalWithTax).toBe(6000);
  698. expect(addItemToOrder.discounts.length).toBe(0);
  699. const { applyCouponCode } = await shopClient.query<
  700. CodegenShop.ApplyCouponCodeMutation,
  701. CodegenShop.ApplyCouponCodeMutationVariables
  702. >(APPLY_COUPON_CODE, {
  703. couponCode,
  704. });
  705. orderResultGuard.assertSuccess(applyCouponCode);
  706. expect(applyCouponCode.discounts.length).toBe(1);
  707. expect(applyCouponCode.discounts[0].description).toBe('$10 discount on order');
  708. expect(applyCouponCode.total).toBe(4000);
  709. expect(applyCouponCode.totalWithTax).toBe(4800);
  710. });
  711. it('prices include tax', async () => {
  712. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  713. const { addItemToOrder } = await shopClient.query<
  714. CodegenShop.AddItemToOrderMutation,
  715. CodegenShop.AddItemToOrderMutationVariables
  716. >(ADD_ITEM_TO_ORDER, {
  717. productVariantId: getVariantBySlug('item-5000').id,
  718. quantity: 1,
  719. });
  720. orderResultGuard.assertSuccess(addItemToOrder);
  721. expect(addItemToOrder.totalWithTax).toBe(6000);
  722. expect(addItemToOrder.discounts.length).toBe(0);
  723. const { applyCouponCode } = await shopClient.query<
  724. CodegenShop.ApplyCouponCodeMutation,
  725. CodegenShop.ApplyCouponCodeMutationVariables
  726. >(APPLY_COUPON_CODE, {
  727. couponCode,
  728. });
  729. orderResultGuard.assertSuccess(applyCouponCode);
  730. expect(applyCouponCode.discounts.length).toBe(1);
  731. expect(applyCouponCode.discounts[0].description).toBe('$10 discount on order');
  732. expect(applyCouponCode.totalWithTax).toBe(5000);
  733. });
  734. it('does not result in negative total when shipping is included', async () => {
  735. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  736. const { addItemToOrder } = await shopClient.query<
  737. CodegenShop.AddItemToOrderMutation,
  738. CodegenShop.AddItemToOrderMutationVariables
  739. >(ADD_ITEM_TO_ORDER, {
  740. productVariantId: getVariantBySlug('item-100').id,
  741. quantity: 1,
  742. });
  743. orderResultGuard.assertSuccess(addItemToOrder);
  744. expect(addItemToOrder.totalWithTax).toBe(120);
  745. expect(addItemToOrder.discounts.length).toBe(0);
  746. const { setOrderShippingMethod } = await shopClient.query<
  747. CodegenShop.SetShippingMethodMutation,
  748. CodegenShop.SetShippingMethodMutationVariables
  749. >(SET_SHIPPING_METHOD, {
  750. id: 'T_1',
  751. });
  752. orderResultGuard.assertSuccess(setOrderShippingMethod);
  753. expect(setOrderShippingMethod.totalWithTax).toBe(620);
  754. const { applyCouponCode } = await shopClient.query<
  755. CodegenShop.ApplyCouponCodeMutation,
  756. CodegenShop.ApplyCouponCodeMutationVariables
  757. >(APPLY_COUPON_CODE, {
  758. couponCode,
  759. });
  760. orderResultGuard.assertSuccess(applyCouponCode);
  761. expect(applyCouponCode.discounts.length).toBe(1);
  762. expect(applyCouponCode.discounts[0].description).toBe('$10 discount on order');
  763. expect(applyCouponCode.subTotalWithTax).toBe(0);
  764. expect(applyCouponCode.totalWithTax).toBe(500); // shipping price
  765. });
  766. });
  767. describe('orderLineFixedDiscount', () => {
  768. const couponCode = '1000_off_order_line';
  769. let promotion: Codegen.PromotionFragment;
  770. beforeAll(async () => {
  771. promotion = await createPromotion({
  772. enabled: true,
  773. name: '$1000 discount on order line',
  774. couponCode,
  775. conditions: [],
  776. actions: [
  777. {
  778. code: orderLineFixedDiscount.code,
  779. arguments: [{ name: 'discount', value: '1000' }],
  780. },
  781. ],
  782. });
  783. });
  784. afterAll(async () => {
  785. await deletePromotion(promotion.id);
  786. });
  787. it('prices exclude tax', async () => {
  788. await shopClient.asAnonymousUser();
  789. const { addItemToOrder } = await shopClient.query<
  790. CodegenShop.AddItemToOrderMutation,
  791. CodegenShop.AddItemToOrderMutationVariables
  792. >(ADD_ITEM_TO_ORDER, {
  793. productVariantId: getVariantBySlug('item-1000').id,
  794. quantity: 3,
  795. });
  796. orderResultGuard.assertSuccess(addItemToOrder);
  797. expect(addItemToOrder.discounts.length).toBe(0);
  798. expect(addItemToOrder.lines[0].discounts.length).toBe(0);
  799. expect(addItemToOrder.total).toBe(3000);
  800. expect(addItemToOrder.totalWithTax).toBe(3600);
  801. const { applyCouponCode } = await shopClient.query<
  802. CodegenShop.ApplyCouponCodeMutation,
  803. CodegenShop.ApplyCouponCodeMutationVariables
  804. >(APPLY_COUPON_CODE, {
  805. couponCode,
  806. });
  807. orderResultGuard.assertSuccess(applyCouponCode);
  808. expect(applyCouponCode.total).toBe(2000);
  809. expect(applyCouponCode.totalWithTax).toBe(2400);
  810. expect(applyCouponCode.lines[0].discounts.length).toBe(1);
  811. });
  812. });
  813. describe('discountOnItemWithFacets', () => {
  814. const couponCode = '50%_off_sale_items';
  815. let promotion: Codegen.PromotionFragment;
  816. function getItemSale1Line<
  817. T extends Array<
  818. | CodegenShop.UpdatedOrderFragment['lines'][number]
  819. | CodegenShop.TestOrderFragmentFragment['lines'][number]
  820. >,
  821. >(lines: T): T[number] {
  822. return lines.find(l => l.productVariant.id === getVariantBySlug('item-sale-100').id)!;
  823. }
  824. beforeAll(async () => {
  825. const { facets } = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  826. const saleFacetValue = facets.items[0].values[0];
  827. promotion = await createPromotion({
  828. enabled: true,
  829. name: '50% off sale items',
  830. couponCode,
  831. conditions: [],
  832. actions: [
  833. {
  834. code: discountOnItemWithFacets.code,
  835. arguments: [
  836. { name: 'discount', value: '50' },
  837. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  838. ],
  839. },
  840. ],
  841. });
  842. await assignPromotionToTaxIncludedChannel(promotion.id);
  843. });
  844. afterAll(async () => {
  845. await deletePromotion(promotion.id);
  846. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  847. });
  848. it('prices exclude tax', async () => {
  849. await shopClient.query<
  850. CodegenShop.AddItemToOrderMutation,
  851. CodegenShop.AddItemToOrderMutationVariables
  852. >(ADD_ITEM_TO_ORDER, {
  853. productVariantId: getVariantBySlug('item-1000').id,
  854. quantity: 1,
  855. });
  856. await shopClient.query<
  857. CodegenShop.AddItemToOrderMutation,
  858. CodegenShop.AddItemToOrderMutationVariables
  859. >(ADD_ITEM_TO_ORDER, {
  860. productVariantId: getVariantBySlug('item-sale-1000').id,
  861. quantity: 1,
  862. });
  863. const { addItemToOrder } = await shopClient.query<
  864. CodegenShop.AddItemToOrderMutation,
  865. CodegenShop.AddItemToOrderMutationVariables
  866. >(ADD_ITEM_TO_ORDER, {
  867. productVariantId: getVariantBySlug('item-sale-100').id,
  868. quantity: 2,
  869. });
  870. orderResultGuard.assertSuccess(addItemToOrder);
  871. expect(addItemToOrder.discounts.length).toBe(0);
  872. expect(getItemSale1Line(addItemToOrder.lines).discounts.length).toBe(0);
  873. expect(addItemToOrder.total).toBe(2200);
  874. expect(addItemToOrder.totalWithTax).toBe(2640);
  875. const { applyCouponCode } = await shopClient.query<
  876. CodegenShop.ApplyCouponCodeMutation,
  877. CodegenShop.ApplyCouponCodeMutationVariables
  878. >(APPLY_COUPON_CODE, {
  879. couponCode,
  880. });
  881. orderResultGuard.assertSuccess(applyCouponCode);
  882. expect(applyCouponCode.total).toBe(1600);
  883. expect(applyCouponCode.totalWithTax).toBe(1920);
  884. expect(getItemSale1Line(applyCouponCode.lines).discounts.length).toBe(1); // 1x promotion
  885. const { removeCouponCode } = await shopClient.query<
  886. CodegenShop.RemoveCouponCodeMutation,
  887. CodegenShop.RemoveCouponCodeMutationVariables
  888. >(REMOVE_COUPON_CODE, {
  889. couponCode,
  890. });
  891. expect(getItemSale1Line(removeCouponCode!.lines).discounts.length).toBe(0);
  892. expect(removeCouponCode!.total).toBe(2200);
  893. expect(removeCouponCode!.totalWithTax).toBe(2640);
  894. const { activeOrder } =
  895. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  896. expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
  897. expect(activeOrder!.total).toBe(2200);
  898. expect(activeOrder!.totalWithTax).toBe(2640);
  899. });
  900. it('prices include tax', async () => {
  901. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  902. await shopClient.query<
  903. CodegenShop.AddItemToOrderMutation,
  904. CodegenShop.AddItemToOrderMutationVariables
  905. >(ADD_ITEM_TO_ORDER, {
  906. productVariantId: getVariantBySlug('item-1000').id,
  907. quantity: 1,
  908. });
  909. await shopClient.query<
  910. CodegenShop.AddItemToOrderMutation,
  911. CodegenShop.AddItemToOrderMutationVariables
  912. >(ADD_ITEM_TO_ORDER, {
  913. productVariantId: getVariantBySlug('item-sale-1000').id,
  914. quantity: 1,
  915. });
  916. const { addItemToOrder } = await shopClient.query<
  917. CodegenShop.AddItemToOrderMutation,
  918. CodegenShop.AddItemToOrderMutationVariables
  919. >(ADD_ITEM_TO_ORDER, {
  920. productVariantId: getVariantBySlug('item-sale-100').id,
  921. quantity: 2,
  922. });
  923. orderResultGuard.assertSuccess(addItemToOrder);
  924. expect(addItemToOrder.discounts.length).toBe(0);
  925. expect(getItemSale1Line(addItemToOrder.lines).discounts.length).toBe(0);
  926. expect(addItemToOrder.total).toBe(2200);
  927. expect(addItemToOrder.totalWithTax).toBe(2640);
  928. const { applyCouponCode } = await shopClient.query<
  929. CodegenShop.ApplyCouponCodeMutation,
  930. CodegenShop.ApplyCouponCodeMutationVariables
  931. >(APPLY_COUPON_CODE, {
  932. couponCode,
  933. });
  934. orderResultGuard.assertSuccess(applyCouponCode);
  935. expect(applyCouponCode.total).toBe(1600);
  936. expect(applyCouponCode.totalWithTax).toBe(1920);
  937. expect(getItemSale1Line(applyCouponCode.lines).discounts.length).toBe(1); // 1x promotion
  938. const { removeCouponCode } = await shopClient.query<
  939. CodegenShop.RemoveCouponCodeMutation,
  940. CodegenShop.RemoveCouponCodeMutationVariables
  941. >(REMOVE_COUPON_CODE, {
  942. couponCode,
  943. });
  944. expect(getItemSale1Line(removeCouponCode!.lines).discounts.length).toBe(0);
  945. expect(removeCouponCode!.total).toBe(2200);
  946. expect(removeCouponCode!.totalWithTax).toBe(2640);
  947. const { activeOrder } =
  948. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  949. expect(getItemSale1Line(activeOrder!.lines).discounts.length).toBe(0);
  950. expect(activeOrder!.total).toBe(2200);
  951. expect(activeOrder!.totalWithTax).toBe(2640);
  952. });
  953. });
  954. describe('productsPercentageDiscount', () => {
  955. const couponCode = '50%_off_product';
  956. let promotion: Codegen.PromotionFragment;
  957. beforeAll(async () => {
  958. promotion = await createPromotion({
  959. enabled: true,
  960. name: '50% off product',
  961. couponCode,
  962. conditions: [],
  963. actions: [
  964. {
  965. code: productsPercentageDiscount.code,
  966. arguments: [
  967. { name: 'discount', value: '50' },
  968. {
  969. name: 'productVariantIds',
  970. value: `["${getVariantBySlug('item-5000').id}"]`,
  971. },
  972. ],
  973. },
  974. ],
  975. });
  976. await assignPromotionToTaxIncludedChannel(promotion.id);
  977. });
  978. afterAll(async () => {
  979. await deletePromotion(promotion.id);
  980. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  981. });
  982. it('prices exclude tax', async () => {
  983. const { addItemToOrder } = await shopClient.query<
  984. CodegenShop.AddItemToOrderMutation,
  985. CodegenShop.AddItemToOrderMutationVariables
  986. >(ADD_ITEM_TO_ORDER, {
  987. productVariantId: getVariantBySlug('item-5000').id,
  988. quantity: 1,
  989. });
  990. orderResultGuard.assertSuccess(addItemToOrder);
  991. expect(addItemToOrder.discounts.length).toBe(0);
  992. expect(addItemToOrder.lines[0].discounts.length).toBe(0);
  993. expect(addItemToOrder.total).toBe(5000);
  994. expect(addItemToOrder.totalWithTax).toBe(6000);
  995. const { applyCouponCode } = await shopClient.query<
  996. CodegenShop.ApplyCouponCodeMutation,
  997. CodegenShop.ApplyCouponCodeMutationVariables
  998. >(APPLY_COUPON_CODE, {
  999. couponCode,
  1000. });
  1001. orderResultGuard.assertSuccess(applyCouponCode);
  1002. expect(applyCouponCode.total).toBe(2500);
  1003. expect(applyCouponCode.totalWithTax).toBe(3000);
  1004. expect(applyCouponCode.lines[0].discounts.length).toBe(1); // 1x promotion
  1005. });
  1006. it('prices include tax', async () => {
  1007. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  1008. const { addItemToOrder } = await shopClient.query<
  1009. CodegenShop.AddItemToOrderMutation,
  1010. CodegenShop.AddItemToOrderMutationVariables
  1011. >(ADD_ITEM_TO_ORDER, {
  1012. productVariantId: getVariantBySlug('item-5000').id,
  1013. quantity: 1,
  1014. });
  1015. orderResultGuard.assertSuccess(addItemToOrder);
  1016. expect(addItemToOrder.discounts.length).toBe(0);
  1017. expect(addItemToOrder.lines[0].discounts.length).toBe(0);
  1018. expect(addItemToOrder.total).toBe(5000);
  1019. expect(addItemToOrder.totalWithTax).toBe(6000);
  1020. const { applyCouponCode } = await shopClient.query<
  1021. CodegenShop.ApplyCouponCodeMutation,
  1022. CodegenShop.ApplyCouponCodeMutationVariables
  1023. >(APPLY_COUPON_CODE, {
  1024. couponCode,
  1025. });
  1026. orderResultGuard.assertSuccess(applyCouponCode);
  1027. expect(applyCouponCode.total).toBe(2500);
  1028. expect(applyCouponCode.totalWithTax).toBe(3000);
  1029. expect(applyCouponCode.lines[0].discounts.length).toBe(1); // 1x promotion
  1030. });
  1031. });
  1032. describe('freeShipping', () => {
  1033. const couponCode = 'FREE_SHIPPING';
  1034. let promotion: Codegen.PromotionFragment;
  1035. // The test shipping method needs to be created in each Channel, since ShippingMethods
  1036. // are ChannelAware
  1037. async function createTestShippingMethod(channelToken: string) {
  1038. adminClient.setChannelToken(channelToken);
  1039. const result = await adminClient.query<
  1040. Codegen.CreateShippingMethodMutation,
  1041. Codegen.CreateShippingMethodMutationVariables
  1042. >(CREATE_SHIPPING_METHOD, {
  1043. input: {
  1044. code: 'test-method',
  1045. fulfillmentHandler: manualFulfillmentHandler.code,
  1046. checker: {
  1047. code: defaultShippingEligibilityChecker.code,
  1048. arguments: [
  1049. {
  1050. name: 'orderMinimum',
  1051. value: '0',
  1052. },
  1053. ],
  1054. },
  1055. calculator: {
  1056. code: defaultShippingCalculator.code,
  1057. arguments: [
  1058. { name: 'rate', value: '345' },
  1059. { name: 'includesTax', value: 'auto' },
  1060. { name: 'taxRate', value: '20' },
  1061. ],
  1062. },
  1063. translations: [
  1064. { languageCode: LanguageCode.en, name: 'test method', description: '' },
  1065. ],
  1066. },
  1067. });
  1068. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1069. return result.createShippingMethod;
  1070. }
  1071. beforeAll(async () => {
  1072. promotion = await createPromotion({
  1073. enabled: true,
  1074. name: 'Free shipping',
  1075. couponCode,
  1076. conditions: [],
  1077. actions: [
  1078. {
  1079. code: freeShipping.code,
  1080. arguments: [],
  1081. },
  1082. ],
  1083. });
  1084. await assignPromotionToTaxIncludedChannel(promotion.id);
  1085. });
  1086. afterAll(async () => {
  1087. await deletePromotion(promotion.id);
  1088. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1089. });
  1090. it('prices exclude tax', async () => {
  1091. const { addItemToOrder } = await shopClient.query<
  1092. CodegenShop.AddItemToOrderMutation,
  1093. CodegenShop.AddItemToOrderMutationVariables
  1094. >(ADD_ITEM_TO_ORDER, {
  1095. productVariantId: getVariantBySlug('item-5000').id,
  1096. quantity: 1,
  1097. });
  1098. const method = await createTestShippingMethod(E2E_DEFAULT_CHANNEL_TOKEN);
  1099. const { setOrderShippingMethod } = await shopClient.query<
  1100. CodegenShop.SetShippingMethodMutation,
  1101. CodegenShop.SetShippingMethodMutationVariables
  1102. >(SET_SHIPPING_METHOD, {
  1103. id: method.id,
  1104. });
  1105. orderResultGuard.assertSuccess(setOrderShippingMethod);
  1106. expect(setOrderShippingMethod.discounts).toEqual([]);
  1107. expect(setOrderShippingMethod.shipping).toBe(345);
  1108. expect(setOrderShippingMethod.shippingWithTax).toBe(414);
  1109. expect(setOrderShippingMethod.total).toBe(5345);
  1110. expect(setOrderShippingMethod.totalWithTax).toBe(6414);
  1111. const { applyCouponCode } = await shopClient.query<
  1112. CodegenShop.ApplyCouponCodeMutation,
  1113. CodegenShop.ApplyCouponCodeMutationVariables
  1114. >(APPLY_COUPON_CODE, {
  1115. couponCode,
  1116. });
  1117. orderResultGuard.assertSuccess(applyCouponCode);
  1118. expect(applyCouponCode.discounts.length).toBe(1);
  1119. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  1120. expect(applyCouponCode.shipping).toBe(0);
  1121. expect(applyCouponCode.shippingWithTax).toBe(0);
  1122. expect(applyCouponCode.total).toBe(5000);
  1123. expect(applyCouponCode.totalWithTax).toBe(6000);
  1124. });
  1125. it('prices include tax', async () => {
  1126. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  1127. const { addItemToOrder } = await shopClient.query<
  1128. CodegenShop.AddItemToOrderMutation,
  1129. CodegenShop.AddItemToOrderMutationVariables
  1130. >(ADD_ITEM_TO_ORDER, {
  1131. productVariantId: getVariantBySlug('item-5000').id,
  1132. quantity: 1,
  1133. });
  1134. const method = await createTestShippingMethod(TAX_INCLUDED_CHANNEL_TOKEN);
  1135. const { setOrderShippingMethod } = await shopClient.query<
  1136. CodegenShop.SetShippingMethodMutation,
  1137. CodegenShop.SetShippingMethodMutationVariables
  1138. >(SET_SHIPPING_METHOD, {
  1139. id: method.id,
  1140. });
  1141. orderResultGuard.assertSuccess(setOrderShippingMethod);
  1142. expect(setOrderShippingMethod.discounts).toEqual([]);
  1143. expect(setOrderShippingMethod.shipping).toBe(288);
  1144. expect(setOrderShippingMethod.shippingWithTax).toBe(345);
  1145. expect(setOrderShippingMethod.total).toBe(5288);
  1146. expect(setOrderShippingMethod.totalWithTax).toBe(6345);
  1147. const { applyCouponCode } = await shopClient.query<
  1148. CodegenShop.ApplyCouponCodeMutation,
  1149. CodegenShop.ApplyCouponCodeMutationVariables
  1150. >(APPLY_COUPON_CODE, {
  1151. couponCode,
  1152. });
  1153. orderResultGuard.assertSuccess(applyCouponCode);
  1154. expect(applyCouponCode.discounts.length).toBe(1);
  1155. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  1156. expect(applyCouponCode.shipping).toBe(0);
  1157. expect(applyCouponCode.shippingWithTax).toBe(0);
  1158. expect(applyCouponCode.total).toBe(5000);
  1159. expect(applyCouponCode.totalWithTax).toBe(6000);
  1160. });
  1161. // https://github.com/vendure-ecommerce/vendure/pull/1150
  1162. it('shipping discounts get correctly removed', async () => {
  1163. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  1164. const { addItemToOrder } = await shopClient.query<
  1165. CodegenShop.AddItemToOrderMutation,
  1166. CodegenShop.AddItemToOrderMutationVariables
  1167. >(ADD_ITEM_TO_ORDER, {
  1168. productVariantId: getVariantBySlug('item-5000').id,
  1169. quantity: 1,
  1170. });
  1171. const method = await createTestShippingMethod(TAX_INCLUDED_CHANNEL_TOKEN);
  1172. const { setOrderShippingMethod } = await shopClient.query<
  1173. CodegenShop.SetShippingMethodMutation,
  1174. CodegenShop.SetShippingMethodMutationVariables
  1175. >(SET_SHIPPING_METHOD, {
  1176. id: method.id,
  1177. });
  1178. orderResultGuard.assertSuccess(setOrderShippingMethod);
  1179. expect(setOrderShippingMethod.discounts).toEqual([]);
  1180. expect(setOrderShippingMethod.shippingWithTax).toBe(345);
  1181. const { applyCouponCode } = await shopClient.query<
  1182. CodegenShop.ApplyCouponCodeMutation,
  1183. CodegenShop.ApplyCouponCodeMutationVariables
  1184. >(APPLY_COUPON_CODE, {
  1185. couponCode,
  1186. });
  1187. orderResultGuard.assertSuccess(applyCouponCode);
  1188. expect(applyCouponCode.discounts.length).toBe(1);
  1189. expect(applyCouponCode.discounts[0].description).toBe('Free shipping');
  1190. expect(applyCouponCode.shippingWithTax).toBe(0);
  1191. const { removeCouponCode } = await shopClient.query<
  1192. CodegenShop.RemoveCouponCodeMutation,
  1193. CodegenShop.RemoveCouponCodeMutationVariables
  1194. >(REMOVE_COUPON_CODE, {
  1195. couponCode,
  1196. });
  1197. orderResultGuard.assertSuccess(removeCouponCode);
  1198. expect(removeCouponCode.discounts).toEqual([]);
  1199. expect(removeCouponCode.shippingWithTax).toBe(345);
  1200. });
  1201. });
  1202. describe('multiple promotions simultaneously', () => {
  1203. const saleItem50pcOffCoupon = 'CODE1';
  1204. const order15pcOffCoupon = 'CODE2';
  1205. let promotion1: Codegen.PromotionFragment;
  1206. let promotion2: Codegen.PromotionFragment;
  1207. beforeAll(async () => {
  1208. const { facets } = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  1209. const saleFacetValue = facets.items[0].values[0];
  1210. promotion1 = await createPromotion({
  1211. enabled: true,
  1212. name: 'item promo',
  1213. couponCode: saleItem50pcOffCoupon,
  1214. conditions: [],
  1215. actions: [
  1216. {
  1217. code: discountOnItemWithFacets.code,
  1218. arguments: [
  1219. { name: 'discount', value: '50' },
  1220. { name: 'facets', value: `["${saleFacetValue.id}"]` },
  1221. ],
  1222. },
  1223. ],
  1224. });
  1225. promotion2 = await createPromotion({
  1226. enabled: true,
  1227. name: 'order promo',
  1228. couponCode: order15pcOffCoupon,
  1229. conditions: [],
  1230. actions: [
  1231. {
  1232. code: orderPercentageDiscount.code,
  1233. arguments: [{ name: 'discount', value: '15' }],
  1234. },
  1235. ],
  1236. });
  1237. await assignPromotionToTaxIncludedChannel([promotion1.id, promotion2.id]);
  1238. });
  1239. afterAll(async () => {
  1240. await deletePromotion(promotion1.id);
  1241. await deletePromotion(promotion2.id);
  1242. shopClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  1243. });
  1244. it('prices exclude tax', async () => {
  1245. await shopClient.query<
  1246. CodegenShop.AddItemToOrderMutation,
  1247. CodegenShop.AddItemToOrderMutationVariables
  1248. >(ADD_ITEM_TO_ORDER, {
  1249. productVariantId: getVariantBySlug('item-sale-1000').id,
  1250. quantity: 2,
  1251. });
  1252. await shopClient.query<
  1253. CodegenShop.AddItemToOrderMutation,
  1254. CodegenShop.AddItemToOrderMutationVariables
  1255. >(ADD_ITEM_TO_ORDER, {
  1256. productVariantId: getVariantBySlug('item-5000').id,
  1257. quantity: 1,
  1258. });
  1259. // Apply the OrderItem-level promo
  1260. const { applyCouponCode: apply1 } = await shopClient.query<
  1261. CodegenShop.ApplyCouponCodeMutation,
  1262. CodegenShop.ApplyCouponCodeMutationVariables
  1263. >(APPLY_COUPON_CODE, {
  1264. couponCode: saleItem50pcOffCoupon,
  1265. });
  1266. orderResultGuard.assertSuccess(apply1);
  1267. const saleItemLine = apply1.lines.find(
  1268. l => l.productVariant.id === getVariantBySlug('item-sale-1000').id,
  1269. )!;
  1270. expect(saleItemLine.discounts.length).toBe(1); // 1x promotion
  1271. expect(
  1272. saleItemLine.discounts.find(a => a.type === AdjustmentType.PROMOTION)?.description,
  1273. ).toBe('item promo');
  1274. expect(apply1.discounts.length).toBe(1);
  1275. expect(apply1.total).toBe(6000);
  1276. expect(apply1.totalWithTax).toBe(7200);
  1277. // Apply the Order-level promo
  1278. const { applyCouponCode: apply2 } = await shopClient.query<
  1279. CodegenShop.ApplyCouponCodeMutation,
  1280. CodegenShop.ApplyCouponCodeMutationVariables
  1281. >(APPLY_COUPON_CODE, {
  1282. couponCode: order15pcOffCoupon,
  1283. });
  1284. orderResultGuard.assertSuccess(apply2);
  1285. expect(apply2.discounts.map(d => d.description).sort()).toEqual([
  1286. 'item promo',
  1287. 'order promo',
  1288. ]);
  1289. expect(apply2.total).toBe(5100);
  1290. expect(apply2.totalWithTax).toBe(6120);
  1291. });
  1292. it('prices include tax', async () => {
  1293. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN);
  1294. await shopClient.query<
  1295. CodegenShop.AddItemToOrderMutation,
  1296. CodegenShop.AddItemToOrderMutationVariables
  1297. >(ADD_ITEM_TO_ORDER, {
  1298. productVariantId: getVariantBySlug('item-sale-1000').id,
  1299. quantity: 2,
  1300. });
  1301. await shopClient.query<
  1302. CodegenShop.AddItemToOrderMutation,
  1303. CodegenShop.AddItemToOrderMutationVariables
  1304. >(ADD_ITEM_TO_ORDER, {
  1305. productVariantId: getVariantBySlug('item-5000').id,
  1306. quantity: 1,
  1307. });
  1308. // Apply the OrderItem-level promo
  1309. const { applyCouponCode: apply1 } = await shopClient.query<
  1310. CodegenShop.ApplyCouponCodeMutation,
  1311. CodegenShop.ApplyCouponCodeMutationVariables
  1312. >(APPLY_COUPON_CODE, {
  1313. couponCode: saleItem50pcOffCoupon,
  1314. });
  1315. orderResultGuard.assertSuccess(apply1);
  1316. const saleItemLine = apply1.lines.find(
  1317. l => l.productVariant.id === getVariantBySlug('item-sale-1000').id,
  1318. )!;
  1319. expect(saleItemLine.discounts.length).toBe(1); // 1x promotion
  1320. expect(
  1321. saleItemLine.discounts.find(a => a.type === AdjustmentType.PROMOTION)?.description,
  1322. ).toBe('item promo');
  1323. expect(apply1.discounts.length).toBe(1);
  1324. expect(apply1.total).toBe(6000);
  1325. expect(apply1.totalWithTax).toBe(7200);
  1326. // Apply the Order-level promo
  1327. const { applyCouponCode: apply2 } = await shopClient.query<
  1328. CodegenShop.ApplyCouponCodeMutation,
  1329. CodegenShop.ApplyCouponCodeMutationVariables
  1330. >(APPLY_COUPON_CODE, {
  1331. couponCode: order15pcOffCoupon,
  1332. });
  1333. orderResultGuard.assertSuccess(apply2);
  1334. expect(apply2.discounts.map(d => d.description).sort()).toEqual([
  1335. 'item promo',
  1336. 'order promo',
  1337. ]);
  1338. expect(apply2.total).toBe(5100);
  1339. expect(apply2.totalWithTax).toBe(6120);
  1340. });
  1341. });
  1342. });
  1343. describe('per-customer usage limit', () => {
  1344. const TEST_COUPON_CODE = 'TESTCOUPON';
  1345. const orderGuard: ErrorResultGuard<CodegenShop.TestOrderWithPaymentsFragment> =
  1346. createErrorResultGuard(input => !!input.lines);
  1347. let promoWithUsageLimit: Codegen.PromotionFragment;
  1348. beforeAll(async () => {
  1349. promoWithUsageLimit = await createPromotion({
  1350. enabled: true,
  1351. name: 'Free with test coupon',
  1352. couponCode: TEST_COUPON_CODE,
  1353. perCustomerUsageLimit: 1,
  1354. conditions: [],
  1355. actions: [freeOrderAction],
  1356. });
  1357. });
  1358. afterAll(async () => {
  1359. await deletePromotion(promoWithUsageLimit.id);
  1360. });
  1361. async function createNewActiveOrder() {
  1362. const { addItemToOrder } = await shopClient.query<
  1363. CodegenShop.AddItemToOrderMutation,
  1364. CodegenShop.AddItemToOrderMutationVariables
  1365. >(ADD_ITEM_TO_ORDER, {
  1366. productVariantId: getVariantBySlug('item-5000').id,
  1367. quantity: 1,
  1368. });
  1369. return addItemToOrder;
  1370. }
  1371. describe('guest customer', () => {
  1372. const GUEST_EMAIL_ADDRESS = 'guest@test.com';
  1373. let orderCode: string;
  1374. function addGuestCustomerToOrder() {
  1375. return shopClient.query<
  1376. CodegenShop.SetCustomerForOrderMutation,
  1377. CodegenShop.SetCustomerForOrderMutationVariables
  1378. >(SET_CUSTOMER, {
  1379. input: {
  1380. emailAddress: GUEST_EMAIL_ADDRESS,
  1381. firstName: 'Guest',
  1382. lastName: 'Customer',
  1383. },
  1384. });
  1385. }
  1386. it('allows initial usage', async () => {
  1387. await shopClient.asAnonymousUser();
  1388. await createNewActiveOrder();
  1389. await addGuestCustomerToOrder();
  1390. const { applyCouponCode } = await shopClient.query<
  1391. CodegenShop.ApplyCouponCodeMutation,
  1392. CodegenShop.ApplyCouponCodeMutationVariables
  1393. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1394. orderResultGuard.assertSuccess(applyCouponCode);
  1395. expect(applyCouponCode.totalWithTax).toBe(0);
  1396. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1397. await proceedToArrangingPayment(shopClient);
  1398. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1399. orderGuard.assertSuccess(order);
  1400. expect(order.state).toBe('PaymentSettled');
  1401. expect(order.active).toBe(false);
  1402. orderCode = order.code;
  1403. });
  1404. it('adds Promotions to Order once payment arranged', async () => {
  1405. const { orderByCode } = await shopClient.query<
  1406. CodegenShop.GetOrderPromotionsByCodeQuery,
  1407. CodegenShop.GetOrderPromotionsByCodeQueryVariables
  1408. >(GET_ORDER_PROMOTIONS_BY_CODE, {
  1409. code: orderCode,
  1410. });
  1411. expect(orderByCode!.promotions.map(pick(['name']))).toEqual([
  1412. { name: 'Free with test coupon' },
  1413. ]);
  1414. });
  1415. it('returns error result when usage exceeds limit', async () => {
  1416. await shopClient.asAnonymousUser();
  1417. await createNewActiveOrder();
  1418. await addGuestCustomerToOrder();
  1419. const { applyCouponCode } = await shopClient.query<
  1420. CodegenShop.ApplyCouponCodeMutation,
  1421. CodegenShop.ApplyCouponCodeMutationVariables
  1422. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1423. orderResultGuard.assertErrorResult(applyCouponCode);
  1424. expect(applyCouponCode.message).toEqual(
  1425. 'Coupon code cannot be used more than once per customer',
  1426. );
  1427. });
  1428. it('removes couponCode from order when adding customer after code applied', async () => {
  1429. await shopClient.asAnonymousUser();
  1430. await createNewActiveOrder();
  1431. const { applyCouponCode } = await shopClient.query<
  1432. CodegenShop.ApplyCouponCodeMutation,
  1433. CodegenShop.ApplyCouponCodeMutationVariables
  1434. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1435. orderResultGuard.assertSuccess(applyCouponCode);
  1436. expect(applyCouponCode.totalWithTax).toBe(0);
  1437. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1438. await addGuestCustomerToOrder();
  1439. const { activeOrder } =
  1440. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1441. expect(activeOrder!.couponCodes).toEqual([]);
  1442. expect(activeOrder!.totalWithTax).toBe(6000);
  1443. });
  1444. it('does not remove valid couponCode when setting guest customer', async () => {
  1445. await shopClient.asAnonymousUser();
  1446. await createNewActiveOrder();
  1447. const { applyCouponCode } = await shopClient.query<
  1448. ApplyCouponCode.Mutation,
  1449. ApplyCouponCode.Variables
  1450. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1451. orderResultGuard.assertSuccess(applyCouponCode);
  1452. expect(applyCouponCode.totalWithTax).toBe(0);
  1453. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1454. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  1455. SET_CUSTOMER,
  1456. {
  1457. input: {
  1458. emailAddress: 'new-guest@test.com',
  1459. firstName: 'New Guest',
  1460. lastName: 'Customer',
  1461. },
  1462. },
  1463. );
  1464. const { activeOrder } = await shopClient.query<GetActiveOrder.Query>(GET_ACTIVE_ORDER);
  1465. expect(activeOrder.couponCodes).toEqual([TEST_COUPON_CODE]);
  1466. expect(applyCouponCode.totalWithTax).toBe(0);
  1467. });
  1468. });
  1469. describe('signed-in customer', () => {
  1470. function logInAsRegisteredCustomer() {
  1471. return shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1472. }
  1473. let orderId: string;
  1474. it('allows initial usage', async () => {
  1475. await logInAsRegisteredCustomer();
  1476. await createNewActiveOrder();
  1477. const { applyCouponCode } = await shopClient.query<
  1478. CodegenShop.ApplyCouponCodeMutation,
  1479. CodegenShop.ApplyCouponCodeMutationVariables
  1480. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1481. orderResultGuard.assertSuccess(applyCouponCode);
  1482. expect(applyCouponCode.totalWithTax).toBe(0);
  1483. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1484. await proceedToArrangingPayment(shopClient);
  1485. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1486. orderGuard.assertSuccess(order);
  1487. orderId = order.id;
  1488. expect(order.state).toBe('PaymentSettled');
  1489. expect(order.active).toBe(false);
  1490. });
  1491. it('returns error result when usage exceeds limit', async () => {
  1492. await logInAsRegisteredCustomer();
  1493. await createNewActiveOrder();
  1494. const { applyCouponCode } = await shopClient.query<
  1495. CodegenShop.ApplyCouponCodeMutation,
  1496. CodegenShop.ApplyCouponCodeMutationVariables
  1497. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1498. orderResultGuard.assertErrorResult(applyCouponCode);
  1499. expect(applyCouponCode.message).toEqual(
  1500. 'Coupon code cannot be used more than once per customer',
  1501. );
  1502. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_LIMIT_ERROR);
  1503. });
  1504. it('removes couponCode from order when logging in after code applied', async () => {
  1505. await shopClient.asAnonymousUser();
  1506. await createNewActiveOrder();
  1507. const { applyCouponCode } = await shopClient.query<
  1508. CodegenShop.ApplyCouponCodeMutation,
  1509. CodegenShop.ApplyCouponCodeMutationVariables
  1510. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1511. orderResultGuard.assertSuccess(applyCouponCode);
  1512. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1513. expect(applyCouponCode.totalWithTax).toBe(0);
  1514. await logInAsRegisteredCustomer();
  1515. const { activeOrder } =
  1516. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1517. expect(activeOrder!.totalWithTax).toBe(6000);
  1518. expect(activeOrder!.couponCodes).toEqual([]);
  1519. });
  1520. // https://github.com/vendure-ecommerce/vendure/issues/1466
  1521. it('cancelled orders do not count against usage limit', async () => {
  1522. const { cancelOrder } = await adminClient.query<
  1523. Codegen.CancelOrderMutation,
  1524. Codegen.CancelOrderMutationVariables
  1525. >(CANCEL_ORDER, {
  1526. input: {
  1527. orderId,
  1528. cancelShipping: true,
  1529. reason: 'request',
  1530. },
  1531. });
  1532. orderResultGuard.assertSuccess(cancelOrder);
  1533. expect(cancelOrder.state).toBe('Cancelled');
  1534. await logInAsRegisteredCustomer();
  1535. await createNewActiveOrder();
  1536. const { applyCouponCode } = await shopClient.query<
  1537. CodegenShop.ApplyCouponCodeMutation,
  1538. CodegenShop.ApplyCouponCodeMutationVariables
  1539. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1540. orderResultGuard.assertSuccess(applyCouponCode);
  1541. expect(applyCouponCode.totalWithTax).toBe(0);
  1542. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1543. });
  1544. });
  1545. });
  1546. describe('usage limit', () => {
  1547. const TEST_COUPON_CODE = 'TESTCOUPON';
  1548. const orderGuard: ErrorResultGuard<CodegenShop.TestOrderWithPaymentsFragment> =
  1549. createErrorResultGuard(input => !!input.lines);
  1550. let promoWithUsageLimit: Codegen.PromotionFragment;
  1551. async function createNewActiveOrder() {
  1552. const { addItemToOrder } = await shopClient.query<
  1553. CodegenShop.AddItemToOrderMutation,
  1554. CodegenShop.AddItemToOrderMutationVariables
  1555. >(ADD_ITEM_TO_ORDER, {
  1556. productVariantId: getVariantBySlug('item-5000').id,
  1557. quantity: 1,
  1558. });
  1559. return addItemToOrder;
  1560. }
  1561. describe('guest customer', () => {
  1562. const GUEST_EMAIL_ADDRESS = 'guest@test.com';
  1563. let orderCode: string;
  1564. beforeAll(async () => {
  1565. promoWithUsageLimit = await createPromotion({
  1566. enabled: true,
  1567. name: 'Free with test coupon',
  1568. couponCode: TEST_COUPON_CODE,
  1569. usageLimit: 1,
  1570. conditions: [],
  1571. actions: [freeOrderAction],
  1572. });
  1573. });
  1574. afterAll(async () => {
  1575. await deletePromotion(promoWithUsageLimit.id);
  1576. });
  1577. function addGuestCustomerToOrder() {
  1578. return shopClient.query<
  1579. CodegenShop.SetCustomerForOrderMutation,
  1580. CodegenShop.SetCustomerForOrderMutationVariables
  1581. >(SET_CUSTOMER, {
  1582. input: {
  1583. emailAddress: GUEST_EMAIL_ADDRESS,
  1584. firstName: 'Guest',
  1585. lastName: 'Customer',
  1586. },
  1587. });
  1588. }
  1589. it('allows initial usage', async () => {
  1590. await shopClient.asAnonymousUser();
  1591. await createNewActiveOrder();
  1592. await addGuestCustomerToOrder();
  1593. const { applyCouponCode } = await shopClient.query<
  1594. CodegenShop.ApplyCouponCodeMutation,
  1595. CodegenShop.ApplyCouponCodeMutationVariables
  1596. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1597. orderResultGuard.assertSuccess(applyCouponCode);
  1598. expect(applyCouponCode.totalWithTax).toBe(0);
  1599. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1600. await proceedToArrangingPayment(shopClient);
  1601. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1602. orderGuard.assertSuccess(order);
  1603. expect(order.state).toBe('PaymentSettled');
  1604. expect(order.active).toBe(false);
  1605. orderCode = order.code;
  1606. });
  1607. it('adds Promotions to Order once payment arranged', async () => {
  1608. const { orderByCode } = await shopClient.query<
  1609. CodegenShop.GetOrderPromotionsByCodeQuery,
  1610. CodegenShop.GetOrderPromotionsByCodeQueryVariables
  1611. >(GET_ORDER_PROMOTIONS_BY_CODE, {
  1612. code: orderCode,
  1613. });
  1614. expect(orderByCode!.promotions.map(pick(['name']))).toEqual([
  1615. { name: 'Free with test coupon' },
  1616. ]);
  1617. });
  1618. it('returns error result when usage exceeds limit', async () => {
  1619. await shopClient.asAnonymousUser();
  1620. await createNewActiveOrder();
  1621. await addGuestCustomerToOrder();
  1622. const { applyCouponCode } = await shopClient.query<
  1623. CodegenShop.ApplyCouponCodeMutation,
  1624. CodegenShop.ApplyCouponCodeMutationVariables
  1625. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1626. orderResultGuard.assertErrorResult(applyCouponCode);
  1627. expect(applyCouponCode.message).toEqual(
  1628. 'Coupon code cannot be used more than once per customer',
  1629. );
  1630. });
  1631. });
  1632. describe('signed-in customer', () => {
  1633. beforeAll(async () => {
  1634. promoWithUsageLimit = await createPromotion({
  1635. enabled: true,
  1636. name: 'Free with test coupon',
  1637. couponCode: TEST_COUPON_CODE,
  1638. usageLimit: 1,
  1639. conditions: [],
  1640. actions: [freeOrderAction],
  1641. });
  1642. });
  1643. afterAll(async () => {
  1644. await deletePromotion(promoWithUsageLimit.id);
  1645. });
  1646. function logInAsRegisteredCustomer() {
  1647. return shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  1648. }
  1649. let orderId: string;
  1650. it('allows initial usage', async () => {
  1651. await logInAsRegisteredCustomer();
  1652. await createNewActiveOrder();
  1653. const { applyCouponCode } = await shopClient.query<
  1654. CodegenShop.ApplyCouponCodeMutation,
  1655. CodegenShop.ApplyCouponCodeMutationVariables
  1656. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1657. orderResultGuard.assertSuccess(applyCouponCode);
  1658. expect(applyCouponCode.totalWithTax).toBe(0);
  1659. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1660. await proceedToArrangingPayment(shopClient);
  1661. const order = await addPaymentToOrder(shopClient, testSuccessfulPaymentMethod);
  1662. orderGuard.assertSuccess(order);
  1663. orderId = order.id;
  1664. expect(order.state).toBe('PaymentSettled');
  1665. expect(order.active).toBe(false);
  1666. });
  1667. it('returns error result when usage exceeds limit', async () => {
  1668. await logInAsRegisteredCustomer();
  1669. await createNewActiveOrder();
  1670. const { applyCouponCode } = await shopClient.query<
  1671. CodegenShop.ApplyCouponCodeMutation,
  1672. CodegenShop.ApplyCouponCodeMutationVariables
  1673. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1674. orderResultGuard.assertErrorResult(applyCouponCode);
  1675. expect(applyCouponCode.message).toEqual(
  1676. 'Coupon code cannot be used more than once per customer',
  1677. );
  1678. expect(applyCouponCode.errorCode).toBe(ErrorCode.COUPON_CODE_LIMIT_ERROR);
  1679. });
  1680. // https://github.com/vendure-ecommerce/vendure/issues/1466
  1681. it('cancelled orders do not count against usage limit', async () => {
  1682. const { cancelOrder } = await adminClient.query<
  1683. Codegen.CancelOrderMutation,
  1684. Codegen.CancelOrderMutationVariables
  1685. >(CANCEL_ORDER, {
  1686. input: {
  1687. orderId,
  1688. cancelShipping: true,
  1689. reason: 'request',
  1690. },
  1691. });
  1692. orderResultGuard.assertSuccess(cancelOrder);
  1693. expect(cancelOrder.state).toBe('Cancelled');
  1694. await logInAsRegisteredCustomer();
  1695. await createNewActiveOrder();
  1696. const { applyCouponCode } = await shopClient.query<
  1697. CodegenShop.ApplyCouponCodeMutation,
  1698. CodegenShop.ApplyCouponCodeMutationVariables
  1699. >(APPLY_COUPON_CODE, { couponCode: TEST_COUPON_CODE });
  1700. orderResultGuard.assertSuccess(applyCouponCode);
  1701. expect(applyCouponCode.totalWithTax).toBe(0);
  1702. expect(applyCouponCode.couponCodes).toEqual([TEST_COUPON_CODE]);
  1703. });
  1704. });
  1705. });
  1706. // https://github.com/vendure-ecommerce/vendure/issues/710
  1707. it('removes order-level discount made invalid by removing OrderLine', async () => {
  1708. const promotion = await createPromotion({
  1709. enabled: true,
  1710. name: 'Test Promo',
  1711. conditions: [minOrderAmountCondition(10000)],
  1712. actions: [
  1713. {
  1714. code: orderFixedDiscount.code,
  1715. arguments: [{ name: 'discount', value: '1000' }],
  1716. },
  1717. ],
  1718. });
  1719. await shopClient.asAnonymousUser();
  1720. await shopClient.query<
  1721. CodegenShop.AddItemToOrderMutation,
  1722. CodegenShop.AddItemToOrderMutationVariables
  1723. >(ADD_ITEM_TO_ORDER, {
  1724. productVariantId: getVariantBySlug('item-1000').id,
  1725. quantity: 8,
  1726. });
  1727. const { addItemToOrder } = await shopClient.query<
  1728. CodegenShop.AddItemToOrderMutation,
  1729. CodegenShop.AddItemToOrderMutationVariables
  1730. >(ADD_ITEM_TO_ORDER, {
  1731. productVariantId: getVariantBySlug('item-5000').id,
  1732. quantity: 1,
  1733. });
  1734. orderResultGuard.assertSuccess(addItemToOrder);
  1735. expect(addItemToOrder.discounts.length).toBe(1);
  1736. expect(addItemToOrder.discounts[0].description).toBe('Test Promo');
  1737. const { activeOrder: check1 } =
  1738. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1739. expect(check1!.discounts.length).toBe(1);
  1740. expect(check1!.discounts[0].description).toBe('Test Promo');
  1741. const { removeOrderLine } = await shopClient.query<
  1742. CodegenShop.RemoveItemFromOrderMutation,
  1743. CodegenShop.RemoveItemFromOrderMutationVariables
  1744. >(REMOVE_ITEM_FROM_ORDER, {
  1745. orderLineId: addItemToOrder.lines[1].id,
  1746. });
  1747. orderResultGuard.assertSuccess(removeOrderLine);
  1748. expect(removeOrderLine.discounts.length).toBe(0);
  1749. const { activeOrder: check2 } =
  1750. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1751. expect(check2!.discounts.length).toBe(0);
  1752. });
  1753. // https://github.com/vendure-ecommerce/vendure/issues/1492
  1754. it('correctly handles pro-ration of variants with 0 price', async () => {
  1755. const couponCode = '20%_off_order';
  1756. const promotion = await createPromotion({
  1757. enabled: true,
  1758. name: '20% discount on order',
  1759. couponCode,
  1760. conditions: [],
  1761. actions: [
  1762. {
  1763. code: orderPercentageDiscount.code,
  1764. arguments: [{ name: 'discount', value: '20' }],
  1765. },
  1766. ],
  1767. });
  1768. await shopClient.asAnonymousUser();
  1769. await shopClient.query<
  1770. CodegenShop.AddItemToOrderMutation,
  1771. CodegenShop.AddItemToOrderMutationVariables
  1772. >(ADD_ITEM_TO_ORDER, {
  1773. productVariantId: getVariantBySlug('item-100').id,
  1774. quantity: 1,
  1775. });
  1776. await shopClient.query<
  1777. CodegenShop.AddItemToOrderMutation,
  1778. CodegenShop.AddItemToOrderMutationVariables
  1779. >(ADD_ITEM_TO_ORDER, {
  1780. productVariantId: getVariantBySlug('item-0').id,
  1781. quantity: 1,
  1782. });
  1783. const { applyCouponCode } = await shopClient.query<
  1784. CodegenShop.ApplyCouponCodeMutation,
  1785. CodegenShop.ApplyCouponCodeMutationVariables
  1786. >(APPLY_COUPON_CODE, { couponCode });
  1787. orderResultGuard.assertSuccess(applyCouponCode);
  1788. expect(applyCouponCode.totalWithTax).toBe(96);
  1789. });
  1790. // https://github.com/vendure-ecommerce/vendure/issues/2385
  1791. describe('prevents negative line price', () => {
  1792. const TAX_INCLUDED_CHANNEL_TOKEN_2 = 'tax_included_channel_2';
  1793. const couponCode1 = '100%_off';
  1794. const couponCode2 = '100%_off';
  1795. let taxIncludedChannel: Codegen.ChannelFragment;
  1796. beforeAll(async () => {
  1797. // Create a channel where the prices include tax, so we can ensure
  1798. // that PromotionActions are working as expected when taxes are included
  1799. const { createChannel } = await adminClient.query<
  1800. Codegen.CreateChannelMutation,
  1801. Codegen.CreateChannelMutationVariables
  1802. >(CREATE_CHANNEL, {
  1803. input: {
  1804. code: 'tax-included-channel-2',
  1805. currencyCode: CurrencyCode.GBP,
  1806. pricesIncludeTax: true,
  1807. defaultTaxZoneId: 'T_1',
  1808. defaultShippingZoneId: 'T_1',
  1809. defaultLanguageCode: LanguageCode.en,
  1810. token: TAX_INCLUDED_CHANNEL_TOKEN_2,
  1811. },
  1812. });
  1813. taxIncludedChannel = createChannel as Codegen.ChannelFragment;
  1814. await adminClient.query<
  1815. Codegen.AssignProductsToChannelMutation,
  1816. Codegen.AssignProductsToChannelMutationVariables
  1817. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  1818. input: {
  1819. channelId: taxIncludedChannel.id,
  1820. priceFactor: 1,
  1821. productIds: products.map(p => p.id),
  1822. },
  1823. });
  1824. const item1000 = getVariantBySlug('item-1000')!;
  1825. const promo100 = await createPromotion({
  1826. enabled: true,
  1827. name: '100% discount ',
  1828. couponCode: couponCode1,
  1829. conditions: [],
  1830. actions: [
  1831. {
  1832. code: productsPercentageDiscount.code,
  1833. arguments: [
  1834. { name: 'discount', value: '100' },
  1835. {
  1836. name: 'productVariantIds',
  1837. value: `["${item1000.id}"]`,
  1838. },
  1839. ],
  1840. },
  1841. ],
  1842. });
  1843. const promo20 = await createPromotion({
  1844. enabled: true,
  1845. name: '20% discount ',
  1846. couponCode: couponCode2,
  1847. conditions: [],
  1848. actions: [
  1849. {
  1850. code: productsPercentageDiscount.code,
  1851. arguments: [
  1852. { name: 'discount', value: '20' },
  1853. {
  1854. name: 'productVariantIds',
  1855. value: `["${item1000.id}"]`,
  1856. },
  1857. ],
  1858. },
  1859. ],
  1860. });
  1861. await adminClient.query<
  1862. Codegen.AssignPromotionToChannelMutation,
  1863. Codegen.AssignPromotionToChannelMutationVariables
  1864. >(ASSIGN_PROMOTIONS_TO_CHANNEL, {
  1865. input: {
  1866. promotionIds: [promo100.id, promo20.id],
  1867. channelId: taxIncludedChannel.id,
  1868. },
  1869. });
  1870. });
  1871. it('prices exclude tax', async () => {
  1872. await shopClient.asAnonymousUser();
  1873. const item1000 = getVariantBySlug('item-1000')!;
  1874. await shopClient.query<
  1875. CodegenShop.ApplyCouponCodeMutation,
  1876. CodegenShop.ApplyCouponCodeMutationVariables
  1877. >(APPLY_COUPON_CODE, { couponCode: couponCode1 });
  1878. await shopClient.query<
  1879. CodegenShop.AddItemToOrderMutation,
  1880. CodegenShop.AddItemToOrderMutationVariables
  1881. >(ADD_ITEM_TO_ORDER, {
  1882. productVariantId: item1000.id,
  1883. quantity: 1,
  1884. });
  1885. const { activeOrder: check1 } =
  1886. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1887. expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
  1888. expect(check1!.totalWithTax).toBe(0);
  1889. await shopClient.query<
  1890. CodegenShop.ApplyCouponCodeMutation,
  1891. CodegenShop.ApplyCouponCodeMutationVariables
  1892. >(APPLY_COUPON_CODE, { couponCode: couponCode2 });
  1893. const { activeOrder: check2 } =
  1894. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1895. expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
  1896. expect(check2!.totalWithTax).toBe(0);
  1897. });
  1898. it('prices include tax', async () => {
  1899. shopClient.setChannelToken(TAX_INCLUDED_CHANNEL_TOKEN_2);
  1900. await shopClient.asAnonymousUser();
  1901. const item1000 = getVariantBySlug('item-1000')!;
  1902. await shopClient.query<
  1903. CodegenShop.ApplyCouponCodeMutation,
  1904. CodegenShop.ApplyCouponCodeMutationVariables
  1905. >(APPLY_COUPON_CODE, { couponCode: couponCode1 });
  1906. await shopClient.query<
  1907. CodegenShop.AddItemToOrderMutation,
  1908. CodegenShop.AddItemToOrderMutationVariables
  1909. >(ADD_ITEM_TO_ORDER, {
  1910. productVariantId: item1000.id,
  1911. quantity: 1,
  1912. });
  1913. const { activeOrder: check1 } =
  1914. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1915. expect(check1!.lines[0].discountedUnitPriceWithTax).toBe(0);
  1916. expect(check1!.totalWithTax).toBe(0);
  1917. await shopClient.query<
  1918. CodegenShop.ApplyCouponCodeMutation,
  1919. CodegenShop.ApplyCouponCodeMutationVariables
  1920. >(APPLY_COUPON_CODE, { couponCode: couponCode2 });
  1921. const { activeOrder: check2 } =
  1922. await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  1923. expect(check2!.lines[0].discountedUnitPriceWithTax).toBe(0);
  1924. expect(check2!.totalWithTax).toBe(0);
  1925. });
  1926. });
  1927. async function getProducts() {
  1928. const result = await adminClient.query<Codegen.GetProductsWithVariantPricesQuery>(
  1929. GET_PRODUCTS_WITH_VARIANT_PRICES,
  1930. {
  1931. options: {
  1932. take: 10,
  1933. skip: 0,
  1934. },
  1935. },
  1936. );
  1937. products = result.products.items;
  1938. }
  1939. async function createGlobalPromotions() {
  1940. const { facets } = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  1941. const saleFacetValue = facets.items[0].values[0];
  1942. await createPromotion({
  1943. enabled: true,
  1944. name: 'Promo not yet started',
  1945. startsAt: new Date(2199, 0, 0),
  1946. conditions: [minOrderAmountCondition(100)],
  1947. actions: [freeOrderAction],
  1948. });
  1949. const deletedPromotion = await createPromotion({
  1950. enabled: true,
  1951. name: 'Deleted promotion',
  1952. conditions: [minOrderAmountCondition(100)],
  1953. actions: [freeOrderAction],
  1954. });
  1955. await deletePromotion(deletedPromotion.id);
  1956. }
  1957. async function createPromotion(
  1958. input: Omit<Codegen.CreatePromotionInput, 'translations'> & { name: string },
  1959. ): Promise<Codegen.PromotionFragment> {
  1960. const correctedInput = {
  1961. ...input,
  1962. translations: [{ languageCode: LanguageCode.en, name: input.name }],
  1963. };
  1964. delete (correctedInput as any).name;
  1965. const result = await adminClient.query<
  1966. Codegen.CreatePromotionMutation,
  1967. Codegen.CreatePromotionMutationVariables
  1968. >(CREATE_PROMOTION, {
  1969. input: correctedInput,
  1970. });
  1971. return result.createPromotion as Codegen.PromotionFragment;
  1972. }
  1973. function getVariantBySlug(
  1974. slug: 'item-100' | 'item-1000' | 'item-5000' | 'item-sale-100' | 'item-sale-1000' | 'item-0',
  1975. ): Codegen.GetProductsWithVariantPricesQuery['products']['items'][number]['variants'][number] {
  1976. return products.find(p => p.slug === slug)!.variants[0];
  1977. }
  1978. async function deletePromotion(promotionId: string) {
  1979. await adminClient.query(gql`
  1980. mutation DeletePromotionAdHoc1 {
  1981. deletePromotion(id: "${promotionId}") {
  1982. result
  1983. }
  1984. }
  1985. `);
  1986. }
  1987. });