payment-process.e2e-spec.ts 14 KB

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