order-process.e2e-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. /* eslint-disable @typescript-eslint/no-non-null-assertion */
  2. import { ErrorCode } from '@vendure/common/lib/generated-types';
  3. import { CustomOrderProcess, defaultOrderProcess, mergeConfig, TransactionalConnection } from '@vendure/core';
  4. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  5. import path from 'path';
  6. import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
  7. import { initialData } from '../../../e2e-common/e2e-initial-data';
  8. import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
  9. import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
  10. import { orderFragment } from './graphql/fragments-admin';
  11. import { FragmentOf } from './graphql/graphql-admin';
  12. import { FragmentOf as FragmentOfShop } from './graphql/graphql-shop';
  13. import { adminTransitionToStateDocument, getOrderDocument } from './graphql/shared-definitions';
  14. import {
  15. addItemToOrderDocument,
  16. addPaymentDocument,
  17. getNextStatesDocument,
  18. setCustomerDocument,
  19. setShippingAddressDocument,
  20. setShippingMethodDocument,
  21. testOrderFragment,
  22. transitionToStateDocument,
  23. updatedOrderFragment,
  24. } from './graphql/shop-definitions';
  25. const initSpy = vi.fn();
  26. const transitionStartSpy = vi.fn();
  27. const transitionEndSpy = vi.fn();
  28. const transitionEndSpy2 = vi.fn();
  29. const transitionErrorSpy = vi.fn();
  30. describe('Order process', () => {
  31. const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
  32. const customOrderProcess: CustomOrderProcess<'ValidatingCustomer' | 'PaymentProcessing'> = {
  33. init(injector) {
  34. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  35. },
  36. transitions: {
  37. AddingItems: {
  38. to: ['ValidatingCustomer'],
  39. mergeStrategy: 'replace',
  40. },
  41. ValidatingCustomer: {
  42. to: ['ArrangingPayment', 'AddingItems'],
  43. },
  44. ArrangingPayment: {
  45. to: ['PaymentProcessing'],
  46. },
  47. PaymentProcessing: {
  48. to: ['PaymentAuthorized', 'PaymentSettled'],
  49. },
  50. },
  51. onTransitionStart(fromState, toState, data) {
  52. transitionStartSpy(fromState, toState, data);
  53. if (toState === 'ValidatingCustomer') {
  54. if (!data.order.customer) {
  55. return false;
  56. }
  57. if (!data.order.customer.emailAddress.includes('@company.com')) {
  58. return VALIDATION_ERROR_MESSAGE;
  59. }
  60. }
  61. },
  62. onTransitionEnd(fromState, toState, data) {
  63. transitionEndSpy(fromState, toState, data);
  64. },
  65. onTransitionError(fromState, toState, message) {
  66. transitionErrorSpy(fromState, toState, message);
  67. },
  68. };
  69. const customOrderProcess2: CustomOrderProcess<'ValidatingCustomer'> = {
  70. transitions: {
  71. ValidatingCustomer: {
  72. to: ['Cancelled'],
  73. },
  74. },
  75. onTransitionEnd(fromState, toState, data) {
  76. transitionEndSpy2(fromState, toState, data);
  77. },
  78. };
  79. // Create guards for different fragment types
  80. type TestOrderFragmentType = FragmentOfShop<typeof testOrderFragment>;
  81. type UpdatedOrderFragmentType = FragmentOfShop<typeof updatedOrderFragment>;
  82. type AdminOrderFragmentType = FragmentOf<typeof orderFragment>;
  83. const testOrderGuard: ErrorResultGuard<TestOrderFragmentType> = createErrorResultGuard(
  84. input => !!input.lines,
  85. );
  86. const updatedOrderGuard: ErrorResultGuard<UpdatedOrderFragmentType> = createErrorResultGuard(
  87. input => !!input.lines,
  88. );
  89. const adminOrderGuard: ErrorResultGuard<AdminOrderFragmentType> = createErrorResultGuard(
  90. input => !!input.id,
  91. );
  92. const { server, adminClient, shopClient } = createTestEnvironment(
  93. mergeConfig(testConfig(), {
  94. orderOptions: { process: [defaultOrderProcess, customOrderProcess, customOrderProcess2] as any },
  95. paymentOptions: {
  96. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  97. },
  98. }),
  99. );
  100. beforeAll(async () => {
  101. await server.init({
  102. initialData: {
  103. ...initialData,
  104. paymentMethods: [
  105. {
  106. name: testSuccessfulPaymentMethod.code,
  107. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  108. },
  109. ],
  110. },
  111. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  112. customerCount: 1,
  113. });
  114. await adminClient.asSuperAdmin();
  115. }, TEST_SETUP_TIMEOUT_MS);
  116. afterAll(async () => {
  117. await server.destroy();
  118. });
  119. describe('Initial transition', () => {
  120. it('transitions from Created to AddingItems on creation', async () => {
  121. transitionStartSpy.mockClear();
  122. transitionEndSpy.mockClear();
  123. await shopClient.asAnonymousUser();
  124. await shopClient.query(addItemToOrderDocument, {
  125. productVariantId: 'T_1',
  126. quantity: 1,
  127. });
  128. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  129. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  130. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  131. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  132. });
  133. });
  134. describe('CustomOrderProcess', () => {
  135. it('CustomOrderProcess is injectable', () => {
  136. expect(initSpy).toHaveBeenCalled();
  137. expect(initSpy.mock.calls[0][0]).toBe('default');
  138. });
  139. it('replaced transition target', async () => {
  140. await shopClient.query(addItemToOrderDocument, {
  141. productVariantId: 'T_1',
  142. quantity: 1,
  143. });
  144. const { nextOrderStates } = await shopClient.query(getNextStatesDocument);
  145. expect(nextOrderStates).toEqual(['ValidatingCustomer']);
  146. });
  147. it('custom onTransitionStart handler returning false', async () => {
  148. transitionStartSpy.mockClear();
  149. transitionEndSpy.mockClear();
  150. const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, {
  151. state: 'ValidatingCustomer',
  152. });
  153. testOrderGuard.assertSuccess(transitionOrderToState!);
  154. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  155. expect(transitionEndSpy).not.toHaveBeenCalled();
  156. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([
  157. 'AddingItems',
  158. 'ValidatingCustomer',
  159. ]);
  160. expect(transitionOrderToState.state).toBe('AddingItems');
  161. });
  162. it('custom onTransitionStart handler returning error message', async () => {
  163. transitionStartSpy.mockClear();
  164. transitionErrorSpy.mockClear();
  165. await shopClient.query(setCustomerDocument, {
  166. input: {
  167. firstName: 'Joe',
  168. lastName: 'Test',
  169. emailAddress: 'joetest@gmail.com',
  170. },
  171. });
  172. const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, {
  173. state: 'ValidatingCustomer',
  174. });
  175. testOrderGuard.assertErrorResult(transitionOrderToState!);
  176. expect(transitionOrderToState.message).toBe(
  177. 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
  178. );
  179. expect(transitionOrderToState.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  180. expect(transitionOrderToState.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  181. expect(transitionOrderToState.fromState).toBe('AddingItems');
  182. expect(transitionOrderToState.toState).toBe('ValidatingCustomer');
  183. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  184. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  185. expect(transitionEndSpy).not.toHaveBeenCalled();
  186. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  187. 'AddingItems',
  188. 'ValidatingCustomer',
  189. VALIDATION_ERROR_MESSAGE,
  190. ]);
  191. });
  192. it('custom onTransitionStart handler allows transition', async () => {
  193. transitionEndSpy.mockClear();
  194. await shopClient.query(setCustomerDocument, {
  195. input: {
  196. firstName: 'Joe',
  197. lastName: 'Test',
  198. emailAddress: 'joetest@company.com',
  199. },
  200. });
  201. const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, {
  202. state: 'ValidatingCustomer',
  203. });
  204. testOrderGuard.assertSuccess(transitionOrderToState!);
  205. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  206. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
  207. expect(transitionOrderToState.state).toBe('ValidatingCustomer');
  208. });
  209. it('composes multiple CustomOrderProcesses', async () => {
  210. transitionEndSpy.mockClear();
  211. transitionEndSpy2.mockClear();
  212. const { nextOrderStates } = await shopClient.query(getNextStatesDocument);
  213. expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
  214. await shopClient.query(transitionToStateDocument, {
  215. state: 'AddingItems',
  216. });
  217. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']);
  218. expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([
  219. 'ValidatingCustomer',
  220. 'AddingItems',
  221. ]);
  222. });
  223. // https://github.com/vendure-ecommerce/vendure/issues/963
  224. it('allows addPaymentToOrder from a custom state', async () => {
  225. await shopClient.query(setShippingMethodDocument, { id: ['T_1'] });
  226. const result0 = await shopClient.query(transitionToStateDocument, {
  227. state: 'ValidatingCustomer',
  228. });
  229. testOrderGuard.assertSuccess(result0.transitionOrderToState!);
  230. const result1 = await shopClient.query(transitionToStateDocument, {
  231. state: 'ArrangingPayment',
  232. });
  233. testOrderGuard.assertSuccess(result1.transitionOrderToState!);
  234. const result2 = await shopClient.query(transitionToStateDocument, {
  235. state: 'PaymentProcessing',
  236. });
  237. testOrderGuard.assertSuccess(result2.transitionOrderToState!);
  238. expect(result2.transitionOrderToState.state).toBe('PaymentProcessing');
  239. const { addPaymentToOrder } = await shopClient.query(addPaymentDocument, {
  240. input: {
  241. method: testSuccessfulPaymentMethod.code,
  242. metadata: {},
  243. },
  244. });
  245. updatedOrderGuard.assertSuccess(addPaymentToOrder);
  246. expect(addPaymentToOrder.state).toBe('PaymentSettled');
  247. });
  248. });
  249. describe('Admin API transition constraints', () => {
  250. let order: FragmentOfShop<typeof testOrderFragment>;
  251. beforeAll(async () => {
  252. await shopClient.asAnonymousUser();
  253. await shopClient.query(addItemToOrderDocument, {
  254. productVariantId: 'T_1',
  255. quantity: 1,
  256. });
  257. await shopClient.query(setCustomerDocument, {
  258. input: {
  259. firstName: 'Su',
  260. lastName: 'Test',
  261. emailAddress: 'sutest@company.com',
  262. },
  263. });
  264. await shopClient.query(setShippingAddressDocument, {
  265. input: {
  266. fullName: 'name',
  267. streetLine1: '12 the street',
  268. city: 'foo',
  269. postalCode: '123456',
  270. countryCode: 'US',
  271. phoneNumber: '4444444',
  272. },
  273. });
  274. await shopClient.query(setShippingMethodDocument, { id: ['T_1'] });
  275. await shopClient.query(transitionToStateDocument, {
  276. state: 'ValidatingCustomer',
  277. });
  278. const { transitionOrderToState } = await shopClient.query(transitionToStateDocument, {
  279. state: 'ArrangingPayment',
  280. });
  281. testOrderGuard.assertSuccess(transitionOrderToState!);
  282. order = transitionOrderToState as TestOrderFragmentType;
  283. });
  284. it('cannot manually transition to PaymentAuthorized', async () => {
  285. expect(order.state).toBe('ArrangingPayment');
  286. const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, {
  287. id: order.id,
  288. state: 'PaymentAuthorized',
  289. });
  290. adminOrderGuard.assertErrorResult(transitionOrderToState!);
  291. expect(transitionOrderToState.message).toBe(
  292. 'Cannot transition Order from "ArrangingPayment" to "PaymentAuthorized"',
  293. );
  294. expect(transitionOrderToState.transitionError).toBe(
  295. 'Cannot transition Order to the "PaymentAuthorized" state when the total is not covered by authorized Payments',
  296. );
  297. const result = await adminClient.query(getOrderDocument, {
  298. id: order.id,
  299. });
  300. expect(result.order?.state).toBe('ArrangingPayment');
  301. });
  302. it('cannot manually transition to PaymentSettled', async () => {
  303. const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, {
  304. id: order.id,
  305. state: 'PaymentSettled',
  306. });
  307. adminOrderGuard.assertErrorResult(transitionOrderToState!);
  308. expect(transitionOrderToState.message).toBe(
  309. 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
  310. );
  311. expect(transitionOrderToState.transitionError).toContain(
  312. 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
  313. );
  314. const result = await adminClient.query(getOrderDocument, {
  315. id: order.id,
  316. });
  317. expect(result.order?.state).toBe('ArrangingPayment');
  318. });
  319. it('cannot manually transition to Cancelled', async () => {
  320. const { addPaymentToOrder } = await shopClient.query(addPaymentDocument, {
  321. input: {
  322. method: testSuccessfulPaymentMethod.code,
  323. metadata: {},
  324. },
  325. });
  326. updatedOrderGuard.assertSuccess(addPaymentToOrder);
  327. expect(addPaymentToOrder?.state).toBe('PaymentSettled');
  328. const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, {
  329. id: order.id,
  330. state: 'Cancelled',
  331. });
  332. adminOrderGuard.assertErrorResult(transitionOrderToState!);
  333. expect(transitionOrderToState.message).toBe(
  334. 'Cannot transition Order from "PaymentSettled" to "Cancelled"',
  335. );
  336. expect(transitionOrderToState.transitionError).toContain(
  337. 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
  338. );
  339. const result = await adminClient.query(getOrderDocument, {
  340. id: order.id,
  341. });
  342. expect(result.order?.state).toBe('PaymentSettled');
  343. });
  344. it('cannot manually transition to PartiallyDelivered', async () => {
  345. const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, {
  346. id: order.id,
  347. state: 'PartiallyDelivered',
  348. });
  349. adminOrderGuard.assertErrorResult(transitionOrderToState!);
  350. expect(transitionOrderToState.message).toBe(
  351. 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
  352. );
  353. expect(transitionOrderToState.transitionError).toContain(
  354. 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
  355. );
  356. const result = await adminClient.query(getOrderDocument, {
  357. id: order.id,
  358. });
  359. expect(result.order?.state).toBe('PaymentSettled');
  360. });
  361. it('cannot manually transition to Delivered', async () => {
  362. const { transitionOrderToState } = await adminClient.query(adminTransitionToStateDocument, {
  363. id: order.id,
  364. state: 'Delivered',
  365. });
  366. adminOrderGuard.assertErrorResult(transitionOrderToState!);
  367. expect(transitionOrderToState.message).toBe(
  368. 'Cannot transition Order from "PaymentSettled" to "Delivered"',
  369. );
  370. expect(transitionOrderToState.transitionError).toContain(
  371. 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
  372. );
  373. const result = await adminClient.query(getOrderDocument, {
  374. id: order.id,
  375. });
  376. expect(result.order?.state).toBe('PaymentSettled');
  377. });
  378. });
  379. });