order-process.e2e-spec.ts 15 KB

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