order-promotion.e2e-spec.ts 99 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354
  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 { TEST_SETUP_TIMEOUT_MS, testConfig } 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 * as Codegen from './graphql/generated-e2e-admin-types';
  34. import { CurrencyCode, HistoryEntryType, LanguageCode } from './graphql/generated-e2e-admin-types';
  35. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  36. import { AdjustmentType, ErrorCode } 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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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/vendurehq/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. // https://github.com/vendurehq/vendure/issues/2052
  1928. describe('multi-channel usage', () => {
  1929. const SECOND_CHANNEL_TOKEN = 'second_channel_token';
  1930. const THIRD_CHANNEL_TOKEN = 'third_channel_token';
  1931. const promoCode = 'TEST_COMMON_CODE';
  1932. async function createChannelAndAssignProducts(code: string, token: string) {
  1933. const result = await adminClient.query<
  1934. Codegen.CreateChannelMutation,
  1935. Codegen.CreateChannelMutationVariables
  1936. >(CREATE_CHANNEL, {
  1937. input: {
  1938. code,
  1939. token,
  1940. defaultLanguageCode: LanguageCode.en,
  1941. currencyCode: CurrencyCode.GBP,
  1942. pricesIncludeTax: true,
  1943. defaultShippingZoneId: 'T_1',
  1944. defaultTaxZoneId: 'T_1',
  1945. },
  1946. });
  1947. await adminClient.query<
  1948. Codegen.AssignProductsToChannelMutation,
  1949. Codegen.AssignProductsToChannelMutationVariables
  1950. >(ASSIGN_PRODUCT_TO_CHANNEL, {
  1951. input: {
  1952. channelId: (result.createChannel as Codegen.ChannelFragment).id,
  1953. priceFactor: 1,
  1954. productIds: products.map(p => p.id),
  1955. },
  1956. });
  1957. return result.createChannel as Codegen.ChannelFragment;
  1958. }
  1959. async function addItemAndApplyPromoCode() {
  1960. await shopClient.asAnonymousUser();
  1961. await shopClient.query<
  1962. CodegenShop.AddItemToOrderMutation,
  1963. CodegenShop.AddItemToOrderMutationVariables
  1964. >(ADD_ITEM_TO_ORDER, {
  1965. productVariantId: getVariantBySlug('item-5000').id,
  1966. quantity: 1,
  1967. });
  1968. const { applyCouponCode } = await shopClient.query<
  1969. CodegenShop.ApplyCouponCodeMutation,
  1970. CodegenShop.ApplyCouponCodeMutationVariables
  1971. >(APPLY_COUPON_CODE, {
  1972. couponCode: promoCode,
  1973. });
  1974. orderResultGuard.assertSuccess(applyCouponCode);
  1975. return applyCouponCode;
  1976. }
  1977. beforeAll(async () => {
  1978. await createChannelAndAssignProducts('second-channel', SECOND_CHANNEL_TOKEN);
  1979. await createChannelAndAssignProducts('third-channel', THIRD_CHANNEL_TOKEN);
  1980. });
  1981. it('create promotion in second channel', async () => {
  1982. adminClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  1983. const result = await createPromotion({
  1984. enabled: true,
  1985. name: 'common-promotion-second-channel',
  1986. couponCode: promoCode,
  1987. actions: [
  1988. {
  1989. code: orderPercentageDiscount.code,
  1990. arguments: [{ name: 'discount', value: '20' }],
  1991. },
  1992. ],
  1993. conditions: [],
  1994. });
  1995. expect(result.name).toBe('common-promotion-second-channel');
  1996. });
  1997. it('create promotion in third channel', async () => {
  1998. adminClient.setChannelToken(THIRD_CHANNEL_TOKEN);
  1999. const result = await createPromotion({
  2000. enabled: true,
  2001. name: 'common-promotion-third-channel',
  2002. couponCode: promoCode,
  2003. actions: [
  2004. {
  2005. code: orderPercentageDiscount.code,
  2006. arguments: [{ name: 'discount', value: '20' }],
  2007. },
  2008. ],
  2009. conditions: [],
  2010. });
  2011. expect(result.name).toBe('common-promotion-third-channel');
  2012. });
  2013. it('applies promotion in second channel', async () => {
  2014. shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  2015. const result = await addItemAndApplyPromoCode();
  2016. expect(result.discounts.length).toBe(1);
  2017. expect(result.discounts[0].description).toBe('common-promotion-second-channel');
  2018. });
  2019. it('applies promotion in third channel', async () => {
  2020. shopClient.setChannelToken(THIRD_CHANNEL_TOKEN);
  2021. const result = await addItemAndApplyPromoCode();
  2022. expect(result.discounts.length).toBe(1);
  2023. expect(result.discounts[0].description).toBe('common-promotion-third-channel');
  2024. });
  2025. it('applies promotion from current channel, not default channel', async () => {
  2026. adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN);
  2027. const defaultChannelPromotion = await createPromotion({
  2028. enabled: true,
  2029. name: 'common-promotion-default-channel',
  2030. couponCode: promoCode,
  2031. actions: [
  2032. {
  2033. code: orderPercentageDiscount.code,
  2034. arguments: [{ name: 'discount', value: '20' }],
  2035. },
  2036. ],
  2037. conditions: [],
  2038. });
  2039. shopClient.setChannelToken(SECOND_CHANNEL_TOKEN);
  2040. const result = await addItemAndApplyPromoCode();
  2041. expect(result.discounts.length).toBe(1);
  2042. expect(result.discounts[0].description).toBe('common-promotion-second-channel');
  2043. });
  2044. });
  2045. async function getProducts() {
  2046. const result = await adminClient.query<Codegen.GetProductsWithVariantPricesQuery>(
  2047. GET_PRODUCTS_WITH_VARIANT_PRICES,
  2048. {
  2049. options: {
  2050. take: 10,
  2051. skip: 0,
  2052. },
  2053. },
  2054. );
  2055. products = result.products.items;
  2056. }
  2057. async function createGlobalPromotions() {
  2058. const { facets } = await adminClient.query<Codegen.GetFacetListQuery>(GET_FACET_LIST);
  2059. const saleFacetValue = facets.items[0].values[0];
  2060. await createPromotion({
  2061. enabled: true,
  2062. name: 'Promo not yet started',
  2063. startsAt: new Date(2199, 0, 0),
  2064. conditions: [minOrderAmountCondition(100)],
  2065. actions: [freeOrderAction],
  2066. });
  2067. const deletedPromotion = await createPromotion({
  2068. enabled: true,
  2069. name: 'Deleted promotion',
  2070. conditions: [minOrderAmountCondition(100)],
  2071. actions: [freeOrderAction],
  2072. });
  2073. await deletePromotion(deletedPromotion.id);
  2074. }
  2075. async function createPromotion(
  2076. input: Omit<Codegen.CreatePromotionInput, 'translations'> & { name: string },
  2077. ): Promise<Codegen.PromotionFragment> {
  2078. const correctedInput = {
  2079. ...input,
  2080. translations: [{ languageCode: LanguageCode.en, name: input.name }],
  2081. };
  2082. delete (correctedInput as any).name;
  2083. const result = await adminClient.query<
  2084. Codegen.CreatePromotionMutation,
  2085. Codegen.CreatePromotionMutationVariables
  2086. >(CREATE_PROMOTION, {
  2087. input: correctedInput,
  2088. });
  2089. return result.createPromotion as Codegen.PromotionFragment;
  2090. }
  2091. function getVariantBySlug(
  2092. slug: 'item-100' | 'item-1000' | 'item-5000' | 'item-sale-100' | 'item-sale-1000' | 'item-0',
  2093. ): Codegen.GetProductsWithVariantPricesQuery['products']['items'][number]['variants'][number] {
  2094. return products.find(p => p.slug === slug)!.variants[0];
  2095. }
  2096. async function deletePromotion(promotionId: string) {
  2097. await adminClient.query(gql`
  2098. mutation DeletePromotionAdHoc1 {
  2099. deletePromotion(id: "${promotionId}") {
  2100. result
  2101. }
  2102. }
  2103. `);
  2104. }
  2105. });