shipping-method-eligibility.e2e-spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. import {
  2. defaultShippingCalculator,
  3. defaultShippingEligibilityChecker,
  4. manualFulfillmentHandler,
  5. ShippingCalculator,
  6. ShippingEligibilityChecker,
  7. } from '@vendure/core';
  8. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  9. import path from 'path';
  10. import { vi } from 'vitest';
  11. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  12. import { initialData } from '../../../e2e-common/e2e-initial-data';
  13. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  14. import * as Codegen from './graphql/generated-e2e-admin-types';
  15. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  16. import { ErrorCode, LanguageCode } from './graphql/generated-e2e-shop-types';
  17. import { CREATE_SHIPPING_METHOD } from './graphql/shared-definitions';
  18. import {
  19. ADD_ITEM_TO_ORDER,
  20. ADJUST_ITEM_QUANTITY,
  21. GET_ACTIVE_ORDER,
  22. GET_ELIGIBLE_SHIPPING_METHODS,
  23. REMOVE_ITEM_FROM_ORDER,
  24. SET_SHIPPING_ADDRESS,
  25. SET_SHIPPING_METHOD,
  26. } from './graphql/shop-definitions';
  27. const check1Spy = vi.fn();
  28. const checker1 = new ShippingEligibilityChecker({
  29. code: 'checker1',
  30. description: [],
  31. args: {},
  32. check: (ctx, order) => {
  33. check1Spy();
  34. return order.lines.length === 1;
  35. },
  36. });
  37. const check2Spy = vi.fn();
  38. const checker2 = new ShippingEligibilityChecker({
  39. code: 'checker2',
  40. description: [],
  41. args: {},
  42. check: (ctx, order) => {
  43. check2Spy();
  44. return order.lines.length > 1;
  45. },
  46. });
  47. const check3Spy = vi.fn();
  48. const checker3 = new ShippingEligibilityChecker({
  49. code: 'checker3',
  50. description: [],
  51. args: {},
  52. check: (ctx, order) => {
  53. check3Spy();
  54. return order.lines.length === 3;
  55. },
  56. shouldRunCheck: (ctx, order) => {
  57. return order.shippingAddress;
  58. },
  59. });
  60. const calculator = new ShippingCalculator({
  61. code: 'calculator',
  62. description: [],
  63. args: {},
  64. calculate: ctx => {
  65. return {
  66. price: 10,
  67. priceIncludesTax: false,
  68. taxRate: 20,
  69. };
  70. },
  71. });
  72. describe('ShippingMethod eligibility', () => {
  73. const { server, adminClient, shopClient } = createTestEnvironment({
  74. ...testConfig(),
  75. shippingOptions: {
  76. shippingEligibilityCheckers: [defaultShippingEligibilityChecker, checker1, checker2, checker3],
  77. shippingCalculators: [defaultShippingCalculator, calculator],
  78. },
  79. });
  80. const orderGuard: ErrorResultGuard<
  81. CodegenShop.UpdatedOrderFragment | CodegenShop.TestOrderFragmentFragment
  82. > = createErrorResultGuard(input => !!input.lines);
  83. let singleLineShippingMethod: Codegen.ShippingMethodFragment;
  84. let multiLineShippingMethod: Codegen.ShippingMethodFragment;
  85. let optimizedShippingMethod: Codegen.ShippingMethodFragment;
  86. beforeAll(async () => {
  87. await server.init({
  88. initialData: {
  89. ...initialData,
  90. shippingMethods: [],
  91. },
  92. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  93. customerCount: 1,
  94. });
  95. await adminClient.asSuperAdmin();
  96. const result1 = await adminClient.query<
  97. Codegen.CreateShippingMethodMutation,
  98. Codegen.CreateShippingMethodMutationVariables
  99. >(CREATE_SHIPPING_METHOD, {
  100. input: {
  101. code: 'single-line',
  102. fulfillmentHandler: manualFulfillmentHandler.code,
  103. checker: {
  104. code: checker1.code,
  105. arguments: [],
  106. },
  107. calculator: {
  108. code: calculator.code,
  109. arguments: [],
  110. },
  111. translations: [
  112. { languageCode: LanguageCode.en, name: 'For single-line orders', description: '' },
  113. ],
  114. },
  115. });
  116. singleLineShippingMethod = result1.createShippingMethod;
  117. const result2 = await adminClient.query<
  118. Codegen.CreateShippingMethodMutation,
  119. Codegen.CreateShippingMethodMutationVariables
  120. >(CREATE_SHIPPING_METHOD, {
  121. input: {
  122. code: 'multi-line',
  123. fulfillmentHandler: manualFulfillmentHandler.code,
  124. checker: {
  125. code: checker2.code,
  126. arguments: [],
  127. },
  128. calculator: {
  129. code: calculator.code,
  130. arguments: [],
  131. },
  132. translations: [
  133. { languageCode: LanguageCode.en, name: 'For multi-line orders', description: '' },
  134. ],
  135. },
  136. });
  137. multiLineShippingMethod = result2.createShippingMethod;
  138. const result3 = await adminClient.query<
  139. Codegen.CreateShippingMethodMutation,
  140. Codegen.CreateShippingMethodMutationVariables
  141. >(CREATE_SHIPPING_METHOD, {
  142. input: {
  143. code: 'optimized',
  144. fulfillmentHandler: manualFulfillmentHandler.code,
  145. checker: {
  146. code: checker3.code,
  147. arguments: [],
  148. },
  149. calculator: {
  150. code: calculator.code,
  151. arguments: [],
  152. },
  153. translations: [
  154. { languageCode: LanguageCode.en, name: 'Optimized with shouldRunCheck', description: '' },
  155. ],
  156. },
  157. });
  158. optimizedShippingMethod = result3.createShippingMethod;
  159. }, TEST_SETUP_TIMEOUT_MS);
  160. afterAll(async () => {
  161. await server.destroy();
  162. });
  163. describe('default behavior', () => {
  164. let order: CodegenShop.UpdatedOrderFragment;
  165. it('Does not run checkers before a ShippingMethod is assigned to Order', async () => {
  166. check1Spy.mockClear();
  167. check2Spy.mockClear();
  168. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  169. const { addItemToOrder } = await shopClient.query<
  170. CodegenShop.AddItemToOrderMutation,
  171. CodegenShop.AddItemToOrderMutationVariables
  172. >(ADD_ITEM_TO_ORDER, {
  173. quantity: 1,
  174. productVariantId: 'T_1',
  175. });
  176. orderGuard.assertSuccess(addItemToOrder);
  177. expect(check1Spy).not.toHaveBeenCalled();
  178. expect(check2Spy).not.toHaveBeenCalled();
  179. await shopClient.query<
  180. CodegenShop.AdjustItemQuantityMutation,
  181. CodegenShop.AdjustItemQuantityMutationVariables
  182. >(ADJUST_ITEM_QUANTITY, {
  183. quantity: 2,
  184. orderLineId: addItemToOrder.lines[0].id,
  185. });
  186. expect(check1Spy).not.toHaveBeenCalled();
  187. expect(check2Spy).not.toHaveBeenCalled();
  188. order = addItemToOrder;
  189. });
  190. it('Runs checkers when querying for eligible ShippingMethods', async () => {
  191. check1Spy.mockClear();
  192. check2Spy.mockClear();
  193. const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
  194. GET_ELIGIBLE_SHIPPING_METHODS,
  195. );
  196. expect(check1Spy).toHaveBeenCalledTimes(1);
  197. expect(check2Spy).toHaveBeenCalledTimes(1);
  198. });
  199. it('Runs checker of assigned method only', async () => {
  200. check1Spy.mockClear();
  201. check2Spy.mockClear();
  202. await shopClient.query<
  203. CodegenShop.SetShippingMethodMutation,
  204. CodegenShop.SetShippingMethodMutationVariables
  205. >(SET_SHIPPING_METHOD, {
  206. id: singleLineShippingMethod.id,
  207. });
  208. // A check is done when assigning the method to ensure it
  209. // is eligible, and again when calculating order adjustments
  210. expect(check1Spy).toHaveBeenCalledTimes(2);
  211. expect(check2Spy).not.toHaveBeenCalled();
  212. await shopClient.query<
  213. CodegenShop.AdjustItemQuantityMutation,
  214. CodegenShop.AdjustItemQuantityMutationVariables
  215. >(ADJUST_ITEM_QUANTITY, {
  216. quantity: 3,
  217. orderLineId: order.lines[0].id,
  218. });
  219. expect(check1Spy).toHaveBeenCalledTimes(3);
  220. expect(check2Spy).not.toHaveBeenCalled();
  221. await shopClient.query<
  222. CodegenShop.AdjustItemQuantityMutation,
  223. CodegenShop.AdjustItemQuantityMutationVariables
  224. >(ADJUST_ITEM_QUANTITY, {
  225. quantity: 4,
  226. orderLineId: order.lines[0].id,
  227. });
  228. expect(check1Spy).toHaveBeenCalledTimes(4);
  229. expect(check2Spy).not.toHaveBeenCalled();
  230. });
  231. it('Prevents ineligible method from being assigned', async () => {
  232. const { setOrderShippingMethod } = await shopClient.query<
  233. CodegenShop.SetShippingMethodMutation,
  234. CodegenShop.SetShippingMethodMutationVariables
  235. >(SET_SHIPPING_METHOD, {
  236. id: multiLineShippingMethod.id,
  237. });
  238. orderGuard.assertErrorResult(setOrderShippingMethod);
  239. expect(setOrderShippingMethod.errorCode).toBe(ErrorCode.INELIGIBLE_SHIPPING_METHOD_ERROR);
  240. expect(setOrderShippingMethod.message).toBe(
  241. 'This Order is not eligible for the selected ShippingMethod',
  242. );
  243. });
  244. it('Runs checks when assigned method becomes ineligible', async () => {
  245. check1Spy.mockClear();
  246. check2Spy.mockClear();
  247. // Adding a second OrderLine will make the singleLineShippingMethod
  248. // ineligible
  249. const { addItemToOrder } = await shopClient.query<
  250. CodegenShop.AddItemToOrderMutation,
  251. CodegenShop.AddItemToOrderMutationVariables
  252. >(ADD_ITEM_TO_ORDER, {
  253. quantity: 1,
  254. productVariantId: 'T_2',
  255. });
  256. orderGuard.assertSuccess(addItemToOrder);
  257. // Checked once to see if still eligible (no)
  258. expect(check1Spy).toHaveBeenCalledTimes(1);
  259. // Checked once when looking for a fallback
  260. expect(check2Spy).toHaveBeenCalledTimes(1);
  261. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  262. // multiLineShippingMethod assigned as a fallback
  263. expect(activeOrder?.shippingLines?.[0]?.shippingMethod?.id).toBe(multiLineShippingMethod.id);
  264. await shopClient.query<
  265. CodegenShop.AdjustItemQuantityMutation,
  266. CodegenShop.AdjustItemQuantityMutationVariables
  267. >(ADJUST_ITEM_QUANTITY, {
  268. quantity: 2,
  269. orderLineId: addItemToOrder.lines[1].id,
  270. });
  271. // No longer called as singleLineShippingMethod not assigned
  272. expect(check1Spy).toHaveBeenCalledTimes(1);
  273. // Called on changes since multiLineShippingMethod is assigned
  274. expect(check2Spy).toHaveBeenCalledTimes(2);
  275. // Remove the second OrderLine and make multiLineShippingMethod ineligible
  276. const { removeOrderLine } = await shopClient.query<
  277. CodegenShop.RemoveItemFromOrderMutation,
  278. CodegenShop.RemoveItemFromOrderMutationVariables
  279. >(REMOVE_ITEM_FROM_ORDER, {
  280. orderLineId: addItemToOrder.lines[1].id,
  281. });
  282. orderGuard.assertSuccess(removeOrderLine);
  283. // Called when looking for a fallback
  284. expect(check1Spy).toHaveBeenCalledTimes(2);
  285. // Called when checking if still eligibile (no)
  286. expect(check2Spy).toHaveBeenCalledTimes(3);
  287. // Falls back to the first eligible shipping method
  288. expect(removeOrderLine.shippingLines[0].shippingMethod?.id).toBe(singleLineShippingMethod.id);
  289. });
  290. });
  291. describe('optimization via shouldRunCheck function', () => {
  292. let order: CodegenShop.UpdatedOrderFragment;
  293. beforeAll(async () => {
  294. await shopClient.asAnonymousUser();
  295. await shopClient.query<
  296. CodegenShop.AddItemToOrderMutation,
  297. CodegenShop.AddItemToOrderMutationVariables
  298. >(ADD_ITEM_TO_ORDER, {
  299. quantity: 1,
  300. productVariantId: 'T_1',
  301. });
  302. await shopClient.query<
  303. CodegenShop.AddItemToOrderMutation,
  304. CodegenShop.AddItemToOrderMutationVariables
  305. >(ADD_ITEM_TO_ORDER, {
  306. quantity: 1,
  307. productVariantId: 'T_2',
  308. });
  309. const { addItemToOrder } = await shopClient.query<
  310. CodegenShop.AddItemToOrderMutation,
  311. CodegenShop.AddItemToOrderMutationVariables
  312. >(ADD_ITEM_TO_ORDER, {
  313. quantity: 1,
  314. productVariantId: 'T_3',
  315. });
  316. orderGuard.assertSuccess(addItemToOrder);
  317. order = addItemToOrder;
  318. await shopClient.query<
  319. CodegenShop.SetShippingAddressMutation,
  320. CodegenShop.SetShippingAddressMutationVariables
  321. >(SET_SHIPPING_ADDRESS, {
  322. input: {
  323. streetLine1: '42 Test Street',
  324. city: 'Doncaster',
  325. postalCode: 'DN1 4EE',
  326. countryCode: 'GB',
  327. },
  328. });
  329. });
  330. it('runs check on getEligibleShippingMethods', async () => {
  331. check3Spy.mockClear();
  332. const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
  333. GET_ELIGIBLE_SHIPPING_METHODS,
  334. );
  335. expect(check3Spy).toHaveBeenCalledTimes(1);
  336. });
  337. it('does not re-run check on setting shipping method', async () => {
  338. check3Spy.mockClear();
  339. await shopClient.query<
  340. CodegenShop.SetShippingMethodMutation,
  341. CodegenShop.SetShippingMethodMutationVariables
  342. >(SET_SHIPPING_METHOD, {
  343. id: optimizedShippingMethod.id,
  344. });
  345. expect(check3Spy).toHaveBeenCalledTimes(0);
  346. });
  347. it('does not re-run check when changing cart contents', async () => {
  348. check3Spy.mockClear();
  349. await shopClient.query<
  350. CodegenShop.AdjustItemQuantityMutation,
  351. CodegenShop.AdjustItemQuantityMutationVariables
  352. >(ADJUST_ITEM_QUANTITY, {
  353. quantity: 3,
  354. orderLineId: order.lines[0].id,
  355. });
  356. expect(check3Spy).toHaveBeenCalledTimes(0);
  357. });
  358. it('re-runs check when shouldRunCheck fn invalidates last check', async () => {
  359. check3Spy.mockClear();
  360. // Update the shipping address, causing the `shouldRunCheck` function
  361. // to trigger a check
  362. await shopClient.query<
  363. CodegenShop.SetShippingAddressMutation,
  364. CodegenShop.SetShippingAddressMutationVariables
  365. >(SET_SHIPPING_ADDRESS, {
  366. input: {
  367. streetLine1: '43 Test Street', // This line changed
  368. city: 'Doncaster',
  369. postalCode: 'DN1 4EE',
  370. countryCode: 'GB',
  371. },
  372. });
  373. await shopClient.query<
  374. CodegenShop.AdjustItemQuantityMutation,
  375. CodegenShop.AdjustItemQuantityMutationVariables
  376. >(ADJUST_ITEM_QUANTITY, {
  377. quantity: 2,
  378. orderLineId: order.lines[0].id,
  379. });
  380. expect(check3Spy).toHaveBeenCalledTimes(1);
  381. // Does not check a second time though, since the shipping address
  382. // is now the same as on the last check.
  383. await shopClient.query<
  384. CodegenShop.AdjustItemQuantityMutation,
  385. CodegenShop.AdjustItemQuantityMutationVariables
  386. >(ADJUST_ITEM_QUANTITY, {
  387. quantity: 3,
  388. orderLineId: order.lines[0].id,
  389. });
  390. expect(check3Spy).toHaveBeenCalledTimes(1);
  391. });
  392. });
  393. });