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

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