order-merge.e2e-spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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 path from 'node:path';
  15. import { afterAll, beforeAll, describe, expect, it } from 'vitest';
  16. import { initialData } from '../../../e2e-common/e2e-initial-data';
  17. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  18. import { currentUserFragment } from './graphql/fragments-admin';
  19. import { FragmentOf, graphql, ResultOf, VariablesOf } from './graphql/graphql-shop';
  20. import { attemptLoginDocument, getCustomerListDocument } from './graphql/shared-definitions';
  21. import {
  22. addItemToOrderCustomFieldsDocument,
  23. addItemToOrderDocument,
  24. getNextStatesDocument,
  25. } from './graphql/shop-definitions';
  26. import { sortById } from './utils/test-order-utils';
  27. /**
  28. * Allows us to change the active OrderMergeStrategy per-test and delegates to the current
  29. * activeStrategy.
  30. */
  31. class DelegateMergeStrategy implements OrderMergeStrategy {
  32. static activeStrategy: OrderMergeStrategy = new MergeOrdersStrategy();
  33. merge(ctx: RequestContext, guestOrder: Order, existingOrder: Order): MergedOrderLine[] {
  34. return DelegateMergeStrategy.activeStrategy.merge(ctx, guestOrder, existingOrder);
  35. }
  36. }
  37. type AddItemToOrderWithCustomFields = VariablesOf<typeof addItemToOrderDocument> & {
  38. customFields?: { inscription?: string };
  39. };
  40. const getActiveOrderWithCustomFieldsDocument = graphql(`
  41. query GetActiveOrderWithCustomFields {
  42. activeOrder {
  43. id
  44. code
  45. state
  46. active
  47. subTotal
  48. subTotalWithTax
  49. shipping
  50. shippingWithTax
  51. total
  52. totalWithTax
  53. currencyCode
  54. couponCodes
  55. discounts {
  56. adjustmentSource
  57. amount
  58. amountWithTax
  59. description
  60. type
  61. }
  62. lines {
  63. id
  64. quantity
  65. linePrice
  66. linePriceWithTax
  67. unitPrice
  68. unitPriceWithTax
  69. unitPriceChangeSinceAdded
  70. unitPriceWithTaxChangeSinceAdded
  71. discountedUnitPriceWithTax
  72. proratedUnitPriceWithTax
  73. productVariant {
  74. id
  75. }
  76. discounts {
  77. adjustmentSource
  78. amount
  79. amountWithTax
  80. description
  81. type
  82. }
  83. customFields {
  84. inscription
  85. }
  86. }
  87. shippingLines {
  88. priceWithTax
  89. shippingMethod {
  90. id
  91. code
  92. description
  93. }
  94. }
  95. }
  96. }
  97. `);
  98. describe('Order merging', () => {
  99. type LoginSuccessResult = FragmentOf<typeof currentUserFragment>;
  100. const loginResultGuard: ErrorResultGuard<LoginSuccessResult> = createErrorResultGuard(
  101. input => !!input && 'id' in input && !('errorCode' in input),
  102. );
  103. let customers: ResultOf<typeof getCustomerListDocument>['customers']['items'];
  104. const { server, shopClient, adminClient } = createTestEnvironment(
  105. mergeConfig(testConfig(), {
  106. orderOptions: {
  107. mergeStrategy: new DelegateMergeStrategy(),
  108. },
  109. customFields: {
  110. OrderLine: [{ name: 'inscription', type: 'string' }],
  111. },
  112. }),
  113. );
  114. beforeAll(async () => {
  115. await server.init({
  116. initialData,
  117. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  118. customerCount: 10,
  119. });
  120. await adminClient.asSuperAdmin();
  121. const result = await adminClient.query(getCustomerListDocument);
  122. customers = result.customers.items;
  123. }, TEST_SETUP_TIMEOUT_MS);
  124. afterAll(async () => {
  125. await server.destroy();
  126. });
  127. async function testMerge(options: {
  128. strategy: OrderMergeStrategy;
  129. customerEmailAddress: string;
  130. existingOrderLines: AddItemToOrderWithCustomFields[];
  131. guestOrderLines: AddItemToOrderWithCustomFields[];
  132. }): Promise<{ lines: any[] }> {
  133. const { strategy, customerEmailAddress, existingOrderLines, guestOrderLines } = options;
  134. DelegateMergeStrategy.activeStrategy = strategy;
  135. await shopClient.asUserWithCredentials(customerEmailAddress, 'test');
  136. for (const line of existingOrderLines) {
  137. await shopClient.query(
  138. addItemToOrderCustomFieldsDocument,
  139. line as VariablesOf<typeof addItemToOrderCustomFieldsDocument>,
  140. );
  141. }
  142. await shopClient.asAnonymousUser();
  143. for (const line of guestOrderLines) {
  144. await shopClient.query(
  145. addItemToOrderCustomFieldsDocument,
  146. line as VariablesOf<typeof addItemToOrderCustomFieldsDocument>,
  147. );
  148. }
  149. await shopClient.query(attemptLoginDocument, {
  150. username: customerEmailAddress,
  151. password: 'test',
  152. });
  153. const { activeOrder } = await shopClient.query(getActiveOrderWithCustomFieldsDocument);
  154. if (!activeOrder) {
  155. throw new Error('Active order not found');
  156. }
  157. return activeOrder;
  158. }
  159. it('MergeOrdersStrategy adds new line', async () => {
  160. const result = await testMerge({
  161. strategy: new MergeOrdersStrategy(),
  162. customerEmailAddress: customers[0].emailAddress,
  163. existingOrderLines: [{ productVariantId: 'T_1', quantity: 1 }],
  164. guestOrderLines: [{ productVariantId: 'T_2', quantity: 1 }],
  165. });
  166. expect(
  167. result.lines.map(line => ({ productVariantId: line.productVariant.id, quantity: line.quantity })),
  168. ).toEqual([
  169. { productVariantId: 'T_1', quantity: 1 },
  170. { productVariantId: 'T_2', quantity: 1 },
  171. ]);
  172. });
  173. it('MergeOrdersStrategy uses guest quantity', async () => {
  174. const result = await testMerge({
  175. strategy: new MergeOrdersStrategy(),
  176. customerEmailAddress: customers[1].emailAddress,
  177. existingOrderLines: [{ productVariantId: 'T_1', quantity: 1 }],
  178. guestOrderLines: [{ productVariantId: 'T_1', quantity: 3 }],
  179. });
  180. expect(
  181. result.lines.map(line => ({ productVariantId: line.productVariant.id, quantity: line.quantity })),
  182. ).toEqual([{ productVariantId: 'T_1', quantity: 3 }]);
  183. });
  184. it('MergeOrdersStrategy accounts for customFields', async () => {
  185. const result = await testMerge({
  186. strategy: new MergeOrdersStrategy(),
  187. customerEmailAddress: customers[2].emailAddress,
  188. existingOrderLines: [
  189. { productVariantId: 'T_1', quantity: 1, customFields: { inscription: 'foo' } },
  190. ],
  191. guestOrderLines: [{ productVariantId: 'T_1', quantity: 3, customFields: { inscription: 'bar' } }],
  192. });
  193. expect(
  194. result.lines.sort(sortById).map(line => ({
  195. productVariantId: line.productVariant.id,
  196. quantity: line.quantity,
  197. customFields: line.customFields,
  198. })),
  199. ).toEqual([
  200. { productVariantId: 'T_1', quantity: 1, customFields: { inscription: 'foo' } },
  201. { productVariantId: 'T_1', quantity: 3, customFields: { inscription: 'bar' } },
  202. ]);
  203. });
  204. it('UseGuestStrategy', async () => {
  205. const result = await testMerge({
  206. strategy: new UseGuestStrategy(),
  207. customerEmailAddress: customers[3].emailAddress,
  208. existingOrderLines: [
  209. { productVariantId: 'T_1', quantity: 1 },
  210. { productVariantId: 'T_3', quantity: 1 },
  211. ],
  212. guestOrderLines: [{ productVariantId: 'T_5', quantity: 3 }],
  213. });
  214. expect(
  215. result.lines.sort(sortById).map(line => ({
  216. productVariantId: line.productVariant.id,
  217. quantity: line.quantity,
  218. })),
  219. ).toEqual([{ productVariantId: 'T_5', quantity: 3 }]);
  220. });
  221. it('UseGuestStrategy with conflicting lines', async () => {
  222. const result = await testMerge({
  223. strategy: new UseGuestStrategy(),
  224. customerEmailAddress: customers[8].emailAddress,
  225. existingOrderLines: [
  226. { productVariantId: 'T_7', quantity: 1 },
  227. { productVariantId: 'T_8', quantity: 1 },
  228. ],
  229. guestOrderLines: [{ productVariantId: 'T_8', quantity: 3 }],
  230. });
  231. expect(
  232. (result?.lines || []).sort(sortById).map(line => ({
  233. productVariantId: line.productVariant.id,
  234. quantity: line.quantity,
  235. })),
  236. ).toEqual([{ productVariantId: 'T_8', quantity: 3 }]);
  237. });
  238. it('UseGuestIfExistingEmptyStrategy with empty existing', async () => {
  239. const result = await testMerge({
  240. strategy: new UseGuestIfExistingEmptyStrategy(),
  241. customerEmailAddress: customers[4].emailAddress,
  242. existingOrderLines: [],
  243. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  244. });
  245. expect(
  246. result.lines.sort(sortById).map(line => ({
  247. productVariantId: line.productVariant.id,
  248. quantity: line.quantity,
  249. })),
  250. ).toEqual([{ productVariantId: 'T_2', quantity: 3 }]);
  251. });
  252. it('UseGuestIfExistingEmptyStrategy with non-empty existing', async () => {
  253. const result = await testMerge({
  254. strategy: new UseGuestIfExistingEmptyStrategy(),
  255. customerEmailAddress: customers[5].emailAddress,
  256. existingOrderLines: [{ productVariantId: 'T_5', quantity: 5 }],
  257. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  258. });
  259. expect(
  260. result.lines.sort(sortById).map(line => ({
  261. productVariantId: line.productVariant.id,
  262. quantity: line.quantity,
  263. })),
  264. ).toEqual([{ productVariantId: 'T_5', quantity: 5 }]);
  265. });
  266. it('UseExistingStrategy', async () => {
  267. const result = await testMerge({
  268. strategy: new UseExistingStrategy(),
  269. customerEmailAddress: customers[6].emailAddress,
  270. existingOrderLines: [{ productVariantId: 'T_8', quantity: 1 }],
  271. guestOrderLines: [{ productVariantId: 'T_2', quantity: 3 }],
  272. });
  273. expect(
  274. result.lines.sort(sortById).map(line => ({
  275. productVariantId: line.productVariant.id,
  276. quantity: line.quantity,
  277. })),
  278. ).toEqual([{ productVariantId: 'T_8', quantity: 1 }]);
  279. });
  280. // https://github.com/vendure-ecommerce/vendure/issues/1454
  281. it('does not throw FK error when merging with a cart with an existing session', async () => {
  282. await shopClient.asUserWithCredentials(customers[7].emailAddress, 'test');
  283. // Create an Order linked with the current session
  284. await shopClient.query(getNextStatesDocument);
  285. // unset last auth token to simulate a guest user in a different browser
  286. shopClient.setAuthToken('');
  287. await shopClient.query(addItemToOrderDocument, { productVariantId: '1', quantity: 2 });
  288. const { login } = await shopClient.query(attemptLoginDocument, {
  289. username: customers[7].emailAddress,
  290. password: 'test',
  291. });
  292. loginResultGuard.assertSuccess(login);
  293. expect(login.id).toBe(customers[7].user?.id);
  294. });
  295. });