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

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