shipping-method.e2e-spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. defaultShippingCalculator,
  4. defaultShippingEligibilityChecker,
  5. ShippingCalculator,
  6. } from '@vendure/core';
  7. import { createTestEnvironment } from '@vendure/testing';
  8. import gql from 'graphql-tag';
  9. import path from 'path';
  10. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  11. import { initialData } from '../../../e2e-common/e2e-initial-data';
  12. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  13. import { manualFulfillmentHandler } from '../src/config/fulfillment/manual-fulfillment-handler';
  14. import { SHIPPING_METHOD_FRAGMENT } from './graphql/fragments';
  15. import * as Codegen from './graphql/generated-e2e-admin-types';
  16. import { DeletionResult, LanguageCode } from './graphql/generated-e2e-admin-types';
  17. import {
  18. CREATE_SHIPPING_METHOD,
  19. DELETE_SHIPPING_METHOD,
  20. GET_SHIPPING_METHOD_LIST,
  21. UPDATE_SHIPPING_METHOD,
  22. } from './graphql/shared-definitions';
  23. import { GET_ACTIVE_SHIPPING_METHODS } from './graphql/shop-definitions';
  24. const TEST_METADATA = {
  25. foo: 'bar',
  26. baz: [1, 2, 3],
  27. };
  28. const calculatorWithMetadata = new ShippingCalculator({
  29. code: 'calculator-with-metadata',
  30. description: [{ languageCode: LanguageCode.en, value: 'Has metadata' }],
  31. args: {},
  32. calculate: () => {
  33. return {
  34. price: 100,
  35. priceIncludesTax: true,
  36. taxRate: 0,
  37. metadata: TEST_METADATA,
  38. };
  39. },
  40. });
  41. describe('ShippingMethod resolver', () => {
  42. const { server, adminClient, shopClient } = createTestEnvironment({
  43. ...testConfig(),
  44. shippingOptions: {
  45. shippingEligibilityCheckers: [defaultShippingEligibilityChecker],
  46. shippingCalculators: [defaultShippingCalculator, calculatorWithMetadata],
  47. },
  48. });
  49. beforeAll(async () => {
  50. await server.init({
  51. initialData,
  52. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  53. customerCount: 1,
  54. });
  55. await adminClient.asSuperAdmin();
  56. }, TEST_SETUP_TIMEOUT_MS);
  57. afterAll(async () => {
  58. await server.destroy();
  59. });
  60. it('shippingEligibilityCheckers', async () => {
  61. const { shippingEligibilityCheckers } =
  62. await adminClient.query<Codegen.GetEligibilityCheckersQuery>(GET_ELIGIBILITY_CHECKERS);
  63. expect(shippingEligibilityCheckers).toEqual([
  64. {
  65. args: [
  66. {
  67. description: 'Order is eligible only if its total is greater or equal to this value',
  68. label: 'Minimum order value',
  69. name: 'orderMinimum',
  70. type: 'int',
  71. ui: {
  72. component: 'currency-form-input',
  73. },
  74. },
  75. ],
  76. code: 'default-shipping-eligibility-checker',
  77. description: 'Default Shipping Eligibility Checker',
  78. },
  79. ]);
  80. });
  81. it('shippingCalculators', async () => {
  82. const { shippingCalculators } = await adminClient.query<Codegen.GetCalculatorsQuery>(GET_CALCULATORS);
  83. expect(shippingCalculators).toEqual([
  84. {
  85. args: [
  86. {
  87. ui: {
  88. component: 'currency-form-input',
  89. },
  90. description: null,
  91. label: 'Shipping price',
  92. name: 'rate',
  93. type: 'int',
  94. },
  95. {
  96. label: 'Price includes tax',
  97. name: 'includesTax',
  98. type: 'string',
  99. description: null,
  100. ui: {
  101. component: 'select-form-input',
  102. options: [
  103. {
  104. label: [{ languageCode: LanguageCode.en, value: 'Includes tax' }],
  105. value: 'include',
  106. },
  107. {
  108. label: [{ languageCode: LanguageCode.en, value: 'Excludes tax' }],
  109. value: 'exclude',
  110. },
  111. {
  112. label: [
  113. { languageCode: LanguageCode.en, value: 'Auto (based on Channel)' },
  114. ],
  115. value: 'auto',
  116. },
  117. ],
  118. },
  119. },
  120. {
  121. ui: {
  122. component: 'number-form-input',
  123. suffix: '%',
  124. },
  125. description: null,
  126. label: 'Tax rate',
  127. name: 'taxRate',
  128. type: 'int',
  129. },
  130. ],
  131. code: 'default-shipping-calculator',
  132. description: 'Default Flat-Rate Shipping Calculator',
  133. },
  134. {
  135. args: [],
  136. code: 'calculator-with-metadata',
  137. description: 'Has metadata',
  138. },
  139. ]);
  140. });
  141. it('shippingMethods', async () => {
  142. const { shippingMethods } =
  143. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  144. expect(shippingMethods.totalItems).toEqual(3);
  145. expect(shippingMethods.items[0].code).toBe('standard-shipping');
  146. expect(shippingMethods.items[1].code).toBe('express-shipping');
  147. expect(shippingMethods.items[2].code).toBe('express-shipping-taxed');
  148. });
  149. it('shippingMethod', async () => {
  150. const { shippingMethod } = await adminClient.query<
  151. Codegen.GetShippingMethodQuery,
  152. Codegen.GetShippingMethodQueryVariables
  153. >(GET_SHIPPING_METHOD, {
  154. id: 'T_1',
  155. });
  156. expect(shippingMethod!.code).toBe('standard-shipping');
  157. });
  158. it('createShippingMethod', async () => {
  159. const { createShippingMethod } = await adminClient.query<
  160. Codegen.CreateShippingMethodMutation,
  161. Codegen.CreateShippingMethodMutationVariables
  162. >(CREATE_SHIPPING_METHOD, {
  163. input: {
  164. code: 'new-method',
  165. fulfillmentHandler: manualFulfillmentHandler.code,
  166. checker: {
  167. code: defaultShippingEligibilityChecker.code,
  168. arguments: [
  169. {
  170. name: 'orderMinimum',
  171. value: '0',
  172. },
  173. ],
  174. },
  175. calculator: {
  176. code: calculatorWithMetadata.code,
  177. arguments: [],
  178. },
  179. translations: [{ languageCode: LanguageCode.en, name: 'new method', description: '' }],
  180. },
  181. });
  182. expect(createShippingMethod).toEqual({
  183. id: 'T_4',
  184. code: 'new-method',
  185. name: 'new method',
  186. description: '',
  187. calculator: {
  188. code: 'calculator-with-metadata',
  189. args: [],
  190. },
  191. checker: {
  192. code: 'default-shipping-eligibility-checker',
  193. args: [
  194. {
  195. name: 'orderMinimum',
  196. value: '0',
  197. },
  198. ],
  199. },
  200. });
  201. });
  202. it('testShippingMethod', async () => {
  203. const { testShippingMethod } = await adminClient.query<
  204. Codegen.TestShippingMethodQuery,
  205. Codegen.TestShippingMethodQueryVariables
  206. >(TEST_SHIPPING_METHOD, {
  207. input: {
  208. calculator: {
  209. code: calculatorWithMetadata.code,
  210. arguments: [],
  211. },
  212. checker: {
  213. code: defaultShippingEligibilityChecker.code,
  214. arguments: [
  215. {
  216. name: 'orderMinimum',
  217. value: '0',
  218. },
  219. ],
  220. },
  221. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  222. shippingAddress: {
  223. streetLine1: '',
  224. countryCode: 'GB',
  225. },
  226. },
  227. });
  228. expect(testShippingMethod).toEqual({
  229. eligible: true,
  230. quote: {
  231. price: 100,
  232. priceWithTax: 100,
  233. metadata: TEST_METADATA,
  234. },
  235. });
  236. });
  237. it('testEligibleShippingMethods', async () => {
  238. const { testEligibleShippingMethods } = await adminClient.query<
  239. Codegen.TestEligibleMethodsQuery,
  240. Codegen.TestEligibleMethodsQueryVariables
  241. >(TEST_ELIGIBLE_SHIPPING_METHODS, {
  242. input: {
  243. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  244. shippingAddress: {
  245. streetLine1: '',
  246. countryCode: 'GB',
  247. },
  248. },
  249. });
  250. expect(testEligibleShippingMethods).toEqual([
  251. {
  252. id: 'T_4',
  253. name: 'new method',
  254. description: '',
  255. price: 100,
  256. priceWithTax: 100,
  257. metadata: TEST_METADATA,
  258. },
  259. {
  260. id: 'T_1',
  261. name: 'Standard Shipping',
  262. description: '',
  263. price: 500,
  264. priceWithTax: 500,
  265. metadata: null,
  266. },
  267. {
  268. id: 'T_2',
  269. name: 'Express Shipping',
  270. description: '',
  271. price: 1000,
  272. priceWithTax: 1000,
  273. metadata: null,
  274. },
  275. {
  276. id: 'T_3',
  277. name: 'Express Shipping (Taxed)',
  278. description: '',
  279. price: 1000,
  280. priceWithTax: 1200,
  281. metadata: null,
  282. },
  283. ]);
  284. });
  285. it('updateShippingMethod', async () => {
  286. const { updateShippingMethod } = await adminClient.query<
  287. Codegen.UpdateShippingMethodMutation,
  288. Codegen.UpdateShippingMethodMutationVariables
  289. >(UPDATE_SHIPPING_METHOD, {
  290. input: {
  291. id: 'T_4',
  292. translations: [{ languageCode: LanguageCode.en, name: 'changed method', description: '' }],
  293. },
  294. });
  295. expect(updateShippingMethod.name).toBe('changed method');
  296. });
  297. it('deleteShippingMethod', async () => {
  298. const listResult1 =
  299. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  300. expect(listResult1.shippingMethods.items.map(i => i.id)).toEqual(['T_1', 'T_2', 'T_3', 'T_4']);
  301. const { deleteShippingMethod } = await adminClient.query<
  302. Codegen.DeleteShippingMethodMutation,
  303. Codegen.DeleteShippingMethodMutationVariables
  304. >(DELETE_SHIPPING_METHOD, {
  305. id: 'T_4',
  306. });
  307. expect(deleteShippingMethod).toEqual({
  308. result: DeletionResult.DELETED,
  309. message: null,
  310. });
  311. const listResult2 =
  312. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  313. expect(listResult2.shippingMethods.items.map(i => i.id)).toEqual(['T_1', 'T_2', 'T_3']);
  314. });
  315. describe('argument ordering', () => {
  316. it('createShippingMethod corrects order of arguments', async () => {
  317. const { createShippingMethod } = await adminClient.query<
  318. Codegen.CreateShippingMethodMutation,
  319. Codegen.CreateShippingMethodMutationVariables
  320. >(CREATE_SHIPPING_METHOD, {
  321. input: {
  322. code: 'new-method',
  323. fulfillmentHandler: manualFulfillmentHandler.code,
  324. checker: {
  325. code: defaultShippingEligibilityChecker.code,
  326. arguments: [
  327. {
  328. name: 'orderMinimum',
  329. value: '0',
  330. },
  331. ],
  332. },
  333. calculator: {
  334. code: defaultShippingCalculator.code,
  335. arguments: [
  336. { name: 'rate', value: '500' },
  337. { name: 'taxRate', value: '20' },
  338. { name: 'includesTax', value: 'include' },
  339. ],
  340. },
  341. translations: [{ languageCode: LanguageCode.en, name: 'new method', description: '' }],
  342. },
  343. });
  344. expect(createShippingMethod.calculator).toEqual({
  345. code: defaultShippingCalculator.code,
  346. args: [
  347. { name: 'rate', value: '500' },
  348. { name: 'includesTax', value: 'include' },
  349. { name: 'taxRate', value: '20' },
  350. ],
  351. });
  352. });
  353. it('updateShippingMethod corrects order of arguments', async () => {
  354. const { updateShippingMethod } = await adminClient.query<
  355. Codegen.UpdateShippingMethodMutation,
  356. Codegen.UpdateShippingMethodMutationVariables
  357. >(UPDATE_SHIPPING_METHOD, {
  358. input: {
  359. id: 'T_5',
  360. translations: [],
  361. calculator: {
  362. code: defaultShippingCalculator.code,
  363. arguments: [
  364. { name: 'rate', value: '500' },
  365. { name: 'taxRate', value: '20' },
  366. { name: 'includesTax', value: 'include' },
  367. ],
  368. },
  369. },
  370. });
  371. expect(updateShippingMethod.calculator).toEqual({
  372. code: defaultShippingCalculator.code,
  373. args: [
  374. { name: 'rate', value: '500' },
  375. { name: 'includesTax', value: 'include' },
  376. { name: 'taxRate', value: '20' },
  377. ],
  378. });
  379. });
  380. it('get shippingMethod preserves correct ordering', async () => {
  381. const { shippingMethod } = await adminClient.query<
  382. Codegen.GetShippingMethodQuery,
  383. Codegen.GetShippingMethodQueryVariables
  384. >(GET_SHIPPING_METHOD, {
  385. id: 'T_5',
  386. });
  387. expect(shippingMethod?.calculator.args).toEqual([
  388. { name: 'rate', value: '500' },
  389. { name: 'includesTax', value: 'include' },
  390. { name: 'taxRate', value: '20' },
  391. ]);
  392. });
  393. it('testShippingMethod corrects order of arguments', async () => {
  394. const { testShippingMethod } = await adminClient.query<
  395. Codegen.TestShippingMethodQuery,
  396. Codegen.TestShippingMethodQueryVariables
  397. >(TEST_SHIPPING_METHOD, {
  398. input: {
  399. calculator: {
  400. code: defaultShippingCalculator.code,
  401. arguments: [
  402. { name: 'rate', value: '500' },
  403. { name: 'taxRate', value: '0' },
  404. { name: 'includesTax', value: 'include' },
  405. ],
  406. },
  407. checker: {
  408. code: defaultShippingEligibilityChecker.code,
  409. arguments: [
  410. {
  411. name: 'orderMinimum',
  412. value: '0',
  413. },
  414. ],
  415. },
  416. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  417. shippingAddress: {
  418. streetLine1: '',
  419. countryCode: 'GB',
  420. },
  421. },
  422. });
  423. expect(testShippingMethod).toEqual({
  424. eligible: true,
  425. quote: {
  426. metadata: null,
  427. price: 500,
  428. priceWithTax: 500,
  429. },
  430. });
  431. });
  432. });
  433. it('returns only active shipping methods', async () => {
  434. // Arrange: Delete all existing shipping methods using deleteShippingMethod
  435. const { shippingMethods } =
  436. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  437. for (const method of shippingMethods.items) {
  438. await adminClient.query<
  439. Codegen.DeleteShippingMethodMutation,
  440. Codegen.DeleteShippingMethodMutationVariables
  441. >(DELETE_SHIPPING_METHOD, {
  442. id: method.id,
  443. });
  444. }
  445. // Create a new active shipping method
  446. const { createShippingMethod } = await adminClient.query<
  447. Codegen.CreateShippingMethodMutation,
  448. Codegen.CreateShippingMethodMutationVariables
  449. >(CREATE_SHIPPING_METHOD, {
  450. input: {
  451. code: 'active-method',
  452. fulfillmentHandler: manualFulfillmentHandler.code,
  453. checker: {
  454. code: defaultShippingEligibilityChecker.code,
  455. arguments: [{ name: 'orderMinimum', value: '0' }],
  456. },
  457. calculator: {
  458. code: defaultShippingCalculator.code,
  459. arguments: [],
  460. },
  461. translations: [
  462. {
  463. languageCode: LanguageCode.en,
  464. name: 'Active Method',
  465. description: 'This is an active shipping method',
  466. },
  467. ],
  468. },
  469. });
  470. // Act: Query active shipping methods
  471. const { activeShippingMethods } = await shopClient.query(GET_ACTIVE_SHIPPING_METHODS);
  472. // Assert: Ensure only the new active method is returned
  473. expect(activeShippingMethods).toHaveLength(1);
  474. expect(activeShippingMethods[0].code).toBe('active-method');
  475. expect(activeShippingMethods[0].name).toBe('Active Method');
  476. expect(activeShippingMethods[0].description).toBe('This is an active shipping method');
  477. });
  478. });
  479. const GET_SHIPPING_METHOD = gql`
  480. query GetShippingMethod($id: ID!) {
  481. shippingMethod(id: $id) {
  482. ...ShippingMethod
  483. }
  484. }
  485. ${SHIPPING_METHOD_FRAGMENT}
  486. `;
  487. const GET_ELIGIBILITY_CHECKERS = gql`
  488. query GetEligibilityCheckers {
  489. shippingEligibilityCheckers {
  490. code
  491. description
  492. args {
  493. name
  494. type
  495. description
  496. label
  497. ui
  498. }
  499. }
  500. }
  501. `;
  502. const GET_CALCULATORS = gql`
  503. query GetCalculators {
  504. shippingCalculators {
  505. code
  506. description
  507. args {
  508. name
  509. type
  510. description
  511. label
  512. ui
  513. }
  514. }
  515. }
  516. `;
  517. const TEST_SHIPPING_METHOD = gql`
  518. query TestShippingMethod($input: TestShippingMethodInput!) {
  519. testShippingMethod(input: $input) {
  520. eligible
  521. quote {
  522. price
  523. priceWithTax
  524. metadata
  525. }
  526. }
  527. }
  528. `;
  529. export const TEST_ELIGIBLE_SHIPPING_METHODS = gql`
  530. query TestEligibleMethods($input: TestEligibleShippingMethodsInput!) {
  531. testEligibleShippingMethods(input: $input) {
  532. id
  533. name
  534. description
  535. price
  536. priceWithTax
  537. metadata
  538. }
  539. }
  540. `;