order-process.e2e-spec.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import {
  3. CustomOrderProcess,
  4. defaultOrderProcess,
  5. mergeConfig,
  6. OrderState,
  7. TransactionalConnection,
  8. } from '@vendure/core';
  9. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  10. import path from 'path';
  11. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  12. import { initialData } from '../../../e2e-common/e2e-initial-data';
  13. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  14. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  15. import * as Codegen from './graphql/generated-e2e-admin-types';
  16. import { OrderFragment } from './graphql/generated-e2e-admin-types';
  17. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  18. import {
  19. AddPaymentToOrderMutation,
  20. AddPaymentToOrderMutationVariables,
  21. ErrorCode,
  22. TestOrderFragmentFragment,
  23. TransitionToStateMutation,
  24. TransitionToStateMutationVariables,
  25. } from './graphql/generated-e2e-shop-types';
  26. import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
  27. import {
  28. ADD_ITEM_TO_ORDER,
  29. ADD_PAYMENT,
  30. GET_NEXT_STATES,
  31. SET_CUSTOMER,
  32. SET_SHIPPING_ADDRESS,
  33. SET_SHIPPING_METHOD,
  34. TRANSITION_TO_STATE,
  35. } from './graphql/shop-definitions';
  36. type TestOrderState = OrderState | 'ValidatingCustomer';
  37. const initSpy = vi.fn();
  38. const transitionStartSpy = vi.fn();
  39. const transitionEndSpy = vi.fn();
  40. const transitionEndSpy2 = vi.fn();
  41. const transitionErrorSpy = vi.fn();
  42. describe('Order process', () => {
  43. const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
  44. const customOrderProcess: CustomOrderProcess<'ValidatingCustomer' | 'PaymentProcessing'> = {
  45. init(injector) {
  46. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  47. },
  48. transitions: {
  49. AddingItems: {
  50. to: ['ValidatingCustomer'],
  51. mergeStrategy: 'replace',
  52. },
  53. ValidatingCustomer: {
  54. to: ['ArrangingPayment', 'AddingItems'],
  55. },
  56. ArrangingPayment: {
  57. to: ['PaymentProcessing'],
  58. },
  59. PaymentProcessing: {
  60. to: ['PaymentAuthorized', 'PaymentSettled'],
  61. },
  62. },
  63. onTransitionStart(fromState, toState, data) {
  64. transitionStartSpy(fromState, toState, data);
  65. if (toState === 'ValidatingCustomer') {
  66. if (!data.order.customer) {
  67. return false;
  68. }
  69. if (!data.order.customer.emailAddress.includes('@company.com')) {
  70. return VALIDATION_ERROR_MESSAGE;
  71. }
  72. }
  73. },
  74. onTransitionEnd(fromState, toState, data) {
  75. transitionEndSpy(fromState, toState, data);
  76. },
  77. onTransitionError(fromState, toState, message) {
  78. transitionErrorSpy(fromState, toState, message);
  79. },
  80. };
  81. const customOrderProcess2: CustomOrderProcess<'ValidatingCustomer'> = {
  82. transitions: {
  83. ValidatingCustomer: {
  84. to: ['Cancelled'],
  85. },
  86. },
  87. onTransitionEnd(fromState, toState, data) {
  88. transitionEndSpy2(fromState, toState, data);
  89. },
  90. };
  91. const orderErrorGuard: ErrorResultGuard<TestOrderFragmentFragment | OrderFragment> =
  92. createErrorResultGuard(input => !!input.total);
  93. const { server, adminClient, shopClient } = createTestEnvironment(
  94. mergeConfig(testConfig(), {
  95. orderOptions: { process: [defaultOrderProcess, customOrderProcess, customOrderProcess2] as any },
  96. paymentOptions: {
  97. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  98. },
  99. }),
  100. );
  101. beforeAll(async () => {
  102. await server.init({
  103. initialData: {
  104. ...initialData,
  105. paymentMethods: [
  106. {
  107. name: testSuccessfulPaymentMethod.code,
  108. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  109. },
  110. ],
  111. },
  112. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  113. customerCount: 1,
  114. });
  115. await adminClient.asSuperAdmin();
  116. }, TEST_SETUP_TIMEOUT_MS);
  117. afterAll(async () => {
  118. await server.destroy();
  119. });
  120. describe('Initial transition', () => {
  121. it('transitions from Created to AddingItems on creation', async () => {
  122. transitionStartSpy.mockClear();
  123. transitionEndSpy.mockClear();
  124. await shopClient.asAnonymousUser();
  125. await shopClient.query<
  126. CodegenShop.AddItemToOrderMutation,
  127. CodegenShop.AddItemToOrderMutationVariables
  128. >(ADD_ITEM_TO_ORDER, {
  129. productVariantId: 'T_1',
  130. quantity: 1,
  131. });
  132. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  133. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  134. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  135. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  136. });
  137. });
  138. describe('CustomOrderProcess', () => {
  139. it('CustomOrderProcess is injectable', () => {
  140. expect(initSpy).toHaveBeenCalled();
  141. expect(initSpy.mock.calls[0][0]).toBe('default');
  142. });
  143. it('replaced transition target', async () => {
  144. await shopClient.query<
  145. CodegenShop.AddItemToOrderMutation,
  146. CodegenShop.AddItemToOrderMutationVariables
  147. >(ADD_ITEM_TO_ORDER, {
  148. productVariantId: 'T_1',
  149. quantity: 1,
  150. });
  151. const { nextOrderStates } =
  152. await shopClient.query<CodegenShop.GetNextOrderStatesQuery>(GET_NEXT_STATES);
  153. expect(nextOrderStates).toEqual(['ValidatingCustomer']);
  154. });
  155. it('custom onTransitionStart handler returning false', async () => {
  156. transitionStartSpy.mockClear();
  157. transitionEndSpy.mockClear();
  158. const { transitionOrderToState } = await shopClient.query<
  159. CodegenShop.TransitionToStateMutation,
  160. CodegenShop.TransitionToStateMutationVariables
  161. >(TRANSITION_TO_STATE, {
  162. state: 'ValidatingCustomer',
  163. });
  164. orderErrorGuard.assertSuccess(transitionOrderToState);
  165. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  166. expect(transitionEndSpy).not.toHaveBeenCalled();
  167. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([
  168. 'AddingItems',
  169. 'ValidatingCustomer',
  170. ]);
  171. expect(transitionOrderToState?.state).toBe('AddingItems');
  172. });
  173. it('custom onTransitionStart handler returning error message', async () => {
  174. transitionStartSpy.mockClear();
  175. transitionErrorSpy.mockClear();
  176. await shopClient.query<
  177. CodegenShop.SetCustomerForOrderMutation,
  178. CodegenShop.SetCustomerForOrderMutationVariables
  179. >(SET_CUSTOMER, {
  180. input: {
  181. firstName: 'Joe',
  182. lastName: 'Test',
  183. emailAddress: 'joetest@gmail.com',
  184. },
  185. });
  186. const { transitionOrderToState } = await shopClient.query<
  187. CodegenShop.TransitionToStateMutation,
  188. CodegenShop.TransitionToStateMutationVariables
  189. >(TRANSITION_TO_STATE, {
  190. state: 'ValidatingCustomer',
  191. });
  192. orderErrorGuard.assertErrorResult(transitionOrderToState);
  193. expect(transitionOrderToState!.message).toBe(
  194. 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
  195. );
  196. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  197. expect(transitionOrderToState!.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  198. expect(transitionOrderToState!.fromState).toBe('AddingItems');
  199. expect(transitionOrderToState!.toState).toBe('ValidatingCustomer');
  200. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  201. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  202. expect(transitionEndSpy).not.toHaveBeenCalled();
  203. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  204. 'AddingItems',
  205. 'ValidatingCustomer',
  206. VALIDATION_ERROR_MESSAGE,
  207. ]);
  208. });
  209. it('custom onTransitionStart handler allows transition', async () => {
  210. transitionEndSpy.mockClear();
  211. await shopClient.query<
  212. CodegenShop.SetCustomerForOrderMutation,
  213. CodegenShop.SetCustomerForOrderMutationVariables
  214. >(SET_CUSTOMER, {
  215. input: {
  216. firstName: 'Joe',
  217. lastName: 'Test',
  218. emailAddress: 'joetest@company.com',
  219. },
  220. });
  221. const { transitionOrderToState } = await shopClient.query<
  222. CodegenShop.TransitionToStateMutation,
  223. CodegenShop.TransitionToStateMutationVariables
  224. >(TRANSITION_TO_STATE, {
  225. state: 'ValidatingCustomer',
  226. });
  227. orderErrorGuard.assertSuccess(transitionOrderToState);
  228. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  229. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
  230. expect(transitionOrderToState?.state).toBe('ValidatingCustomer');
  231. });
  232. it('composes multiple CustomOrderProcesses', async () => {
  233. transitionEndSpy.mockClear();
  234. transitionEndSpy2.mockClear();
  235. const { nextOrderStates } =
  236. await shopClient.query<CodegenShop.GetNextOrderStatesQuery>(GET_NEXT_STATES);
  237. expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
  238. await shopClient.query<
  239. CodegenShop.TransitionToStateMutation,
  240. CodegenShop.TransitionToStateMutationVariables
  241. >(TRANSITION_TO_STATE, {
  242. state: 'AddingItems',
  243. });
  244. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']);
  245. expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([
  246. 'ValidatingCustomer',
  247. 'AddingItems',
  248. ]);
  249. });
  250. // https://github.com/vendurehq/vendure/issues/963
  251. it('allows addPaymentToOrder from a custom state', async () => {
  252. await shopClient.query<
  253. CodegenShop.SetShippingMethodMutation,
  254. CodegenShop.SetShippingMethodMutationVariables
  255. >(SET_SHIPPING_METHOD, { id: 'T_1' });
  256. const result0 = await shopClient.query<
  257. TransitionToStateMutation,
  258. TransitionToStateMutationVariables
  259. >(TRANSITION_TO_STATE, {
  260. state: 'ValidatingCustomer',
  261. });
  262. orderErrorGuard.assertSuccess(result0.transitionOrderToState);
  263. const result1 = await shopClient.query<
  264. TransitionToStateMutation,
  265. TransitionToStateMutationVariables
  266. >(TRANSITION_TO_STATE, {
  267. state: 'ArrangingPayment',
  268. });
  269. orderErrorGuard.assertSuccess(result1.transitionOrderToState);
  270. const result2 = await shopClient.query<
  271. TransitionToStateMutation,
  272. TransitionToStateMutationVariables
  273. >(TRANSITION_TO_STATE, {
  274. state: 'PaymentProcessing',
  275. });
  276. orderErrorGuard.assertSuccess(result2.transitionOrderToState);
  277. expect(result2.transitionOrderToState.state).toBe('PaymentProcessing');
  278. const { addPaymentToOrder } = await shopClient.query<
  279. AddPaymentToOrderMutation,
  280. AddPaymentToOrderMutationVariables
  281. >(ADD_PAYMENT, {
  282. input: {
  283. method: testSuccessfulPaymentMethod.code,
  284. metadata: {},
  285. },
  286. });
  287. orderErrorGuard.assertSuccess(addPaymentToOrder);
  288. expect(addPaymentToOrder.state).toBe('PaymentSettled');
  289. });
  290. });
  291. describe('Admin API transition constraints', () => {
  292. let order: NonNullable<TestOrderFragmentFragment>;
  293. beforeAll(async () => {
  294. await shopClient.asAnonymousUser();
  295. await shopClient.query<
  296. CodegenShop.AddItemToOrderMutation,
  297. CodegenShop.AddItemToOrderMutationVariables
  298. >(ADD_ITEM_TO_ORDER, {
  299. productVariantId: 'T_1',
  300. quantity: 1,
  301. });
  302. await shopClient.query<
  303. CodegenShop.SetCustomerForOrderMutation,
  304. CodegenShop.SetCustomerForOrderMutationVariables
  305. >(SET_CUSTOMER, {
  306. input: {
  307. firstName: 'Su',
  308. lastName: 'Test',
  309. emailAddress: 'sutest@company.com',
  310. },
  311. });
  312. await shopClient.query<
  313. CodegenShop.SetShippingAddressMutation,
  314. CodegenShop.SetShippingAddressMutationVariables
  315. >(SET_SHIPPING_ADDRESS, {
  316. input: {
  317. fullName: 'name',
  318. streetLine1: '12 the street',
  319. city: 'foo',
  320. postalCode: '123456',
  321. countryCode: 'US',
  322. phoneNumber: '4444444',
  323. },
  324. });
  325. await shopClient.query<
  326. CodegenShop.SetShippingMethodMutation,
  327. CodegenShop.SetShippingMethodMutationVariables
  328. >(SET_SHIPPING_METHOD, { id: 'T_1' });
  329. await shopClient.query<
  330. CodegenShop.TransitionToStateMutation,
  331. CodegenShop.TransitionToStateMutationVariables
  332. >(TRANSITION_TO_STATE, {
  333. state: 'ValidatingCustomer',
  334. });
  335. const { transitionOrderToState } = await shopClient.query<
  336. CodegenShop.TransitionToStateMutation,
  337. CodegenShop.TransitionToStateMutationVariables
  338. >(TRANSITION_TO_STATE, {
  339. state: 'ArrangingPayment',
  340. });
  341. orderErrorGuard.assertSuccess(transitionOrderToState);
  342. order = transitionOrderToState!;
  343. });
  344. it('cannot manually transition to PaymentAuthorized', async () => {
  345. expect(order.state).toBe('ArrangingPayment');
  346. const { transitionOrderToState } = await adminClient.query<
  347. Codegen.AdminTransitionMutation,
  348. Codegen.AdminTransitionMutationVariables
  349. >(ADMIN_TRANSITION_TO_STATE, {
  350. id: order.id,
  351. state: 'PaymentAuthorized',
  352. });
  353. orderErrorGuard.assertErrorResult(transitionOrderToState);
  354. expect(transitionOrderToState!.message).toBe(
  355. 'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"',
  356. );
  357. expect(transitionOrderToState!.transitionError).toBe(
  358. 'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
  359. );
  360. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  361. GET_ORDER,
  362. {
  363. id: order.id,
  364. },
  365. );
  366. expect(result.order?.state).toBe('ArrangingPayment');
  367. });
  368. it('cannot manually transition to PaymentSettled', async () => {
  369. const { transitionOrderToState } = await adminClient.query<
  370. Codegen.AdminTransitionMutation,
  371. Codegen.AdminTransitionMutationVariables
  372. >(ADMIN_TRANSITION_TO_STATE, {
  373. id: order.id,
  374. state: 'PaymentSettled',
  375. });
  376. orderErrorGuard.assertErrorResult(transitionOrderToState);
  377. expect(transitionOrderToState!.message).toBe(
  378. 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
  379. );
  380. expect(transitionOrderToState!.transitionError).toContain(
  381. 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
  382. );
  383. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  384. GET_ORDER,
  385. {
  386. id: order.id,
  387. },
  388. );
  389. expect(result.order?.state).toBe('ArrangingPayment');
  390. });
  391. it('cannot manually transition to Cancelled', async () => {
  392. const { addPaymentToOrder } = await shopClient.query<
  393. CodegenShop.AddPaymentToOrderMutation,
  394. CodegenShop.AddPaymentToOrderMutationVariables
  395. >(ADD_PAYMENT, {
  396. input: {
  397. method: testSuccessfulPaymentMethod.code,
  398. metadata: {},
  399. },
  400. });
  401. orderErrorGuard.assertSuccess(addPaymentToOrder);
  402. expect(addPaymentToOrder?.state).toBe('PaymentSettled');
  403. const { transitionOrderToState } = await adminClient.query<
  404. Codegen.AdminTransitionMutation,
  405. Codegen.AdminTransitionMutationVariables
  406. >(ADMIN_TRANSITION_TO_STATE, {
  407. id: order.id,
  408. state: 'Cancelled',
  409. });
  410. orderErrorGuard.assertErrorResult(transitionOrderToState);
  411. expect(transitionOrderToState!.message).toBe(
  412. 'Cannot transition Order from "PaymentSettled" to "Cancelled"',
  413. );
  414. expect(transitionOrderToState!.transitionError).toContain(
  415. 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
  416. );
  417. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  418. GET_ORDER,
  419. {
  420. id: order.id,
  421. },
  422. );
  423. expect(result.order?.state).toBe('PaymentSettled');
  424. });
  425. it('cannot manually transition to PartiallyDelivered', async () => {
  426. const { transitionOrderToState } = await adminClient.query<
  427. Codegen.AdminTransitionMutation,
  428. Codegen.AdminTransitionMutationVariables
  429. >(ADMIN_TRANSITION_TO_STATE, {
  430. id: order.id,
  431. state: 'PartiallyDelivered',
  432. });
  433. orderErrorGuard.assertErrorResult(transitionOrderToState);
  434. expect(transitionOrderToState!.message).toBe(
  435. 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
  436. );
  437. expect(transitionOrderToState!.transitionError).toContain(
  438. 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
  439. );
  440. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  441. GET_ORDER,
  442. {
  443. id: order.id,
  444. },
  445. );
  446. expect(result.order?.state).toBe('PaymentSettled');
  447. });
  448. it('cannot manually transition to PartiallyDelivered', async () => {
  449. const { transitionOrderToState } = await adminClient.query<
  450. Codegen.AdminTransitionMutation,
  451. Codegen.AdminTransitionMutationVariables
  452. >(ADMIN_TRANSITION_TO_STATE, {
  453. id: order.id,
  454. state: 'Delivered',
  455. });
  456. orderErrorGuard.assertErrorResult(transitionOrderToState);
  457. expect(transitionOrderToState!.message).toBe(
  458. 'Cannot transition Order from "PaymentSettled" to "Delivered"',
  459. );
  460. expect(transitionOrderToState!.transitionError).toContain(
  461. 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
  462. );
  463. const result = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  464. GET_ORDER,
  465. {
  466. id: order.id,
  467. },
  468. );
  469. expect(result.order?.state).toBe('PaymentSettled');
  470. });
  471. });
  472. });