mollie-payment.e2e-spec.ts 31 KB

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