payment-process.e2e-spec.ts 14 KB

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