payment-process.e2e-spec.ts 14 KB

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