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

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