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

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