mollie-payment.e2e-spec.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. import { OrderStatus } from '@mollie/api-client';
  2. import { ChannelService, LanguageCode, mergeConfig, OrderService, RequestContext } from '@vendure/core';
  3. import {
  4. SettlePaymentMutation,
  5. SettlePaymentMutationVariables,
  6. } from '@vendure/core/e2e/graphql/generated-e2e-admin-types';
  7. import { SETTLE_PAYMENT } from '@vendure/core/e2e/graphql/shared-definitions';
  8. import {
  9. createTestEnvironment,
  10. E2E_DEFAULT_CHANNEL_TOKEN,
  11. SimpleGraphQLClient,
  12. TestServer,
  13. } from '@vendure/testing';
  14. import nock from 'nock';
  15. import fetch from 'node-fetch';
  16. import path from 'path';
  17. import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
  18. import { initialData } from '../../../e2e-common/e2e-initial-data';
  19. import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
  20. import { UPDATE_PRODUCT_VARIANTS } from '../../core/e2e/graphql/shared-definitions';
  21. import { MolliePlugin } from '../src/mollie';
  22. import { molliePaymentHandler } from '../src/mollie/mollie.handler';
  23. import { CREATE_PAYMENT_METHOD, GET_CUSTOMER_LIST, GET_ORDER_PAYMENTS } from './graphql/admin-queries';
  24. import {
  25. CreatePaymentMethodMutation,
  26. CreatePaymentMethodMutationVariables,
  27. GetCustomerListQuery,
  28. GetCustomerListQueryVariables,
  29. } from './graphql/generated-admin-types';
  30. import {
  31. AddItemToOrderMutation,
  32. AddItemToOrderMutationVariables,
  33. GetOrderByCodeQuery,
  34. GetOrderByCodeQueryVariables,
  35. TestOrderFragmentFragment,
  36. } from './graphql/generated-shop-types';
  37. import { ADD_ITEM_TO_ORDER, GET_ORDER_BY_CODE } from './graphql/shop-queries';
  38. import {
  39. addManualPayment,
  40. CREATE_MOLLIE_PAYMENT_INTENT,
  41. GET_MOLLIE_PAYMENT_METHODS,
  42. refundOrderLine,
  43. setShipping,
  44. } from './payment-helpers';
  45. const mockData = {
  46. host: 'https://my-vendure.io',
  47. redirectUrl: 'https://my-storefront/order',
  48. apiKey: 'myApiKey',
  49. methodCode: `mollie-payment-${E2E_DEFAULT_CHANNEL_TOKEN}`,
  50. methodCodeBroken: `mollie-payment-broken-${E2E_DEFAULT_CHANNEL_TOKEN}`,
  51. mollieOrderResponse: {
  52. id: 'ord_mockId',
  53. _links: {
  54. checkout: {
  55. href: 'https://www.mollie.com/payscreen/select-method/mock-payment',
  56. },
  57. },
  58. lines: [],
  59. _embedded: {
  60. payments: [
  61. {
  62. id: 'tr_mockPayment',
  63. status: 'paid',
  64. resource: 'payment',
  65. },
  66. ],
  67. },
  68. resource: 'order',
  69. mode: 'test',
  70. method: 'test-method',
  71. profileId: '123',
  72. settlementAmount: 'test amount',
  73. customerId: '456',
  74. authorizedAt: new Date(),
  75. paidAt: new Date(),
  76. },
  77. molliePaymentMethodsResponse: {
  78. count: 1,
  79. _embedded: {
  80. methods: [
  81. {
  82. resource: 'method',
  83. id: 'ideal',
  84. description: 'iDEAL',
  85. minimumAmount: {
  86. value: '0.01',
  87. currency: 'EUR',
  88. },
  89. maximumAmount: {
  90. value: '50000.00',
  91. currency: 'EUR',
  92. },
  93. image: {
  94. size1x: 'https://www.mollie.com/external/icons/payment-methods/ideal.png',
  95. size2x: 'https://www.mollie.com/external/icons/payment-methods/ideal%402x.png',
  96. svg: 'https://www.mollie.com/external/icons/payment-methods/ideal.svg',
  97. },
  98. _links: {
  99. self: {
  100. href: 'https://api.mollie.com/v2/methods/ideal',
  101. type: 'application/hal+json',
  102. },
  103. },
  104. },
  105. ],
  106. },
  107. _links: {
  108. self: {
  109. href: 'https://api.mollie.com/v2/methods',
  110. type: 'application/hal+json',
  111. },
  112. documentation: {
  113. href: 'https://docs.mollie.com/reference/v2/methods-api/list-methods',
  114. type: 'text/html',
  115. },
  116. },
  117. },
  118. };
  119. let shopClient: SimpleGraphQLClient;
  120. let adminClient: SimpleGraphQLClient;
  121. let server: TestServer;
  122. let started = false;
  123. let customers: GetCustomerListQuery['customers']['items'];
  124. let order: TestOrderFragmentFragment;
  125. let serverPort: number;
  126. const SURCHARGE_AMOUNT = -20000;
  127. describe('Mollie payments (with useDynamicRedirectUrl set to true)', () => {
  128. beforeAll(async () => {
  129. const devConfig = mergeConfig(testConfig(), {
  130. plugins: [MolliePlugin.init({ vendureHost: mockData.host })],
  131. });
  132. const env = createTestEnvironment(devConfig);
  133. serverPort = devConfig.apiOptions.port;
  134. shopClient = env.shopClient;
  135. adminClient = env.adminClient;
  136. server = env.server;
  137. await server.init({
  138. initialData,
  139. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  140. customerCount: 2,
  141. });
  142. started = true;
  143. await adminClient.asSuperAdmin();
  144. ({
  145. customers: { items: customers },
  146. } = await adminClient.query<GetCustomerListQuery, GetCustomerListQueryVariables>(GET_CUSTOMER_LIST, {
  147. options: {
  148. take: 2,
  149. },
  150. }));
  151. }, TEST_SETUP_TIMEOUT_MS);
  152. afterAll(async () => {
  153. await server.destroy();
  154. });
  155. afterEach(async () => {
  156. nock.cleanAll();
  157. });
  158. it('Should start successfully', async () => {
  159. expect(started).toEqual(true);
  160. expect(customers).toHaveLength(2);
  161. });
  162. describe('Payment intent creation', () => {
  163. it('Should prepare an order', async () => {
  164. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  165. const { addItemToOrder } = await shopClient.query<
  166. AddItemToOrderMutation,
  167. AddItemToOrderMutationVariables
  168. >(ADD_ITEM_TO_ORDER, {
  169. productVariantId: 'T_5',
  170. quantity: 10,
  171. });
  172. order = addItemToOrder as TestOrderFragmentFragment;
  173. // Add surcharge
  174. const ctx = new RequestContext({
  175. apiType: 'admin',
  176. isAuthorized: true,
  177. authorizedAsOwnerOnly: false,
  178. channel: await server.app.get(ChannelService).getDefaultChannel(),
  179. });
  180. await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
  181. description: 'Negative test surcharge',
  182. listPrice: SURCHARGE_AMOUNT,
  183. });
  184. expect(order.code).toBeDefined();
  185. });
  186. it('Should add a Mollie paymentMethod', async () => {
  187. const { createPaymentMethod } = await adminClient.query<
  188. CreatePaymentMethodMutation,
  189. CreatePaymentMethodMutationVariables
  190. >(CREATE_PAYMENT_METHOD, {
  191. input: {
  192. code: mockData.methodCode,
  193. enabled: true,
  194. handler: {
  195. code: molliePaymentHandler.code,
  196. arguments: [
  197. { name: 'redirectUrl', value: mockData.redirectUrl },
  198. { name: 'apiKey', value: mockData.apiKey },
  199. { name: 'autoCapture', value: 'false' },
  200. ],
  201. },
  202. translations: [
  203. {
  204. languageCode: LanguageCode.en,
  205. name: 'Mollie payment test',
  206. description: 'This is a Mollie test payment method',
  207. },
  208. ],
  209. },
  210. });
  211. expect(createPaymentMethod.code).toBe(mockData.methodCode);
  212. });
  213. it('Should fail to create payment intent without shippingmethod', async () => {
  214. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  215. const { createMolliePaymentIntent: result } = await shopClient.query(
  216. CREATE_MOLLIE_PAYMENT_INTENT,
  217. {
  218. input: {
  219. paymentMethodCode: mockData.methodCode,
  220. },
  221. },
  222. );
  223. expect(result.errorCode).toBe('ORDER_PAYMENT_STATE_ERROR');
  224. });
  225. it('Should fail to create payment intent with invalid Mollie method', async () => {
  226. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  227. await setShipping(shopClient);
  228. const { createMolliePaymentIntent: result } = await shopClient.query(
  229. CREATE_MOLLIE_PAYMENT_INTENT,
  230. {
  231. input: {
  232. paymentMethodCode: mockData.methodCode,
  233. molliePaymentMethodCode: 'invalid',
  234. },
  235. },
  236. );
  237. expect(result.errorCode).toBe('INELIGIBLE_PAYMENT_METHOD_ERROR');
  238. });
  239. it('Should fail to get payment url when items are out of stock', async () => {
  240. let { updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
  241. input: {
  242. id: 'T_5',
  243. trackInventory: 'TRUE',
  244. outOfStockThreshold: 0,
  245. stockOnHand: 1,
  246. },
  247. });
  248. expect(updateProductVariants[0].stockOnHand).toBe(1);
  249. const { createMolliePaymentIntent: result } = await shopClient.query(
  250. CREATE_MOLLIE_PAYMENT_INTENT,
  251. {
  252. input: {
  253. paymentMethodCode: mockData.methodCode,
  254. },
  255. },
  256. );
  257. expect(result.message).toContain('The following variants are out of stock');
  258. // Set stock back to not tracking
  259. ({ updateProductVariants } = await adminClient.query(UPDATE_PRODUCT_VARIANTS, {
  260. input: {
  261. id: 'T_5',
  262. trackInventory: 'FALSE',
  263. },
  264. }));
  265. expect(updateProductVariants[0].trackInventory).toBe('FALSE');
  266. });
  267. it('Should get payment url without Mollie method', async () => {
  268. let mollieRequest: any | undefined;
  269. nock('https://api.mollie.com/')
  270. .post('/v2/orders', body => {
  271. mollieRequest = body;
  272. return true;
  273. })
  274. .reply(200, mockData.mollieOrderResponse);
  275. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  276. input: {
  277. paymentMethodCode: mockData.methodCode,
  278. },
  279. });
  280. expect(createMolliePaymentIntent).toEqual({
  281. url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
  282. });
  283. expect(mollieRequest?.orderNumber).toEqual(order.code);
  284. expect(mollieRequest?.redirectUrl).toEqual(`${mockData.redirectUrl}/${order.code}`);
  285. expect(mollieRequest?.webhookUrl).toEqual(
  286. `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
  287. );
  288. expect(mollieRequest?.amount?.value).toBe('1009.90');
  289. expect(mollieRequest?.amount?.currency).toBe('USD');
  290. expect(mollieRequest.lines[0].vatAmount.value).toEqual('199.98');
  291. let totalLineAmount = 0;
  292. for (const line of mollieRequest.lines) {
  293. totalLineAmount += Number(line.totalAmount.value);
  294. }
  295. // Sum of lines should equal order total
  296. expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
  297. });
  298. it('Should get payment url with Mollie method', async () => {
  299. nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
  300. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  301. await setShipping(shopClient);
  302. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  303. input: {
  304. paymentMethodCode: mockData.methodCode,
  305. molliePaymentMethodCode: 'ideal',
  306. },
  307. });
  308. expect(createMolliePaymentIntent).toEqual({
  309. url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
  310. });
  311. });
  312. it('Should get payment url with deducted amount if a payment is already made', async () => {
  313. let mollieRequest: any | undefined;
  314. nock('https://api.mollie.com/')
  315. .post('/v2/orders', body => {
  316. mollieRequest = body;
  317. return true;
  318. })
  319. .reply(200, mockData.mollieOrderResponse);
  320. await addManualPayment(server, 1, 10000);
  321. await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  322. input: {
  323. paymentMethodCode: mockData.methodCode,
  324. },
  325. });
  326. expect(mollieRequest.amount?.value).toBe('909.90'); // minus 100,00 from manual payment
  327. let totalLineAmount = 0;
  328. for (const line of mollieRequest?.lines) {
  329. totalLineAmount += Number(line.totalAmount.value);
  330. }
  331. // Sum of lines should equal order total
  332. expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
  333. });
  334. it('Should get available paymentMethods', async () => {
  335. nock('https://api.mollie.com/')
  336. .get('/v2/methods?resource=orders')
  337. .reply(200, mockData.molliePaymentMethodsResponse);
  338. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  339. const { molliePaymentMethods } = await shopClient.query(GET_MOLLIE_PAYMENT_METHODS, {
  340. input: {
  341. paymentMethodCode: mockData.methodCode,
  342. },
  343. });
  344. const method = molliePaymentMethods[0];
  345. expect(method.code).toEqual('ideal');
  346. expect(method.minimumAmount).toBeDefined();
  347. expect(method.maximumAmount).toBeDefined();
  348. expect(method.image).toBeDefined();
  349. });
  350. });
  351. describe('Handle standard payment methods', () => {
  352. it('Should transition to ArrangingPayment when partially paid', async () => {
  353. nock('https://api.mollie.com/')
  354. .get('/v2/orders/ord_mockId')
  355. .reply(200, {
  356. ...mockData.mollieOrderResponse,
  357. // Add a payment of 20.00
  358. amount: { value: '20.00', currency: 'EUR' },
  359. orderNumber: order.code,
  360. status: OrderStatus.paid,
  361. });
  362. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  363. method: 'post',
  364. body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
  365. headers: { 'Content-Type': 'application/json' },
  366. });
  367. const { order: adminOrder } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order?.id });
  368. expect(adminOrder.state).toBe('ArrangingPayment');
  369. });
  370. it('Should place order after paying outstanding amount', async () => {
  371. nock('https://api.mollie.com/')
  372. .get('/v2/orders/ord_mockId')
  373. .reply(200, {
  374. ...mockData.mollieOrderResponse,
  375. // Add a payment of 1089.90
  376. amount: { value: '1089.90', currency: 'EUR' }, // 1109.90 minus the previously paid 20.00
  377. orderNumber: order.code,
  378. status: OrderStatus.paid,
  379. });
  380. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  381. method: 'post',
  382. body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
  383. headers: { 'Content-Type': 'application/json' },
  384. });
  385. const { orderByCode } = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
  386. GET_ORDER_BY_CODE,
  387. {
  388. code: order.code,
  389. },
  390. );
  391. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  392. order = orderByCode!;
  393. expect(order.state).toBe('PaymentSettled');
  394. });
  395. it('Should have Mollie metadata on payment', async () => {
  396. const {
  397. order: { payments },
  398. } = await adminClient.query(GET_ORDER_PAYMENTS, { id: order.id });
  399. const metadata = payments[1].metadata;
  400. expect(metadata.mode).toBe(mockData.mollieOrderResponse.mode);
  401. expect(metadata.method).toBe(mockData.mollieOrderResponse.method);
  402. expect(metadata.profileId).toBe(mockData.mollieOrderResponse.profileId);
  403. expect(metadata.authorizedAt).toEqual(mockData.mollieOrderResponse.authorizedAt.toISOString());
  404. expect(metadata.paidAt).toEqual(mockData.mollieOrderResponse.paidAt.toISOString());
  405. });
  406. it('Should fail to refund', async () => {
  407. nock('https://api.mollie.com/')
  408. .get('/v2/orders/ord_mockId?embed=payments')
  409. .reply(200, mockData.mollieOrderResponse);
  410. nock('https://api.mollie.com/')
  411. .post('/v2/payments/tr_mockPayment/refunds')
  412. .reply(200, { status: 'failed', resource: 'payment' });
  413. const refund = await refundOrderLine(
  414. adminClient,
  415. order.lines[0].id,
  416. 1,
  417. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  418. order!.payments[1].id,
  419. SURCHARGE_AMOUNT,
  420. );
  421. expect(refund.state).toBe('Failed');
  422. });
  423. it('Should successfully refund the Mollie payment', async () => {
  424. let mollieRequest;
  425. nock('https://api.mollie.com/')
  426. .get('/v2/orders/ord_mockId?embed=payments')
  427. .reply(200, mockData.mollieOrderResponse);
  428. nock('https://api.mollie.com/')
  429. .post('/v2/payments/tr_mockPayment/refunds', body => {
  430. mollieRequest = body;
  431. return true;
  432. })
  433. .reply(200, { status: 'pending', resource: 'payment' });
  434. const refund = await refundOrderLine(
  435. adminClient,
  436. order.lines[0].id,
  437. 10,
  438. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  439. order.payments!.find(p => p.amount === 108990)!.id,
  440. SURCHARGE_AMOUNT,
  441. );
  442. expect(mollieRequest?.amount.value).toBe('999.90'); // Only refund mollie amount, not the gift card
  443. expect(refund.total).toBe(99990);
  444. expect(refund.state).toBe('Settled');
  445. });
  446. });
  447. describe('Handle pay-later methods', () => {
  448. // TODO: Add testcases that mock incoming webhook to: 1. Authorize payment and 2. AutoCapture payments
  449. it('Should prepare a new order', async () => {
  450. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  451. const { addItemToOrder } = await shopClient.query<
  452. AddItemToOrderMutation,
  453. AddItemToOrderMutationVariables
  454. >(ADD_ITEM_TO_ORDER, {
  455. productVariantId: 'T_1',
  456. quantity: 2,
  457. });
  458. order = addItemToOrder as TestOrderFragmentFragment;
  459. await setShipping(shopClient);
  460. expect(order.code).toBeDefined();
  461. });
  462. it('Should authorize payment for pay-later payment methods', async () => {
  463. nock('https://api.mollie.com/')
  464. .get('/v2/orders/ord_mockId')
  465. .reply(200, {
  466. ...mockData.mollieOrderResponse,
  467. amount: { value: '3127.60', currency: 'EUR' },
  468. orderNumber: order.code,
  469. status: OrderStatus.authorized,
  470. });
  471. await fetch(`http://localhost:${serverPort}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`, {
  472. method: 'post',
  473. body: JSON.stringify({ id: mockData.mollieOrderResponse.id }),
  474. headers: { 'Content-Type': 'application/json' },
  475. });
  476. const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
  477. GET_ORDER_BY_CODE,
  478. {
  479. code: order.code,
  480. },
  481. );
  482. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  483. order = orderByCode!;
  484. expect(order.state).toBe('PaymentAuthorized');
  485. });
  486. it('Should settle payment via settlePayment mutation', async () => {
  487. // Mock the getOrder Mollie call
  488. nock('https://api.mollie.com/')
  489. .get('/v2/orders/ord_mockId')
  490. .reply(200, {
  491. ...mockData.mollieOrderResponse,
  492. orderNumber: order.code,
  493. status: OrderStatus.authorized,
  494. });
  495. // Mock the createShipment call
  496. let createShipmentBody;
  497. nock('https://api.mollie.com/')
  498. .post('/v2/orders/ord_mockId/shipments', body => {
  499. createShipmentBody = body;
  500. return true;
  501. })
  502. .reply(200, { resource: 'shipment', lines: [] });
  503. const { settlePayment } = await adminClient.query<
  504. SettlePaymentMutation,
  505. SettlePaymentMutationVariables
  506. >(SETTLE_PAYMENT, {
  507. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  508. id: order.payments![0].id,
  509. });
  510. const { orderByCode } = await shopClient.query<GetOrderByCodeQuery, GetOrderByCodeQueryVariables>(
  511. GET_ORDER_BY_CODE,
  512. {
  513. code: order.code,
  514. },
  515. );
  516. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  517. order = orderByCode!;
  518. expect(createShipmentBody).toBeDefined();
  519. expect(order.state).toBe('PaymentSettled');
  520. });
  521. });
  522. it('Should add an unusable Mollie paymentMethod (missing redirectUrl)', async () => {
  523. const { createPaymentMethod } = await adminClient.query<
  524. CreatePaymentMethod.Mutation,
  525. CreatePaymentMethod.Variables
  526. >(CREATE_PAYMENT_METHOD, {
  527. input: {
  528. code: mockData.methodCodeBroken,
  529. enabled: true,
  530. handler: {
  531. code: molliePaymentHandler.code,
  532. arguments: [
  533. { name: 'apiKey', value: mockData.apiKey },
  534. { name: 'autoCapture', value: 'false' },
  535. ],
  536. },
  537. translations: [
  538. {
  539. languageCode: LanguageCode.en,
  540. name: 'Mollie payment test',
  541. description: 'This is a Mollie test payment method',
  542. },
  543. ],
  544. },
  545. });
  546. expect(createPaymentMethod.code).toBe(mockData.methodCodeBroken);
  547. });
  548. it('Should prepare an order', async () => {
  549. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  550. const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
  551. ADD_ITEM_TO_ORDER,
  552. {
  553. productVariantId: 'T_5',
  554. quantity: 10,
  555. },
  556. );
  557. order = addItemToOrder as TestOrderFragmentFragment;
  558. // Add surcharge
  559. const ctx = new RequestContext({
  560. apiType: 'admin',
  561. isAuthorized: true,
  562. authorizedAsOwnerOnly: false,
  563. channel: await server.app.get(ChannelService).getDefaultChannel(),
  564. });
  565. await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
  566. description: 'Negative test surcharge',
  567. listPrice: SURCHARGE_AMOUNT,
  568. });
  569. expect(order.code).toBeDefined();
  570. });
  571. it('Should fail to get payment url with Mollie method without redirectUrl configured', async () => {
  572. nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
  573. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  574. await setShipping(shopClient);
  575. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  576. input: {
  577. paymentMethodCode: mockData.methodCodeBroken,
  578. molliePaymentMethodCode: 'ideal',
  579. },
  580. });
  581. expect(createMolliePaymentIntent.message).toContain(
  582. 'Cannot create payment intent without redirectUrl specified in paymentMethod',
  583. );
  584. });
  585. });
  586. describe('Mollie payments (with useDynamicRedirectUrl set to true)', () => {
  587. beforeAll(async () => {
  588. const devConfig = mergeConfig(testConfig(), {
  589. plugins: [MolliePlugin.init({ vendureHost: mockData.host, useDynamicRedirectUrl: true })],
  590. });
  591. const env = createTestEnvironment(devConfig);
  592. serverPort = devConfig.apiOptions.port;
  593. shopClient = env.shopClient;
  594. adminClient = env.adminClient;
  595. server = env.server;
  596. await server.init({
  597. initialData,
  598. productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
  599. customerCount: 2,
  600. });
  601. started = true;
  602. await adminClient.asSuperAdmin();
  603. ({
  604. customers: { items: customers },
  605. } = await adminClient.query<GetCustomerList.Query, GetCustomerList.Variables>(GET_CUSTOMER_LIST, {
  606. options: {
  607. take: 2,
  608. },
  609. }));
  610. }, TEST_SETUP_TIMEOUT_MS);
  611. afterAll(async () => {
  612. await server.destroy();
  613. });
  614. afterEach(async () => {
  615. nock.cleanAll();
  616. });
  617. it('Should start successfully', async () => {
  618. expect(started).toEqual(true);
  619. expect(customers).toHaveLength(2);
  620. });
  621. it('Should prepare an order', async () => {
  622. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  623. const { addItemToOrder } = await shopClient.query<AddItemToOrder.Mutation, AddItemToOrder.Variables>(
  624. ADD_ITEM_TO_ORDER,
  625. {
  626. productVariantId: 'T_5',
  627. quantity: 10,
  628. },
  629. );
  630. order = addItemToOrder as TestOrderFragmentFragment;
  631. // Add surcharge
  632. const ctx = new RequestContext({
  633. apiType: 'admin',
  634. isAuthorized: true,
  635. authorizedAsOwnerOnly: false,
  636. channel: await server.app.get(ChannelService).getDefaultChannel(),
  637. });
  638. await server.app.get(OrderService).addSurchargeToOrder(ctx, 1, {
  639. description: 'Negative test surcharge',
  640. listPrice: SURCHARGE_AMOUNT,
  641. });
  642. expect(order.code).toBeDefined();
  643. });
  644. it('Should add a working Mollie paymentMethod without specifying redirectUrl', async () => {
  645. const { createPaymentMethod } = await adminClient.query<
  646. CreatePaymentMethod.Mutation,
  647. CreatePaymentMethod.Variables
  648. >(CREATE_PAYMENT_METHOD, {
  649. input: {
  650. code: mockData.methodCode,
  651. enabled: true,
  652. handler: {
  653. code: molliePaymentHandler.code,
  654. arguments: [
  655. { name: 'apiKey', value: mockData.apiKey },
  656. { name: 'autoCapture', value: 'false' },
  657. ],
  658. },
  659. translations: [
  660. {
  661. languageCode: LanguageCode.en,
  662. name: 'Mollie payment test',
  663. description: 'This is a Mollie test payment method',
  664. },
  665. ],
  666. },
  667. });
  668. expect(createPaymentMethod.code).toBe(mockData.methodCode);
  669. });
  670. it('Should get payment url without Mollie method', async () => {
  671. await setShipping(shopClient);
  672. let mollieRequest: any | undefined;
  673. nock('https://api.mollie.com/')
  674. .post('/v2/orders', body => {
  675. mollieRequest = body;
  676. return true;
  677. })
  678. .reply(200, mockData.mollieOrderResponse);
  679. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  680. input: {
  681. paymentMethodCode: mockData.methodCode,
  682. redirectUrl: mockData.redirectUrl,
  683. },
  684. });
  685. expect(createMolliePaymentIntent).toEqual({
  686. url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
  687. });
  688. expect(mollieRequest?.orderNumber).toEqual(order.code);
  689. expect(mollieRequest?.redirectUrl).toEqual(mockData.redirectUrl);
  690. expect(mollieRequest?.webhookUrl).toEqual(
  691. `${mockData.host}/payments/mollie/${E2E_DEFAULT_CHANNEL_TOKEN}/1`,
  692. );
  693. expect(mollieRequest?.amount?.value).toBe('1009.90');
  694. expect(mollieRequest?.amount?.currency).toBe('USD');
  695. expect(mollieRequest.lines[0].vatAmount.value).toEqual('199.98');
  696. let totalLineAmount = 0;
  697. for (const line of mollieRequest.lines) {
  698. totalLineAmount += Number(line.totalAmount.value);
  699. }
  700. // Sum of lines should equal order total
  701. expect(mollieRequest.amount.value).toEqual(totalLineAmount.toFixed(2));
  702. });
  703. it('Should get payment url with Mollie method', async () => {
  704. nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
  705. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  706. await setShipping(shopClient);
  707. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  708. input: {
  709. paymentMethodCode: mockData.methodCode,
  710. molliePaymentMethodCode: 'ideal',
  711. redirectUrl: mockData.redirectUrl,
  712. },
  713. });
  714. expect(createMolliePaymentIntent).toEqual({
  715. url: 'https://www.mollie.com/payscreen/select-method/mock-payment',
  716. });
  717. });
  718. it('Should fail to get payment url without specifying redirectUrl in the createMolliePaymentIntent mutation', async () => {
  719. nock('https://api.mollie.com/').post('/v2/orders').reply(200, mockData.mollieOrderResponse);
  720. await shopClient.asUserWithCredentials(customers[0].emailAddress, 'test');
  721. await setShipping(shopClient);
  722. const { createMolliePaymentIntent } = await shopClient.query(CREATE_MOLLIE_PAYMENT_INTENT, {
  723. input: {
  724. paymentMethodCode: mockData.methodCode,
  725. molliePaymentMethodCode: 'ideal',
  726. },
  727. });
  728. expect(createMolliePaymentIntent.message).toContain(
  729. 'Cannot create payment intent without redirectUrl specified',
  730. );
  731. });
  732. });