shipping-method.e2e-spec.ts 19 KB

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