diff --git a/processor/package-lock.json b/processor/package-lock.json index ee5ea33..6723425 100644 --- a/processor/package-lock.json +++ b/processor/package-lock.json @@ -1,12 +1,12 @@ { "name": "shopmacher-mollie-processor", - "version": "0.0.20", + "version": "0.0.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shopmacher-mollie-processor", - "version": "0.0.20", + "version": "0.0.21", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -15,10 +15,10 @@ "@commercetools/sdk-client-v2": "^2.5.0", "@mollie/api-client": "^3.7.0", "@types/uuid": "^10.0.0", - "axios": "^1.7.2", "body-parser": "^1.20.2", "dotenv": "^16.4.5", "express": "^4.19.2", + "node-fetch": "^2.7.0", "proxy-from-env": "^1.1.0", "uuid": "^10.0.0", "validator": "^13.12.0" @@ -28,6 +28,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "^18.19.39", + "@types/node-fetch": "^2.6.11", "@types/validator": "^13.12.0", "@typescript-eslint/eslint-plugin": "7.13.1", "@typescript-eslint/parser": "7.13.1", @@ -1833,6 +1834,16 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -2414,16 +2425,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", diff --git a/processor/package.json b/processor/package.json index b012f75..f5d5df4 100644 --- a/processor/package.json +++ b/processor/package.json @@ -1,7 +1,7 @@ { "name": "shopmacher-mollie-processor", "description": "Integration between commercetools and mollie payment service provider", - "version": "0.0.22", + "version": "0.0.23", "main": "index.js", "private": true, "scripts": { @@ -40,6 +40,7 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.12", "@types/node": "^18.19.39", + "@types/node-fetch": "^2.6.11", "@types/validator": "^13.12.0", "@typescript-eslint/eslint-plugin": "7.13.1", "@typescript-eslint/parser": "7.13.1", @@ -67,10 +68,10 @@ "@commercetools/sdk-client-v2": "^2.5.0", "@mollie/api-client": "^3.7.0", "@types/uuid": "^10.0.0", - "axios": "^1.7.2", "body-parser": "^1.20.2", "dotenv": "^16.4.5", "express": "^4.19.2", + "node-fetch": "^2.7.0", "proxy-from-env": "^1.1.0", "uuid": "^10.0.0", "validator": "^13.12.0" diff --git a/processor/src/mollie/payment.mollie.ts b/processor/src/mollie/payment.mollie.ts index c724834..7c41bbe 100644 --- a/processor/src/mollie/payment.mollie.ts +++ b/processor/src/mollie/payment.mollie.ts @@ -10,10 +10,10 @@ import { import { initMollieClient } from '../client/mollie.client'; import CustomError from '../errors/custom.error'; import { logger } from '../utils/logger.utils'; -import axios, { AxiosError } from 'axios'; import { readConfiguration } from '../utils/config.utils'; import { LIBRARY_NAME, LIBRARY_VERSION } from '../utils/constant.utils'; import { CustomPayment } from '../types/mollie.types'; +import fetch from 'node-fetch'; /** * Creates a Mollie payment using the provided payment parameters. @@ -104,29 +104,47 @@ export const cancelPayment = async (paymentId: string): Promise => { }; export const createPaymentWithCustomMethod = async (paymentParams: PaymentCreateParams): Promise => { + let errorMessage; + try { const { mollie } = readConfiguration(); const headers = { + 'Content-Type': 'application/json', Authorization: `Bearer ${mollie.apiKey}`, versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`, }; - const response = await axios.post('https://api.mollie.com/v2/payments', paymentParams, { headers }); + const response = await fetch('https://api.mollie.com/v2/payments', { + method: 'POST', + headers, + body: JSON.stringify(paymentParams), + }); - return response.data; - } catch (error: unknown) { - let errorMessage; + const data = await response.json(); - if (error instanceof AxiosError) { - errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${error.response?.data?.detail}, field: ${error.response?.data?.field}`; - } else { - errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + if (response.status !== 201) { + if (response.status === 422 || response.status === 503) { + errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${data?.detail}, field: ${data?.field}`; + } else { + errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + } + + logger.error(errorMessage, { + response: data, + }); + + throw new CustomError(400, errorMessage); } - logger.error(errorMessage, { - error, - }); + return data; + } catch (error: unknown) { + if (!errorMessage) { + errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + logger.error(errorMessage, { + error, + }); + } throw new CustomError(400, errorMessage); } diff --git a/processor/tests/mollie/payment.mollie.spec.ts b/processor/tests/mollie/payment.mollie.spec.ts index 4b30ebf..2f26a25 100644 --- a/processor/tests/mollie/payment.mollie.spec.ts +++ b/processor/tests/mollie/payment.mollie.spec.ts @@ -11,7 +11,7 @@ import { logger } from '../../src/utils/logger.utils'; import CustomError from '../../src/errors/custom.error'; import { LIBRARY_NAME, LIBRARY_VERSION } from '../../src/utils/constant.utils'; import { readConfiguration } from '../../src/utils/config.utils'; -import axios, { AxiosError, AxiosResponse } from 'axios'; +import fetch from 'node-fetch'; const mockPaymentsCreate = jest.fn(); const mockPaymentsGet = jest.fn(); @@ -31,11 +31,8 @@ jest.mock('../../src/client/mollie.client', () => ({ })), })); -jest.mock('axios', () => ({ - // @ts-expect-error ignore type error - ...jest.requireActual('axios'), - post: jest.fn(), -})); +// @ts-expect-error: Mock fetch globally +fetch = jest.fn() as jest.Mock; describe('createMolliePayment', () => { afterEach(() => { @@ -119,7 +116,7 @@ describe('createPaymentWithCustomMethod', () => { jest.clearAllMocks(); // Clear all mocks after each test }); - it('should call axios.post with the correct parameters', async () => { + it('should call fetch with the correct parameters', async () => { const paymentParams: PaymentCreateParams = { amount: { value: '10.00', @@ -130,21 +127,34 @@ describe('createPaymentWithCustomMethod', () => { const createPaymentEndpoint = 'https://api.mollie.com/v2/payments'; const headers = { + 'Content-Type': 'application/json', Authorization: `Bearer ${readConfiguration().mollie.apiKey}`, versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`, }; - (axios.post as jest.Mock).mockResolvedValue({ - data: [], - } as never); + (fetch as unknown as jest.Mock).mockImplementation(async () => + Promise.resolve({ + json: () => Promise.resolve({ data: [] }), + headers: new Headers(), + ok: true, + redirected: false, + status: 201, + statusText: 'OK', + url: '', + }), + ); await createPaymentWithCustomMethod(paymentParams); - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(createPaymentEndpoint, paymentParams, { headers }); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(createPaymentEndpoint, { + method: 'POST', + headers, + body: JSON.stringify(paymentParams), + }); }); - it('should throw AxiosError', async () => { + it('should log error if Mollie API returns an error', async () => { const paymentParams: PaymentCreateParams = { amount: { value: '10.00', @@ -155,39 +165,109 @@ describe('createPaymentWithCustomMethod', () => { const createPaymentEndpoint = 'https://api.mollie.com/v2/payments'; const headers = { + 'Content-Type': 'application/json', Authorization: `Bearer ${readConfiguration().mollie.apiKey}`, versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`, }; - const axiosErrorResponse = { + const response = { data: { detail: 'Something went wrong', field: 'amount', }, }; - const errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${axiosErrorResponse.data.detail}, field: ${axiosErrorResponse.data.field}`; - - const axiosError = new AxiosError( - 'Bad request', - 'GeneralError', - undefined, - undefined, - axiosErrorResponse as AxiosResponse, + const errorMessage = `SCTM - createPaymentWithCustomMethod - error: ${response.data.detail}, field: ${response.data.field}`; + + (fetch as unknown as jest.Mock).mockImplementation(async () => + Promise.resolve({ + json: () => + Promise.resolve({ + detail: 'Something went wrong', + field: 'amount', + }), + headers: new Headers(), + ok: false, + redirected: false, + status: 422, + statusText: 'OK', + url: '', + }), ); - (axios.post as jest.Mock).mockImplementation(() => { - throw axiosError; - }); + try { + await createPaymentWithCustomMethod(paymentParams); + } catch (error: unknown) { + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(createPaymentEndpoint, { + method: 'POST', + headers, + body: JSON.stringify(paymentParams), + }); + + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith(errorMessage, { + response: { + detail: 'Something went wrong', + field: 'amount', + }, + }); + + expect(error).toBeInstanceOf(CustomError); + expect((error as CustomError).statusCode).toBe(400); + expect((error as CustomError).message).toBe(errorMessage); + } + }); + + it('should throw a general error when the request failed with status neither 422 nor 503', async () => { + const paymentParams: PaymentCreateParams = { + amount: { + value: '10.00', + currency: 'EUR', + }, + description: 'Test payment', + }; + + const createPaymentEndpoint = 'https://api.mollie.com/v2/payments'; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${readConfiguration().mollie.apiKey}`, + versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`, + }; + + const response = { + detail: 'Something went wrong', + field: 'amount', + extra: 'testing', + title: 'mollie', + }; + + const errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + + (fetch as unknown as jest.Mock).mockImplementation(async () => + Promise.resolve({ + json: () => Promise.resolve(response), + headers: new Headers(), + ok: false, + redirected: false, + status: 500, + statusText: 'Failed', + url: '', + }), + ); try { await createPaymentWithCustomMethod(paymentParams); } catch (error: any) { - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(createPaymentEndpoint, paymentParams, { headers }); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(createPaymentEndpoint, { + method: 'POST', + headers, + body: JSON.stringify(paymentParams), + }); expect(logger.error).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(errorMessage, { error: axiosError }); + expect(logger.error).toHaveBeenCalledWith(errorMessage, { response: response }); expect(error).toBeInstanceOf(CustomError); expect((error as CustomError).statusCode).toBe(400); @@ -195,7 +275,7 @@ describe('createPaymentWithCustomMethod', () => { } }); - it('should throw other exception', async () => { + it('should throw a general error when the an exception is thrown somewhere in the process', async () => { const paymentParams: PaymentCreateParams = { amount: { value: '10.00', @@ -206,23 +286,28 @@ describe('createPaymentWithCustomMethod', () => { const createPaymentEndpoint = 'https://api.mollie.com/v2/payments'; const headers = { + 'Content-Type': 'application/json', Authorization: `Bearer ${readConfiguration().mollie.apiKey}`, versionStrings: `${LIBRARY_NAME}/${LIBRARY_VERSION}`, }; - const generalError = new Error('Bad request'); + const errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + + const generalError = new Error('General error'); - (axios.post as jest.Mock).mockImplementation(() => { + (fetch as unknown as jest.Mock).mockImplementation(async () => { throw generalError; }); try { await createPaymentWithCustomMethod(paymentParams); } catch (error: any) { - expect(axios.post).toHaveBeenCalledTimes(1); - expect(axios.post).toHaveBeenCalledWith(createPaymentEndpoint, paymentParams, { headers }); - - const errorMessage = 'SCTM - createPaymentWithCustomMethod - Failed to create a payment with unknown errors'; + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith(createPaymentEndpoint, { + method: 'POST', + headers, + body: JSON.stringify(paymentParams), + }); expect(logger.error).toHaveBeenCalledTimes(1); expect(logger.error).toHaveBeenCalledWith(errorMessage, { error: generalError });