shipping-method.e2e-spec.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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 { TEST_SETUP_TIMEOUT_MS, testConfig } 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. min: 0,
  124. suffix: '%',
  125. },
  126. description: null,
  127. label: 'Tax rate',
  128. name: 'taxRate',
  129. type: 'float',
  130. },
  131. ],
  132. code: 'default-shipping-calculator',
  133. description: 'Default Flat-Rate Shipping Calculator',
  134. },
  135. {
  136. args: [],
  137. code: 'calculator-with-metadata',
  138. description: 'Has metadata',
  139. },
  140. ]);
  141. });
  142. it('shippingMethods', async () => {
  143. const { shippingMethods } =
  144. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  145. expect(shippingMethods.totalItems).toEqual(3);
  146. expect(shippingMethods.items[0].code).toBe('standard-shipping');
  147. expect(shippingMethods.items[1].code).toBe('express-shipping');
  148. expect(shippingMethods.items[2].code).toBe('express-shipping-taxed');
  149. });
  150. it('shippingMethod', async () => {
  151. const { shippingMethod } = await adminClient.query<
  152. Codegen.GetShippingMethodQuery,
  153. Codegen.GetShippingMethodQueryVariables
  154. >(GET_SHIPPING_METHOD, {
  155. id: 'T_1',
  156. });
  157. expect(shippingMethod!.code).toBe('standard-shipping');
  158. });
  159. it('createShippingMethod', async () => {
  160. const { createShippingMethod } = await adminClient.query<
  161. Codegen.CreateShippingMethodMutation,
  162. Codegen.CreateShippingMethodMutationVariables
  163. >(CREATE_SHIPPING_METHOD, {
  164. input: {
  165. code: 'new-method',
  166. fulfillmentHandler: manualFulfillmentHandler.code,
  167. checker: {
  168. code: defaultShippingEligibilityChecker.code,
  169. arguments: [
  170. {
  171. name: 'orderMinimum',
  172. value: '0',
  173. },
  174. ],
  175. },
  176. calculator: {
  177. code: calculatorWithMetadata.code,
  178. arguments: [],
  179. },
  180. translations: [{ languageCode: LanguageCode.en, name: 'new method', description: '' }],
  181. },
  182. });
  183. expect(createShippingMethod).toEqual({
  184. id: 'T_4',
  185. code: 'new-method',
  186. name: 'new method',
  187. description: '',
  188. calculator: {
  189. code: 'calculator-with-metadata',
  190. args: [],
  191. },
  192. checker: {
  193. code: 'default-shipping-eligibility-checker',
  194. args: [
  195. {
  196. name: 'orderMinimum',
  197. value: '0',
  198. },
  199. ],
  200. },
  201. });
  202. });
  203. it('testShippingMethod', async () => {
  204. const { testShippingMethod } = await adminClient.query<
  205. Codegen.TestShippingMethodQuery,
  206. Codegen.TestShippingMethodQueryVariables
  207. >(TEST_SHIPPING_METHOD, {
  208. input: {
  209. calculator: {
  210. code: calculatorWithMetadata.code,
  211. arguments: [],
  212. },
  213. checker: {
  214. code: defaultShippingEligibilityChecker.code,
  215. arguments: [
  216. {
  217. name: 'orderMinimum',
  218. value: '0',
  219. },
  220. ],
  221. },
  222. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  223. shippingAddress: {
  224. streetLine1: '',
  225. countryCode: 'GB',
  226. },
  227. },
  228. });
  229. expect(testShippingMethod).toEqual({
  230. eligible: true,
  231. quote: {
  232. price: 100,
  233. priceWithTax: 100,
  234. metadata: TEST_METADATA,
  235. },
  236. });
  237. });
  238. it('testEligibleShippingMethods', async () => {
  239. const { testEligibleShippingMethods } = await adminClient.query<
  240. Codegen.TestEligibleMethodsQuery,
  241. Codegen.TestEligibleMethodsQueryVariables
  242. >(TEST_ELIGIBLE_SHIPPING_METHODS, {
  243. input: {
  244. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  245. shippingAddress: {
  246. streetLine1: '',
  247. countryCode: 'GB',
  248. },
  249. },
  250. });
  251. expect(testEligibleShippingMethods).toEqual([
  252. {
  253. id: 'T_4',
  254. name: 'new method',
  255. description: '',
  256. price: 100,
  257. priceWithTax: 100,
  258. metadata: TEST_METADATA,
  259. },
  260. {
  261. id: 'T_1',
  262. name: 'Standard Shipping',
  263. description: '',
  264. price: 500,
  265. priceWithTax: 500,
  266. metadata: null,
  267. },
  268. {
  269. id: 'T_2',
  270. name: 'Express Shipping',
  271. description: '',
  272. price: 1000,
  273. priceWithTax: 1000,
  274. metadata: null,
  275. },
  276. {
  277. id: 'T_3',
  278. name: 'Express Shipping (Taxed)',
  279. description: '',
  280. price: 1000,
  281. priceWithTax: 1200,
  282. metadata: null,
  283. },
  284. ]);
  285. });
  286. it('updateShippingMethod', async () => {
  287. const { updateShippingMethod } = await adminClient.query<
  288. Codegen.UpdateShippingMethodMutation,
  289. Codegen.UpdateShippingMethodMutationVariables
  290. >(UPDATE_SHIPPING_METHOD, {
  291. input: {
  292. id: 'T_4',
  293. translations: [{ languageCode: LanguageCode.en, name: 'changed method', description: '' }],
  294. },
  295. });
  296. expect(updateShippingMethod.name).toBe('changed method');
  297. });
  298. it('deleteShippingMethod', async () => {
  299. const listResult1 =
  300. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  301. expect(listResult1.shippingMethods.items.map(i => i.id)).toEqual(['T_1', 'T_2', 'T_3', 'T_4']);
  302. const { deleteShippingMethod } = await adminClient.query<
  303. Codegen.DeleteShippingMethodMutation,
  304. Codegen.DeleteShippingMethodMutationVariables
  305. >(DELETE_SHIPPING_METHOD, {
  306. id: 'T_4',
  307. });
  308. expect(deleteShippingMethod).toEqual({
  309. result: DeletionResult.DELETED,
  310. message: null,
  311. });
  312. const listResult2 =
  313. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  314. expect(listResult2.shippingMethods.items.map(i => i.id)).toEqual(['T_1', 'T_2', 'T_3']);
  315. });
  316. describe('argument ordering', () => {
  317. it('createShippingMethod corrects order of arguments', async () => {
  318. const { createShippingMethod } = await adminClient.query<
  319. Codegen.CreateShippingMethodMutation,
  320. Codegen.CreateShippingMethodMutationVariables
  321. >(CREATE_SHIPPING_METHOD, {
  322. input: {
  323. code: 'new-method',
  324. fulfillmentHandler: manualFulfillmentHandler.code,
  325. checker: {
  326. code: defaultShippingEligibilityChecker.code,
  327. arguments: [
  328. {
  329. name: 'orderMinimum',
  330. value: '0',
  331. },
  332. ],
  333. },
  334. calculator: {
  335. code: defaultShippingCalculator.code,
  336. arguments: [
  337. { name: 'rate', value: '500' },
  338. { name: 'taxRate', value: '20' },
  339. { name: 'includesTax', value: 'include' },
  340. ],
  341. },
  342. translations: [{ languageCode: LanguageCode.en, name: 'new method', description: '' }],
  343. },
  344. });
  345. expect(createShippingMethod.calculator).toEqual({
  346. code: defaultShippingCalculator.code,
  347. args: [
  348. { name: 'rate', value: '500' },
  349. { name: 'includesTax', value: 'include' },
  350. { name: 'taxRate', value: '20' },
  351. ],
  352. });
  353. });
  354. it('updateShippingMethod corrects order of arguments', async () => {
  355. const { updateShippingMethod } = await adminClient.query<
  356. Codegen.UpdateShippingMethodMutation,
  357. Codegen.UpdateShippingMethodMutationVariables
  358. >(UPDATE_SHIPPING_METHOD, {
  359. input: {
  360. id: 'T_5',
  361. translations: [],
  362. calculator: {
  363. code: defaultShippingCalculator.code,
  364. arguments: [
  365. { name: 'rate', value: '500' },
  366. { name: 'taxRate', value: '20' },
  367. { name: 'includesTax', value: 'include' },
  368. ],
  369. },
  370. },
  371. });
  372. expect(updateShippingMethod.calculator).toEqual({
  373. code: defaultShippingCalculator.code,
  374. args: [
  375. { name: 'rate', value: '500' },
  376. { name: 'includesTax', value: 'include' },
  377. { name: 'taxRate', value: '20' },
  378. ],
  379. });
  380. });
  381. it('get shippingMethod preserves correct ordering', async () => {
  382. const { shippingMethod } = await adminClient.query<
  383. Codegen.GetShippingMethodQuery,
  384. Codegen.GetShippingMethodQueryVariables
  385. >(GET_SHIPPING_METHOD, {
  386. id: 'T_5',
  387. });
  388. expect(shippingMethod?.calculator.args).toEqual([
  389. { name: 'rate', value: '500' },
  390. { name: 'includesTax', value: 'include' },
  391. { name: 'taxRate', value: '20' },
  392. ]);
  393. });
  394. it('testShippingMethod corrects order of arguments', async () => {
  395. const { testShippingMethod } = await adminClient.query<
  396. Codegen.TestShippingMethodQuery,
  397. Codegen.TestShippingMethodQueryVariables
  398. >(TEST_SHIPPING_METHOD, {
  399. input: {
  400. calculator: {
  401. code: defaultShippingCalculator.code,
  402. arguments: [
  403. { name: 'rate', value: '500' },
  404. { name: 'taxRate', value: '0' },
  405. { name: 'includesTax', value: 'include' },
  406. ],
  407. },
  408. checker: {
  409. code: defaultShippingEligibilityChecker.code,
  410. arguments: [
  411. {
  412. name: 'orderMinimum',
  413. value: '0',
  414. },
  415. ],
  416. },
  417. lines: [{ productVariantId: 'T_1', quantity: 1 }],
  418. shippingAddress: {
  419. streetLine1: '',
  420. countryCode: 'GB',
  421. },
  422. },
  423. });
  424. expect(testShippingMethod).toEqual({
  425. eligible: true,
  426. quote: {
  427. metadata: null,
  428. price: 500,
  429. priceWithTax: 500,
  430. },
  431. });
  432. });
  433. });
  434. it('returns only active shipping methods', async () => {
  435. // Arrange: Delete all existing shipping methods using deleteShippingMethod
  436. const { shippingMethods } =
  437. await adminClient.query<Codegen.GetShippingMethodListQuery>(GET_SHIPPING_METHOD_LIST);
  438. for (const method of shippingMethods.items) {
  439. await adminClient.query<
  440. Codegen.DeleteShippingMethodMutation,
  441. Codegen.DeleteShippingMethodMutationVariables
  442. >(DELETE_SHIPPING_METHOD, {
  443. id: method.id,
  444. });
  445. }
  446. // Create a new active shipping method
  447. const { createShippingMethod } = await adminClient.query<
  448. Codegen.CreateShippingMethodMutation,
  449. Codegen.CreateShippingMethodMutationVariables
  450. >(CREATE_SHIPPING_METHOD, {
  451. input: {
  452. code: 'active-method',
  453. fulfillmentHandler: manualFulfillmentHandler.code,
  454. checker: {
  455. code: defaultShippingEligibilityChecker.code,
  456. arguments: [{ name: 'orderMinimum', value: '0' }],
  457. },
  458. calculator: {
  459. code: defaultShippingCalculator.code,
  460. arguments: [],
  461. },
  462. translations: [
  463. {
  464. languageCode: LanguageCode.en,
  465. name: 'Active Method',
  466. description: 'This is an active shipping method',
  467. },
  468. ],
  469. },
  470. });
  471. // Act: Query active shipping methods
  472. const { activeShippingMethods } = await shopClient.query(GET_ACTIVE_SHIPPING_METHODS);
  473. // Assert: Ensure only the new active method is returned
  474. expect(activeShippingMethods).toHaveLength(1);
  475. expect(activeShippingMethods[0].code).toBe('active-method');
  476. expect(activeShippingMethods[0].name).toBe('Active Method');
  477. expect(activeShippingMethods[0].description).toBe('This is an active shipping method');
  478. });
  479. });
  480. const GET_SHIPPING_METHOD = gql`
  481. query GetShippingMethod($id: ID!) {
  482. shippingMethod(id: $id) {
  483. ...ShippingMethod
  484. }
  485. }
  486. ${SHIPPING_METHOD_FRAGMENT}
  487. `;
  488. const GET_ELIGIBILITY_CHECKERS = gql`
  489. query GetEligibilityCheckers {
  490. shippingEligibilityCheckers {
  491. code
  492. description
  493. args {
  494. name
  495. type
  496. description
  497. label
  498. ui
  499. }
  500. }
  501. }
  502. `;
  503. const GET_CALCULATORS = gql`
  504. query GetCalculators {
  505. shippingCalculators {
  506. code
  507. description
  508. args {
  509. name
  510. type
  511. description
  512. label
  513. ui
  514. }
  515. }
  516. }
  517. `;
  518. const TEST_SHIPPING_METHOD = gql`
  519. query TestShippingMethod($input: TestShippingMethodInput!) {
  520. testShippingMethod(input: $input) {
  521. eligible
  522. quote {
  523. price
  524. priceWithTax
  525. metadata
  526. }
  527. }
  528. }
  529. `;
  530. export const TEST_ELIGIBLE_SHIPPING_METHODS = gql`
  531. query TestEligibleMethods($input: TestEligibleShippingMethodsInput!) {
  532. testEligibleShippingMethods(input: $input) {
  533. id
  534. name
  535. description
  536. price
  537. priceWithTax
  538. metadata
  539. }
  540. }
  541. `;