order-process.e2e-spec.ts 20 KB

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