order-merge.e2e-spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. mergeConfig,
  4. MergedOrderLine,
  5. MergeOrdersStrategy,
  6. Order,
  7. OrderMergeStrategy,
  8. RequestContext,
  9. UseExistingStrategy,
  10. UseGuestIfExistingEmptyStrategy,
  11. UseGuestStrategy,
  12. } from '@vendure/core';
  13. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  14. import gql from 'graphql-tag';
  15. import path from 'path';
  16. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  17. import { initialData } from '../../../e2e-common/e2e-initial-data';
  18. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  19. import {
  20. AttemptLogin,
  21. AttemptLoginMutation,
  22. AttemptLoginMutationVariables,
  23. GetCustomerList,
  24. } from './graphql/generated-e2e-admin-types';
  25. import * as Codegen from './graphql/generated-e2e-admin-types';
  26. import {
  27. AddItemToOrder,
  28. AddItemToOrderMutation,
  29. AddItemToOrderMutation,
  30. AddItemToOrderMutationVariables,
  31. GetActiveOrderPaymentsQuery,
  32. GetNextOrderStatesQuery,
  33. TestOrderFragmentFragment,
  34. UpdatedOrderFragment,
  35. } from './graphql/generated-e2e-shop-types';
  36. import { ATTEMPT_LOGIN, GET_CUSTOMER_LIST } from './graphql/shared-definitions';
  37. import { GET_ACTIVE_ORDER_PAYMENTS, GET_NEXT_STATES, TEST_ORDER_FRAGMENT } from './graphql/shop-definitions';
  38. import { sortById } from './utils/test-order-utils';
  39. /**
  40. * Allows us to change the active OrderMergeStrategy per-test and delegates to the current
  41. * activeStrategy.
  42. */
  43. class DelegateMergeStrategy implements OrderMergeStrategy {
  44. static activeStrategy: OrderMergeStrategy = new MergeOrdersStrategy();
  45. merge(ctx: RequestContext, guestOrder: Order, existingOrder: Order): MergedOrderLine[] {
  46. return DelegateMergeStrategy.activeStrategy.merge(ctx, guestOrder, existingOrder);
  47. }
  48. }
  49. type AddItemToOrderWithCustomFields = AddItemToOrderMutationVariables & {
  50. customFields?: { inscription?: string };
  51. };
  52. describe('Order merging', () => {
  53. type OrderSuccessResult = UpdatedOrderFragment | TestOrderFragmentFragment;
  54. const orderResultGuard: ErrorResultGuard<OrderSuccessResult> = createErrorResultGuard(
  55. input => !!input.lines,
  56. );
  57. let customers: Codegen.GetCustomerListQuery['customers']['items'];
  58. const { server, shopClient, adminClient } = createTestEnvironment(
  59. mergeConfig(testConfig(), {
  60. orderOptions: {
  61. mergeStrategy: new DelegateMergeStrategy(),
  62. },
  63. customFields: {
  64. OrderLine: [{ name: 'inscription', type: 'string' }],
  65. },
  66. }),
  67. );
  68. beforeAll(async () => {
  69. await server.init({
  70. initialData,
  71. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  72. customerCount: 10,
  73. });
  74. await adminClient.asSuperAdmin();
  75. const result = await adminClient.query<Codegen.GetCustomerListQuery>(GET_CUSTOMER_LIST);
  76. customers = result.customers.items;
  77. }, TEST_SETUP_TIMEOUT_MS);
  78. afterAll(async () => {
  79. await server.destroy();
  80. });
  81. async function testMerge(options: {
  82. strategy: OrderMergeStrategy;
  83. customerEmailAddress: string;
  84. existingOrderLines: AddItemToOrderWithCustomFields[];
  85. guestOrderLines: AddItemToOrderWithCustomFields[];
  86. }): Promise<{ lines: any[] }> {
  87. const { strategy, customerEmailAddress, existingOrderLines, guestOrderLines } = options;
  88. DelegateMergeStrategy.activeStrategy = strategy;
  89. await shopClient.asUserWithCredentials(customerEmailAddress, 'test');
  90. for (const line of existingOrderLines) {
  91. await shopClient.query<AddItemToOrderMutation, AddItemToOrderWithCustomFields>(
  92. ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
  93. line,
  94. );
  95. }
  96. await shopClient.asAnonymousUser();
  97. for (const line of guestOrderLines) {
  98. await shopClient.query<AddItemToOrderMutation, AddItemToOrderWithCustomFields>(
  99. ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
  100. line,
  101. );
  102. }
  103. await shopClient.query<Codegen.AttemptLoginMutation, Codegen.AttemptLoginMutationVariables>(
  104. ATTEMPT_LOGIN,
  105. {
  106. username: customerEmailAddress,
  107. password: 'test',
  108. },
  109. );
  110. const { activeOrder } = await shopClient.query(GET_ACTIVE_ORDER_WITH_CUSTOM_FIELDS);
  111. return activeOrder;
  112. }
  113. it('MergeOrdersStrategy adds new line', async () => {
  114. const result = await testMerge({
  115. strategy: new MergeOrdersStrategy(),
  116. customerEmailAddress: customers[0].emailAddress,
  117. existingOrderLines: [{ productVariantId: 'T_1', quantity: 1 }],
  118. guestOrderLines: [{ productVariantId: 'T_2', quantity: 1 }],
  119. });
  120. expect(
  121. result.lines.map(line => ({ productVariantId: line.productVariant.id, quantity: line.quantity })),
  122. ).toEqual([
  123. { productVariantId: 'T_1', quantity: 1 },
  124. { productVariantId: 'T_2', quantity: 1 },
  125. ]);
  126. });
  127. it('MergeOrdersStrategy uses guest quantity', async () => {
  128. const result = await testMerge({
  129. strategy: new MergeOrdersStrategy(),
  130. customerEmailAddress: customers[1].emailAddress,
  131. existingOrderLines: [{ productVariantId: 'T_1', quantity: 1 }],
  132. guestOrderLines: [{ productVariantId: 'T_1', quantity: 3 }],
  133. });
  134. expect(
  135. result.lines.map(line => ({ productVariantId: line.productVariant.id, quantity: line.quantity })),
  136. ).toEqual([{ productVariantId: 'T_1', quantity: 3 }]);
  137. });
  138. it('MergeOrdersStrategy accounts for customFields', async () => {
  139. const result = await testMerge({
  140. strategy: new MergeOrdersStrategy(),
  141. customerEmailAddress: customers[2].emailAddress,
  142. existingOrderLines: [
  143. { productVariantId: 'T_1', quantity: 1, customFields: { inscription: 'foo' } },
  144. ],
  145. guestOrderLines: [{ productVariantId: 'T_1', quantity: 3, customFields: { inscription: 'bar' } }],
  146. });
  147. expect(
  148. result.lines.sort(sortById).map(line => ({
  149. productVariantId: line.productVariant.id,
  150. quantity: line.quantity,
  151. customFields: line.customFields,
  152. })),
  153. ).toEqual([
  154. { productVariantId: 'T_1', quantity: 1, customFields: { inscription: 'foo' } },
  155. { productVariantId: 'T_1', quantity: 3, customFields: { inscription: 'bar' } },
  156. ]);
  157. });
  158. it('UseGuestStrategy', async () => {
  159. const result = await testMerge({
  160. strategy: new UseGuestStrategy(),
  161. customerEmailAddress: customers[3].emailAddress,
  162. existingOrderLines: [
  163. { productVariantId: 'T_1', quantity: 1 },
  164. { productVariantId: 'T_3', quantity: 1 },
  165. ],
  166. guestOrderLines: [{ productVariantId: 'T_5', quantity: 3 }],
  167. });
  168. expect(
  169. result.lines.sort(sortById).map(line => ({
  170. productVariantId: line.productVariant.id,
  171. quantity: line.quantity,
  172. })),
  173. ).toEqual([{ productVariantId: 'T_5', quantity: 3 }]);
  174. });
  175. it('UseGuestIfExistingEmptyStrategy with empty existing', async () => {
  176. const result = await testMerge({
  177. strategy: new UseGuestIfExistingEmptyStrategy(),
  178. customerEmailAddress: customers[4].emailAddress,
  179. existingOrderLines: [],
  180. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  181. });
  182. expect(
  183. result.lines.sort(sortById).map(line => ({
  184. productVariantId: line.productVariant.id,
  185. quantity: line.quantity,
  186. })),
  187. ).toEqual([{ productVariantId: 'T_2', quantity: 3 }]);
  188. });
  189. it('UseGuestIfExistingEmptyStrategy with non-empty existing', async () => {
  190. const result = await testMerge({
  191. strategy: new UseGuestIfExistingEmptyStrategy(),
  192. customerEmailAddress: customers[5].emailAddress,
  193. existingOrderLines: [{ productVariantId: 'T_5', quantity: 5 }],
  194. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  195. });
  196. expect(
  197. result.lines.sort(sortById).map(line => ({
  198. productVariantId: line.productVariant.id,
  199. quantity: line.quantity,
  200. })),
  201. ).toEqual([{ productVariantId: 'T_5', quantity: 5 }]);
  202. });
  203. it('UseExistingStrategy', async () => {
  204. const result = await testMerge({
  205. strategy: new UseExistingStrategy(),
  206. customerEmailAddress: customers[6].emailAddress,
  207. existingOrderLines: [{ productVariantId: 'T_8', quantity: 1 }],
  208. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  209. });
  210. expect(
  211. result.lines.sort(sortById).map(line => ({
  212. productVariantId: line.productVariant.id,
  213. quantity: line.quantity,
  214. })),
  215. ).toEqual([{ productVariantId: 'T_8', quantity: 1 }]);
  216. });
  217. // https://github.com/vendure-ecommerce/vendure/issues/1454
  218. it('does not throw FK error when merging with a cart with an existing session', async () => {
  219. await shopClient.asUserWithCredentials(customers[7].emailAddress, 'test');
  220. // Create an Order linked with the current session
  221. const { nextOrderStates } = await shopClient.query<GetNextOrderStatesQuery>(GET_NEXT_STATES);
  222. // unset last auth token to simulate a guest user in a different browser
  223. shopClient.setAuthToken('');
  224. await shopClient.query<AddItemToOrderMutation, AddItemToOrderWithCustomFields>(
  225. ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS,
  226. { productVariantId: '1', quantity: 2 },
  227. );
  228. const { login } = await shopClient.query<AttemptLoginMutation, AttemptLoginMutationVariables>(
  229. ATTEMPT_LOGIN,
  230. { username: customers[7].emailAddress, password: 'test' },
  231. );
  232. expect(login.id).toBe(customers[7].user?.id);
  233. });
  234. });
  235. export const ADD_ITEM_TO_ORDER_WITH_CUSTOM_FIELDS = gql`
  236. mutation AddItemToOrder(
  237. $productVariantId: ID!
  238. $quantity: Int!
  239. $customFields: OrderLineCustomFieldsInput
  240. ) {
  241. addItemToOrder(
  242. productVariantId: $productVariantId
  243. quantity: $quantity
  244. customFields: $customFields
  245. ) {
  246. ... on Order {
  247. id
  248. }
  249. ... on ErrorResult {
  250. errorCode
  251. message
  252. }
  253. }
  254. }
  255. `;
  256. export const GET_ACTIVE_ORDER_WITH_CUSTOM_FIELDS = gql`
  257. query GetActiveOrder {
  258. activeOrder {
  259. ...TestOrderFragment
  260. ... on Order {
  261. lines {
  262. customFields {
  263. inscription
  264. }
  265. }
  266. }
  267. }
  268. }
  269. ${TEST_ORDER_FRAGMENT}
  270. `;