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

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