order-process.e2e-spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  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. } from './graphql/generated-e2e-shop-types';
  20. import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
  21. import {
  22. ADD_ITEM_TO_ORDER,
  23. ADD_PAYMENT,
  24. GET_NEXT_STATES,
  25. SET_CUSTOMER,
  26. SET_SHIPPING_ADDRESS,
  27. SET_SHIPPING_METHOD,
  28. TRANSITION_TO_STATE,
  29. } from './graphql/shop-definitions';
  30. type TestOrderState = OrderState | 'ValidatingCustomer';
  31. const initSpy = jest.fn();
  32. const transitionStartSpy = jest.fn();
  33. const transitionEndSpy = jest.fn();
  34. const transitionEndSpy2 = jest.fn();
  35. const transitionErrorSpy = jest.fn();
  36. describe('Order process', () => {
  37. const VALIDATION_ERROR_MESSAGE = 'Customer must have a company email address';
  38. const customOrderProcess: CustomOrderProcess<'ValidatingCustomer'> = {
  39. init(injector) {
  40. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  41. },
  42. transitions: {
  43. AddingItems: {
  44. to: ['ValidatingCustomer'],
  45. mergeStrategy: 'replace',
  46. },
  47. ValidatingCustomer: {
  48. to: ['ArrangingPayment', 'AddingItems'],
  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. const orderErrorGuard: ErrorResultGuard<TestOrderFragmentFragment | OrderFragment> =
  80. createErrorResultGuard(input => !!input.total);
  81. const { server, adminClient, shopClient } = createTestEnvironment(
  82. mergeConfig(testConfig(), {
  83. orderOptions: { process: [customOrderProcess as any, customOrderProcess2 as any] },
  84. paymentOptions: {
  85. paymentMethodHandlers: [testSuccessfulPaymentMethod],
  86. },
  87. }),
  88. );
  89. beforeAll(async () => {
  90. await server.init({
  91. initialData: {
  92. ...initialData,
  93. paymentMethods: [
  94. {
  95. name: testSuccessfulPaymentMethod.code,
  96. handler: { code: testSuccessfulPaymentMethod.code, arguments: [] },
  97. },
  98. ],
  99. },
  100. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  101. customerCount: 1,
  102. });
  103. await adminClient.asSuperAdmin();
  104. }, TEST_SETUP_TIMEOUT_MS);
  105. afterAll(async () => {
  106. await server.destroy();
  107. });
  108. describe('Initial transition', () => {
  109. it('transitions from Created to AddingItems on creation', async () => {
  110. transitionStartSpy.mockClear();
  111. transitionEndSpy.mockClear();
  112. await shopClient.asAnonymousUser();
  113. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  114. productVariantId: 'T_1',
  115. quantity: 1,
  116. });
  117. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  118. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  119. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  120. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'AddingItems']);
  121. });
  122. });
  123. describe('CustomOrderProcess', () => {
  124. it('CustomOrderProcess is injectable', () => {
  125. expect(initSpy).toHaveBeenCalled();
  126. expect(initSpy.mock.calls[0][0]).toBe('default');
  127. });
  128. it('replaced transition target', async () => {
  129. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  130. productVariantId: 'T_1',
  131. quantity: 1,
  132. });
  133. const { nextOrderStates } = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
  134. expect(nextOrderStates).toEqual(['ValidatingCustomer']);
  135. });
  136. it('custom onTransitionStart handler returning false', async () => {
  137. transitionStartSpy.mockClear();
  138. transitionEndSpy.mockClear();
  139. const { transitionOrderToState } = await shopClient.query<
  140. TransitionToState.Mutation,
  141. TransitionToState.Variables
  142. >(TRANSITION_TO_STATE, {
  143. state: 'ValidatingCustomer',
  144. });
  145. orderErrorGuard.assertSuccess(transitionOrderToState);
  146. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  147. expect(transitionEndSpy).not.toHaveBeenCalled();
  148. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual([
  149. 'AddingItems',
  150. 'ValidatingCustomer',
  151. ]);
  152. expect(transitionOrderToState?.state).toBe('AddingItems');
  153. });
  154. it('custom onTransitionStart handler returning error message', async () => {
  155. transitionStartSpy.mockClear();
  156. transitionErrorSpy.mockClear();
  157. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  158. SET_CUSTOMER,
  159. {
  160. input: {
  161. firstName: 'Joe',
  162. lastName: 'Test',
  163. emailAddress: 'joetest@gmail.com',
  164. },
  165. },
  166. );
  167. const { transitionOrderToState } = await shopClient.query<
  168. TransitionToState.Mutation,
  169. TransitionToState.Variables
  170. >(TRANSITION_TO_STATE, {
  171. state: 'ValidatingCustomer',
  172. });
  173. orderErrorGuard.assertErrorResult(transitionOrderToState);
  174. expect(transitionOrderToState!.message).toBe(
  175. 'Cannot transition Order from "AddingItems" to "ValidatingCustomer"',
  176. );
  177. expect(transitionOrderToState!.errorCode).toBe(ErrorCode.ORDER_STATE_TRANSITION_ERROR);
  178. expect(transitionOrderToState!.transitionError).toBe(VALIDATION_ERROR_MESSAGE);
  179. expect(transitionOrderToState!.fromState).toBe('AddingItems');
  180. expect(transitionOrderToState!.toState).toBe('ValidatingCustomer');
  181. expect(transitionStartSpy).toHaveBeenCalledTimes(1);
  182. expect(transitionErrorSpy).toHaveBeenCalledTimes(1);
  183. expect(transitionEndSpy).not.toHaveBeenCalled();
  184. expect(transitionErrorSpy.mock.calls[0]).toEqual([
  185. 'AddingItems',
  186. 'ValidatingCustomer',
  187. VALIDATION_ERROR_MESSAGE,
  188. ]);
  189. });
  190. it('custom onTransitionStart handler allows transition', async () => {
  191. transitionEndSpy.mockClear();
  192. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  193. SET_CUSTOMER,
  194. {
  195. input: {
  196. firstName: 'Joe',
  197. lastName: 'Test',
  198. emailAddress: 'joetest@company.com',
  199. },
  200. },
  201. );
  202. const { transitionOrderToState } = await shopClient.query<
  203. TransitionToState.Mutation,
  204. TransitionToState.Variables
  205. >(TRANSITION_TO_STATE, {
  206. state: 'ValidatingCustomer',
  207. });
  208. orderErrorGuard.assertSuccess(transitionOrderToState);
  209. expect(transitionEndSpy).toHaveBeenCalledTimes(1);
  210. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['AddingItems', 'ValidatingCustomer']);
  211. expect(transitionOrderToState?.state).toBe('ValidatingCustomer');
  212. });
  213. it('composes multiple CustomOrderProcesses', async () => {
  214. transitionEndSpy.mockClear();
  215. transitionEndSpy2.mockClear();
  216. const { nextOrderStates } = await shopClient.query<GetNextOrderStates.Query>(GET_NEXT_STATES);
  217. expect(nextOrderStates).toEqual(['ArrangingPayment', 'AddingItems', 'Cancelled']);
  218. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
  219. TRANSITION_TO_STATE,
  220. {
  221. state: 'AddingItems',
  222. },
  223. );
  224. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['ValidatingCustomer', 'AddingItems']);
  225. expect(transitionEndSpy2.mock.calls[0].slice(0, 2)).toEqual([
  226. 'ValidatingCustomer',
  227. 'AddingItems',
  228. ]);
  229. });
  230. });
  231. describe('Admin API transition constraints', () => {
  232. let order: NonNullable<TestOrderFragmentFragment>;
  233. beforeAll(async () => {
  234. await shopClient.asAnonymousUser();
  235. await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(ADD_ITEM_TO_ORDER, {
  236. productVariantId: 'T_1',
  237. quantity: 1,
  238. });
  239. await shopClient.query<SetCustomerForOrder.Mutation, SetCustomerForOrder.Variables>(
  240. SET_CUSTOMER,
  241. {
  242. input: {
  243. firstName: 'Su',
  244. lastName: 'Test',
  245. emailAddress: 'sutest@company.com',
  246. },
  247. },
  248. );
  249. await shopClient.query<SetShippingAddress.Mutation, SetShippingAddress.Variables>(
  250. SET_SHIPPING_ADDRESS,
  251. {
  252. input: {
  253. fullName: 'name',
  254. streetLine1: '12 the street',
  255. city: 'foo',
  256. postalCode: '123456',
  257. countryCode: 'US',
  258. phoneNumber: '4444444',
  259. },
  260. },
  261. );
  262. await shopClient.query<SetShippingMethod.Mutation, SetShippingMethod.Variables>(
  263. SET_SHIPPING_METHOD,
  264. { id: 'T_1' },
  265. );
  266. await shopClient.query<TransitionToState.Mutation, TransitionToState.Variables>(
  267. TRANSITION_TO_STATE,
  268. {
  269. state: 'ValidatingCustomer',
  270. },
  271. );
  272. const { transitionOrderToState } = await shopClient.query<
  273. TransitionToState.Mutation,
  274. TransitionToState.Variables
  275. >(TRANSITION_TO_STATE, {
  276. state: 'ArrangingPayment',
  277. });
  278. orderErrorGuard.assertSuccess(transitionOrderToState);
  279. order = transitionOrderToState!;
  280. });
  281. it('cannot manually transition to PaymentAuthorized', async () => {
  282. expect(order.state).toBe('ArrangingPayment');
  283. const { transitionOrderToState } = await adminClient.query<
  284. AdminTransition.Mutation,
  285. AdminTransition.Variables
  286. >(ADMIN_TRANSITION_TO_STATE, {
  287. id: order.id,
  288. state: 'PaymentAuthorized',
  289. });
  290. orderErrorGuard.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<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  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<
  304. AdminTransition.Mutation,
  305. AdminTransition.Variables
  306. >(ADMIN_TRANSITION_TO_STATE, {
  307. id: order.id,
  308. state: 'PaymentSettled',
  309. });
  310. orderErrorGuard.assertErrorResult(transitionOrderToState);
  311. expect(transitionOrderToState!.message).toBe(
  312. 'Cannot transition Order from "ArrangingPayment" to "PaymentSettled"',
  313. );
  314. expect(transitionOrderToState!.transitionError).toContain(
  315. 'Cannot transition Order to the "PaymentSettled" state when the total is not covered by settled Payments',
  316. );
  317. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  318. id: order.id,
  319. });
  320. expect(result.order?.state).toBe('ArrangingPayment');
  321. });
  322. it('cannot manually transition to Cancelled', async () => {
  323. const { addPaymentToOrder } = await shopClient.query<
  324. AddPaymentToOrder.Mutation,
  325. AddPaymentToOrder.Variables
  326. >(ADD_PAYMENT, {
  327. input: {
  328. method: testSuccessfulPaymentMethod.code,
  329. metadata: {},
  330. },
  331. });
  332. orderErrorGuard.assertSuccess(addPaymentToOrder);
  333. expect(addPaymentToOrder?.state).toBe('PaymentSettled');
  334. const { transitionOrderToState } = await adminClient.query<
  335. AdminTransition.Mutation,
  336. AdminTransition.Variables
  337. >(ADMIN_TRANSITION_TO_STATE, {
  338. id: order.id,
  339. state: 'Cancelled',
  340. });
  341. orderErrorGuard.assertErrorResult(transitionOrderToState);
  342. expect(transitionOrderToState!.message).toBe(
  343. 'Cannot transition Order from "PaymentSettled" to "Cancelled"',
  344. );
  345. expect(transitionOrderToState!.transitionError).toContain(
  346. 'Cannot transition Order to the "Cancelled" state unless all OrderItems are cancelled',
  347. );
  348. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  349. id: order.id,
  350. });
  351. expect(result.order?.state).toBe('PaymentSettled');
  352. });
  353. it('cannot manually transition to PartiallyDelivered', async () => {
  354. const { transitionOrderToState } = await adminClient.query<
  355. AdminTransition.Mutation,
  356. AdminTransition.Variables
  357. >(ADMIN_TRANSITION_TO_STATE, {
  358. id: order.id,
  359. state: 'PartiallyDelivered',
  360. });
  361. orderErrorGuard.assertErrorResult(transitionOrderToState);
  362. expect(transitionOrderToState!.message).toBe(
  363. 'Cannot transition Order from "PaymentSettled" to "PartiallyDelivered"',
  364. );
  365. expect(transitionOrderToState!.transitionError).toContain(
  366. 'Cannot transition Order to the "PartiallyDelivered" state unless some OrderItems are delivered',
  367. );
  368. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  369. id: order.id,
  370. });
  371. expect(result.order?.state).toBe('PaymentSettled');
  372. });
  373. it('cannot manually transition to PartiallyDelivered', async () => {
  374. const { transitionOrderToState } = await adminClient.query<
  375. AdminTransition.Mutation,
  376. AdminTransition.Variables
  377. >(ADMIN_TRANSITION_TO_STATE, {
  378. id: order.id,
  379. state: 'Delivered',
  380. });
  381. orderErrorGuard.assertErrorResult(transitionOrderToState);
  382. expect(transitionOrderToState!.message).toBe(
  383. 'Cannot transition Order from "PaymentSettled" to "Delivered"',
  384. );
  385. expect(transitionOrderToState!.transitionError).toContain(
  386. 'Cannot transition Order to the "Delivered" state unless all OrderItems are delivered',
  387. );
  388. const result = await adminClient.query<GetOrder.Query, GetOrder.Variables>(GET_ORDER, {
  389. id: order.id,
  390. });
  391. expect(result.order?.state).toBe('PaymentSettled');
  392. });
  393. });
  394. });