payment-process.e2e-spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. /* tslint:disable:no-non-null-assertion */
  2. import {
  3. CustomOrderProcess,
  4. CustomPaymentProcess,
  5. DefaultLogger,
  6. LanguageCode,
  7. mergeConfig,
  8. Order,
  9. OrderPlacedStrategy,
  10. OrderState,
  11. PaymentMethodHandler,
  12. RequestContext,
  13. TransactionalConnection,
  14. } from '@vendure/core';
  15. import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
  16. import gql from 'graphql-tag';
  17. import path from 'path';
  18. import { initialData } from '../../../e2e-common/e2e-initial-data';
  19. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  20. import { ORDER_WITH_LINES_FRAGMENT, PAYMENT_FRAGMENT } from './graphql/fragments';
  21. import * as Codegen from './graphql/generated-e2e-admin-types';
  22. import { ErrorCode } from './graphql/generated-e2e-admin-types';
  23. import * as CodegenShop from './graphql/generated-e2e-shop-types';
  24. import { ADMIN_TRANSITION_TO_STATE, GET_ORDER } from './graphql/shared-definitions';
  25. import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, GET_ACTIVE_ORDER } from './graphql/shop-definitions';
  26. import { proceedToArrangingPayment } from './utils/test-order-utils';
  27. const initSpy = jest.fn();
  28. const transitionStartSpy = jest.fn();
  29. const transitionEndSpy = jest.fn();
  30. const transitionErrorSpy = jest.fn();
  31. const settlePaymentSpy = jest.fn();
  32. describe('Payment process', () => {
  33. let orderId: string;
  34. let payment1Id: string;
  35. const PAYMENT_ERROR_MESSAGE = 'Payment is not valid';
  36. const customPaymentProcess: CustomPaymentProcess<'Validating'> = {
  37. init(injector) {
  38. initSpy(injector.get(TransactionalConnection).rawConnection.name);
  39. },
  40. transitions: {
  41. Created: {
  42. to: ['Validating'],
  43. mergeStrategy: 'merge',
  44. },
  45. Validating: {
  46. to: ['Settled', 'Declined', 'Cancelled'],
  47. },
  48. },
  49. onTransitionStart(fromState, toState, data) {
  50. transitionStartSpy(fromState, toState, data);
  51. if (fromState === 'Validating' && toState === 'Settled') {
  52. if (!data.payment.metadata.valid) {
  53. return PAYMENT_ERROR_MESSAGE;
  54. }
  55. }
  56. },
  57. onTransitionEnd(fromState, toState, data) {
  58. transitionEndSpy(fromState, toState, data);
  59. },
  60. onTransitionError(fromState, toState, message) {
  61. transitionErrorSpy(fromState, toState, message);
  62. },
  63. };
  64. const customOrderProcess: CustomOrderProcess<'ValidatingPayment'> = {
  65. transitions: {
  66. ArrangingPayment: {
  67. to: ['ValidatingPayment'],
  68. mergeStrategy: 'replace',
  69. },
  70. ValidatingPayment: {
  71. to: ['PaymentAuthorized', 'PaymentSettled', 'ArrangingAdditionalPayment'],
  72. },
  73. },
  74. };
  75. const testPaymentHandler = new PaymentMethodHandler({
  76. code: 'test-handler',
  77. description: [{ languageCode: LanguageCode.en, value: 'Test handler' }],
  78. args: {},
  79. createPayment: (ctx, order, amount, args, metadata) => {
  80. return {
  81. state: 'Validating' as any,
  82. amount,
  83. metadata,
  84. };
  85. },
  86. settlePayment: (ctx, order, payment) => {
  87. settlePaymentSpy();
  88. return {
  89. success: true,
  90. };
  91. },
  92. });
  93. class TestOrderPlacedStrategy implements OrderPlacedStrategy {
  94. shouldSetAsPlaced(
  95. ctx: RequestContext,
  96. fromState: OrderState,
  97. toState: OrderState,
  98. order: Order,
  99. ): boolean | Promise<boolean> {
  100. return fromState === 'ArrangingPayment' && toState === ('ValidatingPayment' as any);
  101. }
  102. }
  103. const orderGuard: ErrorResultGuard<CodegenShop.TestOrderFragmentFragment | Codegen.OrderFragment> =
  104. createErrorResultGuard(input => !!input.total);
  105. const paymentGuard: ErrorResultGuard<Codegen.PaymentFragment> = createErrorResultGuard(
  106. input => !!input.id,
  107. );
  108. const { server, adminClient, shopClient } = createTestEnvironment(
  109. mergeConfig(testConfig(), {
  110. logger: new DefaultLogger(),
  111. orderOptions: {
  112. process: [customOrderProcess as any],
  113. orderPlacedStrategy: new TestOrderPlacedStrategy(),
  114. },
  115. paymentOptions: {
  116. paymentMethodHandlers: [testPaymentHandler],
  117. customPaymentProcess: [customPaymentProcess as any],
  118. },
  119. }),
  120. );
  121. beforeAll(async () => {
  122. await server.init({
  123. initialData: {
  124. ...initialData,
  125. paymentMethods: [
  126. {
  127. name: testPaymentHandler.code,
  128. handler: { code: testPaymentHandler.code, arguments: [] },
  129. },
  130. ],
  131. },
  132. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'),
  133. customerCount: 1,
  134. });
  135. await adminClient.asSuperAdmin();
  136. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  137. await shopClient.query<
  138. CodegenShop.AddItemToOrderMutation,
  139. CodegenShop.AddItemToOrderMutationVariables
  140. >(ADD_ITEM_TO_ORDER, {
  141. productVariantId: 'T_1',
  142. quantity: 1,
  143. });
  144. orderId = (await proceedToArrangingPayment(shopClient)) as string;
  145. }, TEST_SETUP_TIMEOUT_MS);
  146. afterAll(async () => {
  147. await server.destroy();
  148. });
  149. it('CustomPaymentProcess is injectable', () => {
  150. expect(initSpy).toHaveBeenCalled();
  151. expect(initSpy.mock.calls[0][0]).toBe('default');
  152. });
  153. it('creates Payment in custom state', async () => {
  154. const { addPaymentToOrder } = await shopClient.query<
  155. CodegenShop.AddPaymentToOrderMutation,
  156. CodegenShop.AddPaymentToOrderMutationVariables
  157. >(ADD_PAYMENT, {
  158. input: {
  159. method: testPaymentHandler.code,
  160. metadata: {
  161. valid: true,
  162. },
  163. },
  164. });
  165. orderGuard.assertSuccess(addPaymentToOrder);
  166. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  167. GET_ORDER,
  168. {
  169. id: orderId,
  170. },
  171. );
  172. expect(order?.state).toBe('ArrangingPayment');
  173. expect(order?.payments?.length).toBe(1);
  174. expect(order?.payments?.[0].state).toBe('Validating');
  175. payment1Id = addPaymentToOrder?.payments?.[0].id!;
  176. });
  177. it('calls transition hooks', async () => {
  178. expect(transitionStartSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'Validating']);
  179. expect(transitionEndSpy.mock.calls[0].slice(0, 2)).toEqual(['Created', 'Validating']);
  180. expect(transitionErrorSpy).not.toHaveBeenCalled();
  181. });
  182. it('Payment next states', async () => {
  183. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  184. GET_ORDER,
  185. {
  186. id: orderId,
  187. },
  188. );
  189. expect(order?.payments?.[0].nextStates).toEqual(['Settled', 'Declined', 'Cancelled']);
  190. });
  191. it('transition Order to custom state, custom OrderPlacedStrategy sets as placed', async () => {
  192. const { activeOrder: activeOrderPre } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  193. GET_ACTIVE_ORDER,
  194. );
  195. expect(activeOrderPre).not.toBeNull();
  196. const { transitionOrderToState } = await adminClient.query<
  197. Codegen.AdminTransitionMutation,
  198. Codegen.AdminTransitionMutationVariables
  199. >(ADMIN_TRANSITION_TO_STATE, {
  200. id: orderId,
  201. state: 'ValidatingPayment',
  202. });
  203. orderGuard.assertSuccess(transitionOrderToState);
  204. expect(transitionOrderToState.state).toBe('ValidatingPayment');
  205. expect(transitionOrderToState?.active).toBe(false);
  206. const { activeOrder: activeOrderPost } = await shopClient.query<CodegenShop.GetActiveOrderQuery>(
  207. GET_ACTIVE_ORDER,
  208. );
  209. expect(activeOrderPost).toBeNull();
  210. });
  211. it('transitionPaymentToState succeeds', async () => {
  212. const { transitionPaymentToState } = await adminClient.query<
  213. Codegen.TransitionPaymentToStateMutation,
  214. Codegen.TransitionPaymentToStateMutationVariables
  215. >(TRANSITION_PAYMENT_TO_STATE, {
  216. id: payment1Id,
  217. state: 'Settled',
  218. });
  219. paymentGuard.assertSuccess(transitionPaymentToState);
  220. expect(transitionPaymentToState.state).toBe('Settled');
  221. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  222. GET_ORDER,
  223. {
  224. id: orderId,
  225. },
  226. );
  227. expect(order?.state).toBe('PaymentSettled');
  228. expect(settlePaymentSpy).toHaveBeenCalled();
  229. });
  230. describe('failing, cancelling, and manually adding a Payment', () => {
  231. let order2Id: string;
  232. let payment2Id: string;
  233. beforeAll(async () => {
  234. await shopClient.asUserWithCredentials('hayden.zieme12@hotmail.com', 'test');
  235. await shopClient.query<
  236. CodegenShop.AddItemToOrderMutation,
  237. CodegenShop.AddItemToOrderMutationVariables
  238. >(ADD_ITEM_TO_ORDER, {
  239. productVariantId: 'T_1',
  240. quantity: 1,
  241. });
  242. order2Id = (await proceedToArrangingPayment(shopClient)) as string;
  243. const { addPaymentToOrder } = await shopClient.query<
  244. CodegenShop.AddPaymentToOrderMutation,
  245. CodegenShop.AddPaymentToOrderMutationVariables
  246. >(ADD_PAYMENT, {
  247. input: {
  248. method: testPaymentHandler.code,
  249. metadata: {
  250. valid: false,
  251. },
  252. },
  253. });
  254. orderGuard.assertSuccess(addPaymentToOrder);
  255. payment2Id = addPaymentToOrder!.payments![0].id;
  256. await adminClient.query<
  257. Codegen.AdminTransitionMutation,
  258. Codegen.AdminTransitionMutationVariables
  259. >(ADMIN_TRANSITION_TO_STATE, {
  260. id: order2Id,
  261. state: 'ValidatingPayment',
  262. });
  263. });
  264. it('attempting to transition payment to settled fails', async () => {
  265. const { transitionPaymentToState } = await adminClient.query<
  266. Codegen.TransitionPaymentToStateMutation,
  267. Codegen.TransitionPaymentToStateMutationVariables
  268. >(TRANSITION_PAYMENT_TO_STATE, {
  269. id: payment2Id,
  270. state: 'Settled',
  271. });
  272. paymentGuard.assertErrorResult(transitionPaymentToState);
  273. expect(transitionPaymentToState.errorCode).toBe(ErrorCode.PAYMENT_STATE_TRANSITION_ERROR);
  274. expect((transitionPaymentToState as any).transitionError).toBe(PAYMENT_ERROR_MESSAGE);
  275. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  276. GET_ORDER,
  277. {
  278. id: order2Id,
  279. },
  280. );
  281. expect(order?.state).toBe('ValidatingPayment');
  282. });
  283. it('cancel failed payment', async () => {
  284. const { transitionPaymentToState } = await adminClient.query<
  285. Codegen.TransitionPaymentToStateMutation,
  286. Codegen.TransitionPaymentToStateMutationVariables
  287. >(TRANSITION_PAYMENT_TO_STATE, {
  288. id: payment2Id,
  289. state: 'Cancelled',
  290. });
  291. paymentGuard.assertSuccess(transitionPaymentToState);
  292. expect(transitionPaymentToState.state).toBe('Cancelled');
  293. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  294. GET_ORDER,
  295. {
  296. id: order2Id,
  297. },
  298. );
  299. expect(order?.state).toBe('ValidatingPayment');
  300. });
  301. it('manually adds payment', async () => {
  302. const { transitionOrderToState } = await adminClient.query<
  303. Codegen.AdminTransitionMutation,
  304. Codegen.AdminTransitionMutationVariables
  305. >(ADMIN_TRANSITION_TO_STATE, {
  306. id: order2Id,
  307. state: 'ArrangingAdditionalPayment',
  308. });
  309. orderGuard.assertSuccess(transitionOrderToState);
  310. const { addManualPaymentToOrder } = await adminClient.query<
  311. Codegen.AddManualPayment2Mutation,
  312. Codegen.AddManualPayment2MutationVariables
  313. >(ADD_MANUAL_PAYMENT, {
  314. input: {
  315. orderId: order2Id,
  316. metadata: {},
  317. method: 'manual payment',
  318. transactionId: '12345',
  319. },
  320. });
  321. orderGuard.assertSuccess(addManualPaymentToOrder);
  322. expect(addManualPaymentToOrder.state).toBe('ArrangingAdditionalPayment');
  323. expect(addManualPaymentToOrder.payments![1].state).toBe('Settled');
  324. expect(addManualPaymentToOrder.payments![1].amount).toBe(addManualPaymentToOrder.totalWithTax);
  325. });
  326. it('transitions Order to PaymentSettled', async () => {
  327. const { transitionOrderToState } = await adminClient.query<
  328. Codegen.AdminTransitionMutation,
  329. Codegen.AdminTransitionMutationVariables
  330. >(ADMIN_TRANSITION_TO_STATE, {
  331. id: order2Id,
  332. state: 'PaymentSettled',
  333. });
  334. orderGuard.assertSuccess(transitionOrderToState);
  335. expect(transitionOrderToState.state).toBe('PaymentSettled');
  336. const { order } = await adminClient.query<Codegen.GetOrderQuery, Codegen.GetOrderQueryVariables>(
  337. GET_ORDER,
  338. {
  339. id: order2Id,
  340. },
  341. );
  342. const settledPaymentAmount = order?.payments
  343. ?.filter(p => p.state === 'Settled')
  344. .reduce((sum, p) => sum + p.amount, 0);
  345. expect(settledPaymentAmount).toBe(order?.totalWithTax);
  346. });
  347. });
  348. });
  349. const TRANSITION_PAYMENT_TO_STATE = gql`
  350. mutation TransitionPaymentToState($id: ID!, $state: String!) {
  351. transitionPaymentToState(id: $id, state: $state) {
  352. ...Payment
  353. ... on ErrorResult {
  354. errorCode
  355. message
  356. }
  357. ... on PaymentStateTransitionError {
  358. transitionError
  359. }
  360. }
  361. }
  362. ${PAYMENT_FRAGMENT}
  363. `;
  364. export const ADD_MANUAL_PAYMENT = gql`
  365. mutation AddManualPayment2($input: ManualPaymentInput!) {
  366. addManualPaymentToOrder(input: $input) {
  367. ...OrderWithLines
  368. ... on ErrorResult {
  369. errorCode
  370. message
  371. }
  372. }
  373. }
  374. ${ORDER_WITH_LINES_FRAGMENT}
  375. `;