order-multiple-shipping.e2e-spec.ts 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { summate } from '@vendure/common/lib/shared-utils';
  3. import {
  4. mergeConfig,
  5. RequestContext,
  6. ShippingLineAssignmentStrategy,
  7. ShippingLine,
  8. Order,
  9. OrderLine,
  10. manualFulfillmentHandler,
  11. defaultShippingCalculator,
  12. defaultShippingEligibilityChecker,
  13. OrderService,
  14. RequestContextService,
  15. } from '@vendure/core';
  16. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  17. import path from 'path';
  18. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  19. import { initialData } from '../../../e2e-common/e2e-initial-data';
  20. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  21. import { hydratingShippingEligibilityChecker } from './fixtures/test-shipping-eligibility-checkers';
  22. import {
  23. CreateAddressInput,
  24. CreateShippingMethodDocument,
  25. LanguageCode,
  26. } from './graphql/generated-e2e-admin-types';
  27. import * as Codegen from './graphql/generated-e2e-admin-types';
  28. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  29. import { GET_COUNTRY_LIST, UPDATE_COUNTRY } from './graphql/shared-definitions';
  30. import {
  31. ADD_ITEM_TO_ORDER,
  32. GET_ACTIVE_ORDER,
  33. GET_AVAILABLE_COUNTRIES,
  34. GET_ELIGIBLE_SHIPPING_METHODS,
  35. REMOVE_ITEM_FROM_ORDER,
  36. SET_SHIPPING_ADDRESS,
  37. SET_SHIPPING_METHOD,
  38. } from './graphql/shop-definitions';
  39. declare module '@vendure/core/dist/entity/custom-entity-fields' {
  40. interface CustomShippingMethodFields {
  41. minPrice: number;
  42. maxPrice: number;
  43. }
  44. }
  45. class CustomShippingLineAssignmentStrategy implements ShippingLineAssignmentStrategy {
  46. assignShippingLineToOrderLines(
  47. ctx: RequestContext,
  48. shippingLine: ShippingLine,
  49. order: Order,
  50. ): OrderLine[] | Promise<OrderLine[]> {
  51. const { minPrice, maxPrice } = shippingLine.shippingMethod.customFields;
  52. return order.lines.filter(l => l.linePriceWithTax >= minPrice && l.linePriceWithTax <= maxPrice);
  53. }
  54. }
  55. describe('Shop orders', () => {
  56. const { server, adminClient, shopClient } = createTestEnvironment(
  57. mergeConfig(testConfig(), {
  58. customFields: {
  59. ShippingMethod: [
  60. { name: 'minPrice', type: 'int' },
  61. { name: 'maxPrice', type: 'int' },
  62. ],
  63. },
  64. shippingOptions: {
  65. shippingLineAssignmentStrategy: new CustomShippingLineAssignmentStrategy(),
  66. },
  67. }),
  68. );
  69. type OrderSuccessResult =
  70. | CodegenShop.UpdatedOrderFragment
  71. | CodegenShop.TestOrderFragmentFragment
  72. | CodegenShop.TestOrderWithPaymentsFragment
  73. | CodegenShop.ActiveOrderCustomerFragment;
  74. const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
  75. input => !!input.lines,
  76. );
  77. let lessThan100MethodId: string;
  78. let greaterThan100MethodId: string;
  79. let orderService: OrderService;
  80. beforeAll(async () => {
  81. await server.init({
  82. initialData,
  83. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  84. customerCount: 3,
  85. });
  86. await adminClient.asSuperAdmin();
  87. orderService = server.app.get(OrderService);
  88. }, TEST_SETUP_TIMEOUT_MS);
  89. afterAll(async () => {
  90. await server.destroy();
  91. });
  92. it('setup shipping methods', async () => {
  93. const result1 = await adminClient.query(CreateShippingMethodDocument, {
  94. input: {
  95. code: 'less-than-100',
  96. translations: [{ languageCode: LanguageCode.en, name: 'Less than 100', description: '' }],
  97. fulfillmentHandler: manualFulfillmentHandler.code,
  98. checker: {
  99. code: defaultShippingEligibilityChecker.code,
  100. arguments: [{ name: 'orderMinimum', value: '0' }],
  101. },
  102. calculator: {
  103. code: defaultShippingCalculator.code,
  104. arguments: [
  105. { name: 'rate', value: '1000' },
  106. { name: 'taxRate', value: '0' },
  107. { name: 'includesTax', value: 'auto' },
  108. ],
  109. },
  110. customFields: {
  111. minPrice: 0,
  112. maxPrice: 100_00,
  113. },
  114. },
  115. });
  116. const result2 = await adminClient.query(CreateShippingMethodDocument, {
  117. input: {
  118. code: 'greater-than-100',
  119. translations: [{ languageCode: LanguageCode.en, name: 'Greater than 200', description: '' }],
  120. fulfillmentHandler: manualFulfillmentHandler.code,
  121. checker: {
  122. code: defaultShippingEligibilityChecker.code,
  123. arguments: [{ name: 'orderMinimum', value: '0' }],
  124. },
  125. calculator: {
  126. code: defaultShippingCalculator.code,
  127. arguments: [
  128. { name: 'rate', value: '2000' },
  129. { name: 'taxRate', value: '0' },
  130. { name: 'includesTax', value: 'auto' },
  131. ],
  132. },
  133. customFields: {
  134. minPrice: 100_00,
  135. maxPrice: 500000_00,
  136. },
  137. },
  138. });
  139. expect(result1.createShippingMethod.id).toBe('T_3');
  140. expect(result2.createShippingMethod.id).toBe('T_4');
  141. lessThan100MethodId = result1.createShippingMethod.id;
  142. greaterThan100MethodId = result2.createShippingMethod.id;
  143. });
  144. it('assigns shipping methods to correct order lines', async () => {
  145. await shopClient.query<
  146. CodegenShop.AddItemToOrderMutation,
  147. CodegenShop.AddItemToOrderMutationVariables
  148. >(ADD_ITEM_TO_ORDER, {
  149. productVariantId: 'T_1',
  150. quantity: 1,
  151. });
  152. await shopClient.query<
  153. CodegenShop.AddItemToOrderMutation,
  154. CodegenShop.AddItemToOrderMutationVariables
  155. >(ADD_ITEM_TO_ORDER, {
  156. productVariantId: 'T_11',
  157. quantity: 1,
  158. });
  159. await shopClient.query<
  160. CodegenShop.SetShippingAddressMutation,
  161. CodegenShop.SetShippingAddressMutationVariables
  162. >(SET_SHIPPING_ADDRESS, {
  163. input: {
  164. streetLine1: '12 the street',
  165. postalCode: '123456',
  166. countryCode: 'US',
  167. },
  168. });
  169. const { eligibleShippingMethods } = await shopClient.query<CodegenShop.GetShippingMethodsQuery>(
  170. GET_ELIGIBLE_SHIPPING_METHODS,
  171. );
  172. expect(eligibleShippingMethods.map(m => m.id).includes(lessThan100MethodId)).toBe(true);
  173. expect(eligibleShippingMethods.map(m => m.id).includes(greaterThan100MethodId)).toBe(true);
  174. const { setOrderShippingMethod } = await shopClient.query<
  175. CodegenShop.SetShippingMethodMutation,
  176. CodegenShop.SetShippingMethodMutationVariables
  177. >(SET_SHIPPING_METHOD, {
  178. id: [lessThan100MethodId, greaterThan100MethodId],
  179. });
  180. orderResultGuard.assertSuccess(setOrderShippingMethod);
  181. const order = await getInternalOrder(setOrderShippingMethod.id);
  182. expect(order?.lines[0].shippingLine?.shippingMethod.code).toBe('greater-than-100');
  183. expect(order?.lines[1].shippingLine?.shippingMethod.code).toBe('less-than-100');
  184. expect(order?.shippingLines.length).toBe(2);
  185. });
  186. it('removes shipping methods that are no longer applicable', async () => {
  187. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  188. const { removeOrderLine } = await shopClient.query<
  189. CodegenShop.RemoveItemFromOrderMutation,
  190. CodegenShop.RemoveItemFromOrderMutationVariables
  191. >(REMOVE_ITEM_FROM_ORDER, {
  192. orderLineId: activeOrder!.lines[0].id,
  193. });
  194. orderResultGuard.assertSuccess(removeOrderLine);
  195. const order = await getInternalOrder(activeOrder!.id);
  196. expect(order?.lines.length).toBe(1);
  197. expect(order?.shippingLines.length).toBe(1);
  198. expect(order?.shippingLines[0].shippingMethod.code).toBe('less-than-100');
  199. const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  200. GET_ACTIVE_ORDER,
  201. );
  202. expect(activeOrder2?.shippingWithTax).toBe(summate(activeOrder2!.shippingLines, 'priceWithTax'));
  203. });
  204. it('removes remaining shipping method when removing all items', async () => {
  205. const { activeOrder } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(GET_ACTIVE_ORDER);
  206. const { removeOrderLine } = await shopClient.query<
  207. CodegenShop.RemoveItemFromOrderMutation,
  208. CodegenShop.RemoveItemFromOrderMutationVariables
  209. >(REMOVE_ITEM_FROM_ORDER, {
  210. orderLineId: activeOrder!.lines[0].id,
  211. });
  212. orderResultGuard.assertSuccess(removeOrderLine);
  213. const order = await getInternalOrder(activeOrder!.id);
  214. expect(order?.lines.length).toBe(0);
  215. const { activeOrder: activeOrder2 } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  216. GET_ACTIVE_ORDER,
  217. );
  218. expect(activeOrder2?.shippingWithTax).toBe(0);
  219. });
  220. async function getInternalOrder(externalOrderId: string): Promise<Order> {
  221. const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' });
  222. const internalOrderId = +externalOrderId.replace('T_', '');
  223. const order = await orderService.findOne(ctx, internalOrderId, [
  224. 'lines',
  225. 'lines.shippingLine',
  226. 'lines.shippingLine.shippingMethod',
  227. 'shippingLines',
  228. 'shippingLines.shippingMethod',
  229. ]);
  230. return order!;
  231. }
  232. });