diff --git a/README.md b/README.md index 4ac9674fa..f1e1e0a0e 100644 --- a/README.md +++ b/README.md @@ -89,10 +89,10 @@ Check for breaking changes on the [releases page](https://github.com/Adyen/adyen ``` javascript // Step 1: Require the parts of the module you want to use -const { Client, CheckoutAPI} = require('@adyen/api-library'); +const { Client, CheckoutAPI, EnvironmentEnum} = require('@adyen/api-library'); // Step 2: Initialize the client object -const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST"}); +const client = new Client({apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST}); // Step 3: Initialize the API object const checkoutApi = new CheckoutAPI(client); @@ -143,7 +143,7 @@ Use the Node.js `require` function to load the `Client` and API objects from the For example, to use the [Checkout API](https://docs.adyen.com/api-explorer/Checkout/70/overview): ``` javascript -const { Client, CheckoutAPI} = require('@adyen/api-library'); +const { Client, CheckoutAPI, EnvironmentEnum} = require('@adyen/api-library'); ``` ### Step 2: Initialize the client object @@ -155,7 +155,7 @@ Initialize the client object, passing the following: For example: ``` javascript -const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST"}); +const client = new Client({apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.TEST}); ``` ### Step 3: Initialize the API object @@ -208,9 +208,9 @@ checkoutApi.PaymentsApi.payments(paymentRequest) For APIS that require your [Live URL Prefix](https://docs.adyen.com/development-resources/live-endpoints#live-url-prefix) (Binlookup, BalanceControl, Checkout, Payout and Recurring) the client is set up as follows in order to start processing live payments: ``` typescript -const { Client } = require('@adyen/api-library'); +const { Client, EnvironmentEnum } = require('@adyen/api-library'); -const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST", liveEndpointUrlPrefix: "YOUR_LIVE_URL_PREFIX"}); +const client = new Client({apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.LIVE, liveEndpointUrlPrefix: "YOUR_LIVE_URL_PREFIX"}); ``` ### Usage in TypeScript @@ -218,8 +218,8 @@ const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST", liveEndp Alternatively, you can use the `Types` included in this module for Typescript and `async` syntax. ``` typescript - const { Client, CheckoutAPI, Types } = require('@adyen/api-library'); - const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST"}); + const { Client, EnvironmentEnum, CheckoutAPI, Types } = require('@adyen/api-library'); + const client = new Client({apiKey: "YOUR_API_KEY", environment: EnvironmentEnum.LIVE, liveEndpointUrlPrefix: "YOUR_LIVE_URL_PREFIX"}); const makePaymentsRequest = async () => { const paymentsRequest : Types.checkout.PaymentRequest = { @@ -265,6 +265,8 @@ const paymentRequest: checkout.PaymentRequest = await checkout.ObjectSerializer. By default, [Node.js https](https://nodejs.org/api/https.html) is used to make API requests. Alternatively, you can set a custom `HttpClient` for your `Client` object. +**Note**: when using your custom `HttpClient`, you must define all required properties (API key, content-type, timeouts, etc..) + For example, to set `axios` as your HTTP client: ``` javascript @@ -330,20 +332,26 @@ const client = new Client({ config }); const httpClient = new HttpURLConnectionClient(); httpClient.proxy = { host: "http://google.com", port: 8888, }; -client.setEnvironment('TEST'); +client.setEnvironment(EnvironmentEnum.TEST); client.httpClient = httpClient; // ... more code ``` -### Using the Cloud Terminal API Integration -In order to submit In-Person requests with [Terminal API over Cloud](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/) you need to initialize the client in a similar way as the steps listed above for Ecommerce transactions, but make sure to include `TerminalCloudAPI`: +### Using the Cloud Terminal API +For In-Person Payments integrations with the [Cloud Terminal API](https://docs.adyen.com/point-of-sale/design-your-integration/choose-your-architecture/cloud/), you must initialise the Client **setting the closest** [Region](https://docs.adyen.com/point-of-sale/design-your-integration/terminal-api/#cloud): ``` javascript // Step 1: Require the parts of the module you want to use const {Client, TerminalCloudAPI} from "@adyen/api-library"; +const { Config, EnvironmentEnum, RegionEnum } = require("@adyen/api-library"); // Step 2: Initialize the client object -const client = new Client({apiKey: "YOUR_API_KEY", environment: "TEST"}); +const config = new Config({ + apiKey: "YOUR_API_KEY", + environment: EnvironmentEnum.LIVE, + region: RegionEnum.US +}); +const client = new Client(config); // Step 3: Initialize the API object const terminalCloudAPI = new TerminalCloudAPI(client); diff --git a/src/__mocks__/base.ts b/src/__mocks__/base.ts index 206268a43..047aeae13 100644 --- a/src/__mocks__/base.ts +++ b/src/__mocks__/base.ts @@ -18,7 +18,7 @@ */ import Client from "../client"; -import Config from "../config"; +import Config, { EnvironmentEnum } from "../config"; import { AmountsReq, MessageCategoryType, @@ -37,18 +37,19 @@ import { export const createClient = (apiKey = process.env.ADYEN_API_KEY): Client => { const config: Config = new Config(); + config.environment = EnvironmentEnum.TEST; config.terminalApiCloudEndpoint = Client.TERMINAL_API_ENDPOINT_TEST; config.terminalApiLocalEndpoint = "https://mocked_local_endpoint.com"; config.marketPayEndpoint = Client.MARKETPAY_ENDPOINT_TEST; config.apiKey = apiKey == null ? "apiKey" : apiKey; - return new Client({ config }); + return new Client(config); }; export const createBasicAuthClient = (): Client => { return new Client({ username: process.env.ADYEN_USER!, password: process.env.ADYEN_PASSWORD!, - environment: "TEST", + environment: EnvironmentEnum.TEST, applicationName: "adyen-node-api-library" }); }; diff --git a/src/__tests__/checkout.spec.ts b/src/__tests__/checkout.spec.ts index fce542872..e36db0320 100644 --- a/src/__tests__/checkout.spec.ts +++ b/src/__tests__/checkout.spec.ts @@ -16,6 +16,7 @@ import { checkout } from "../typings"; import { IRequest } from "../typings/requestOptions"; import { SessionResultResponse } from "../typings/checkout/sessionResultResponse"; import { payments3DS2NativeAction } from "../__mocks__/checkout/payments3DS2NativeAction"; +import { EnvironmentEnum } from "../config"; const merchantAccount = process.env.ADYEN_MERCHANT!; const reference = "Your order number"; @@ -384,14 +385,14 @@ describe("Checkout", (): void => { // }); test("should have missing identifier on live", async (): Promise => { - client.setEnvironment("LIVE"); + client.config.environment = EnvironmentEnum.LIVE; try { const liveCheckout = new CheckoutAPI(client); await liveCheckout.PaymentsApi.payments(createPaymentsCheckoutRequest()); fail(); } catch (e) { if(e instanceof Error) { - expect(e.message).toEqual("Please provide your unique live url prefix on the setEnvironment() call on the Client."); + expect(e.message).toEqual("Live endpoint URL prefix must be provided for LIVE environment."); } else { fail(); diff --git a/src/__tests__/client.spec.ts b/src/__tests__/client.spec.ts index 4ecbf683e..3fccc952f 100644 --- a/src/__tests__/client.spec.ts +++ b/src/__tests__/client.spec.ts @@ -1,21 +1,23 @@ import Client from "../client"; +import Config, { EnvironmentEnum, RegionEnum } from "../config"; describe("API Client", function (): void { test("should be able to make a request using basic auth", async function (): Promise { new Client({ username: process.env.ADYEN_USER!, password: process.env.ADYEN_PASSWORD!, - environment: "TEST" + environment: EnvironmentEnum.TEST }); }); test("should create client with API key", () => { const client = new Client({ apiKey: "ADYEN_API_KEY", - environment: "TEST" + environment: EnvironmentEnum.TEST }); expect(client.config.apiKey).toBe("ADYEN_API_KEY"); + expect(client.config.environment).toBe(EnvironmentEnum.TEST); expect(client.config.environment).toBe("TEST"); expect(client.config.marketPayEndpoint).toBe(Client.MARKETPAY_ENDPOINT_TEST); }); @@ -24,31 +26,134 @@ describe("API Client", function (): void { const client = new Client({ username: "username", password: "password", - environment: "TEST" + environment: EnvironmentEnum.TEST }); expect(client.config.username).toBe("username"); expect(client.config.password).toBe("password"); - expect(client.config.environment).toBe("TEST"); + expect(client.config.environment).toBe(EnvironmentEnum.TEST); }); test("should set application name", () => { const client = new Client({ apiKey: "ADYEN_API_KEY", - environment: "TEST", + environment: EnvironmentEnum.TEST, applicationName: "my_application_name" }); expect(client.config.applicationName).toBe("my_application_name"); }); + test("should define timeout in Config", () => { + const client = new Client({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.TEST, + connectionTimeoutMillis: 30000 + }); + + expect(client.config.connectionTimeoutMillis).toBe(30000); + }); + test("should set timeout", () => { const client = new Client({ apiKey: "ADYEN_API_KEY", - environment: "TEST" + environment: EnvironmentEnum.TEST }); client.setTimeouts(30000); expect(client.config.connectionTimeoutMillis).toBe(30000); }); + + test("should throw error if environment is not defined", () => { + expect(() => { + new Client({ apiKey: "ADYEN_API_KEY" } as any); + }).toThrow("Environment must be defined"); + }); + + test("should throw error if environment is LIVE and region is invalid", () => { + const config = new Config({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.LIVE, + region: "INVALID" as RegionEnum, + liveEndpointUrlPrefix: "prefix" + }); + expect(() => { + new Client(config); + }).toThrow("Invalid region provided: INVALID"); + }); + + test("should set terminalApiCloudEndpoint for TEST region", () => { + const config = new Config({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.TEST + }); + const client = new Client(config); + expect(client.config.terminalApiCloudEndpoint).toBeDefined(); + expect(client.config.terminalApiCloudEndpoint).toBe("https://terminal-api-test.adyen.com"); + }); + + test("should set terminalApiCloudEndpoint for LIVE region", () => { + const config = new Config({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.LIVE, + region: RegionEnum.US, + liveEndpointUrlPrefix: "prefix" + }); + const client = new Client(config); + expect(client.config.terminalApiCloudEndpoint).toBeDefined(); + expect(client.config.terminalApiCloudEndpoint).toBe("https://terminal-api-us.adyen.com"); + }); + + test("should set and get custom http client", () => { + const config = new Config({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.TEST + }); + const client = new Client(config); + const mockHttpClient = { request: jest.fn() }; + client.httpClient = mockHttpClient as any; + expect(client.httpClient).toBe(mockHttpClient); + }); + + test("should set application name via setApplicationName", () => { + const config = new Config({ + apiKey: "ADYEN_API_KEY", + environment: EnvironmentEnum.TEST + }); + const client = new Client(config); + client.setApplicationName("test_app"); + expect(client.config.applicationName).toBe("test_app"); + }); + + test("should return true for valid environments", () => { + expect(Config.isEnvironmentValid(EnvironmentEnum.LIVE)).toBe(true); + expect(Config.isEnvironmentValid(EnvironmentEnum.TEST)).toBe(true); + }); + + test("should return false for invalid environments", () => { + // @ts-expect-error purposely passing invalid value + expect(Config.isEnvironmentValid("INVALID")).toBe(false); + // @ts-expect-error purposely passing undefined + expect(Config.isEnvironmentValid(undefined)).toBe(false); + // @ts-expect-error purposely passing null + expect(Config.isEnvironmentValid(null)).toBe(false); + }); + + test("should return true for valid regions", () => { + expect(Config.isRegionValid(RegionEnum.EU)).toBe(true); + expect(Config.isRegionValid(RegionEnum.AU)).toBe(true); + expect(Config.isRegionValid(RegionEnum.US)).toBe(true); + expect(Config.isRegionValid(RegionEnum.APSE)).toBe(true); + }); + + test("should return false for invalid regions", () => { + // @ts-expect-error purposely passing invalid value + expect(Config.isRegionValid("INVALID")).toBe(false); + // @ts-expect-error purposely passing undefined + expect(Config.isRegionValid(undefined)).toBe(false); + // @ts-expect-error purposely passing null + expect(Config.isRegionValid(null)).toBe(false); + }); + }); + diff --git a/src/__tests__/httpClient.spec.ts b/src/__tests__/httpClient.spec.ts index fa58e61d8..1bc2c635a 100644 --- a/src/__tests__/httpClient.spec.ts +++ b/src/__tests__/httpClient.spec.ts @@ -7,7 +7,7 @@ import HttpClientException from "../httpClient/httpClientException"; import { binlookup } from "../typings"; import { ApiConstants } from "../constants/apiConstants"; import { paymentMethodsSuccess } from "../__mocks__/checkout/paymentMethodsSuccess"; -import Config from "../config"; +import Config, { EnvironmentEnum } from "../config"; import LibraryConstants from "../constants/libraryConstants"; @@ -28,7 +28,7 @@ const threeDSAvailabilitySuccess = { type errorType = "HttpClientException" | "ApiException"; type testOptions = { errorType: errorType; errorMessageContains?: string; errorMessageEquals?: string }; -const getResponse = async ({ apiKey, environment }: { apiKey: string; environment: Environment }, cb: (scope: Interceptor) => testOptions): Promise => { +const getResponse = async ({ apiKey, environment }: { apiKey: string; environment: EnvironmentEnum }, cb: (scope: Interceptor) => testOptions): Promise => { const client = new Client({ apiKey, environment }); const binLookup = new BinLookupAPI(client); @@ -55,7 +55,7 @@ describe("HTTP Client", function (): void { test("Should return ApiException with message containing 'mocked_error_response'", async () => { await getResponse( - { apiKey: "", environment: "TEST" }, + { apiKey: "", environment: EnvironmentEnum.TEST }, (scope) => { scope.replyWithError("mocked_error_response"); return { @@ -69,7 +69,7 @@ describe("HTTP Client", function (): void { test("Should return ApiException with message equal to 'some_error'", async () => { await getResponse( - { apiKey: "MOCKED_API_KEY", environment: "TEST" }, + { apiKey: "MOCKED_API_KEY", environment: EnvironmentEnum.TEST }, (scope) => { scope.replyWithError("some_error"); return { @@ -83,7 +83,7 @@ describe("HTTP Client", function (): void { test("Should return HttpClientException with message equal to 'HTTP Exception: 401. null: Invalid Request'", async () => { await getResponse( - { apiKey: "API_KEY", environment: "TEST" }, + { apiKey: "API_KEY", environment: EnvironmentEnum.TEST }, (scope) => { scope.reply(401, { status: 401, message: "Invalid Request", errorCode: "171", errorType: "validationError" }); return { @@ -97,7 +97,7 @@ describe("HTTP Client", function (): void { test("Should return HttpClientException with message equal to 'HTTP Exception: 401. null'", async () => { await getResponse( - { apiKey: "API_KEY", environment: "TEST" }, + { apiKey: "API_KEY", environment: EnvironmentEnum.TEST }, (scope) => { scope.reply(401, {}); return { @@ -111,7 +111,7 @@ describe("HTTP Client", function (): void { test("Should return HttpClientException with message starting with 'HTTP Exception: 401'", async () => { await getResponse( - { apiKey: "API_KEY", environment: "TEST" }, + { apiKey: "API_KEY", environment: EnvironmentEnum.TEST }, (scope) => { scope.reply(401, "fail"); return { diff --git a/src/__tests__/service.spec.ts b/src/__tests__/service.spec.ts new file mode 100644 index 000000000..670d04ac5 --- /dev/null +++ b/src/__tests__/service.spec.ts @@ -0,0 +1,134 @@ +import Service from "../service"; +import Client from "../client"; +import Config, { EnvironmentEnum } from "../config"; + +class TestService extends Service { + public constructor(client: Client) { + super(client); + } + public testCreateBaseUrl(url: string): string { + return this.createBaseUrl(url); + } +} + +describe("Service", () => { + let client: Client; + + beforeEach(() => { + // Default config for each test + client = new Client(new Config({ + environment: EnvironmentEnum.TEST + })); + }); + + it("should replace -live with -test for non-LIVE environments", () => { + const service = new TestService(client); + const url = "https://pal-live.adyen.com/pal/servlet/"; + expect(service.testCreateBaseUrl(url)).toBe("https://pal-test.adyen.com/pal/servlet/"); + }); + + it("should throw error if liveEndpointUrlPrefix is undefined in LIVE environment", () => { + // create Client for TEST environment without liveEndpointUrlPrefix + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.TEST + }); + client = new Client(config); + // change to LIVE + client.config.environment = EnvironmentEnum.LIVE; + + const service = new TestService(client); + expect(() => service.testCreateBaseUrl("https://pal-live.adyen.com/pal/servlet/")) + .toThrow("Live endpoint URL prefix must be provided for LIVE environment."); + }); + + it("should throw error if liveEndpointUrlPrefix is empty for pal- URLs", () => { + // create Client for TEST environment without liveEndpointUrlPrefix + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.TEST, + liveEndpointUrlPrefix: "" + }); + client = new Client(config); + // change to LIVE + client.config.environment = EnvironmentEnum.LIVE; + + const service = new TestService(client); + expect(() => service.testCreateBaseUrl("https://pal-live.adyen.com/pal/servlet/")) + .toThrow("Live endpoint URL prefix must be provided for LIVE environment."); + }); + + it("should replace pal-test with pal-live using liveEndpointUrlPrefix", () => { + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.LIVE, + liveEndpointUrlPrefix: "mycompany" + }); + client = new Client(config); + + const service = new TestService(client); + const url = "https://pal-test.adyen.com/pal/servlet/"; + expect(service.testCreateBaseUrl(url)).toBe( + "https://mycompany-pal-live.adyenpayments.com/pal/servlet/" + ); + }); + + it("should throw error if liveEndpointUrlPrefix is empty for checkout- URLs", () => { + // create Client for TEST environment without liveEndpointUrlPrefix + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.TEST, + liveEndpointUrlPrefix: "" + }); + client = new Client(config); + // change to LIVE + client.config.environment = EnvironmentEnum.LIVE; + + const service = new TestService(client); + expect(() => service.testCreateBaseUrl("https://checkout-test.adyen.com/")) + .toThrow("Live endpoint URL prefix must be provided for LIVE environment."); + }); + + it("should replace checkout-test with checkout-live using liveEndpointUrlPrefix", () => { + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.LIVE, + liveEndpointUrlPrefix: "mycompany" + }); + client = new Client(config); + + const service = new TestService(client); + const url = "https://checkout-test.adyen.com/"; + expect(service.testCreateBaseUrl(url)).toBe( + "https://mycompany-checkout-live.adyenpayments.com/checkout/" + ); + }); + + it("should replace checkout-test with checkout-live for possdk/v68 using liveEndpointUrlPrefix", () => { + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.LIVE, + liveEndpointUrlPrefix: "mycompany" + }); + client = new Client(config); + + const service = new TestService(client); + const url = "https://checkout-test.adyen.com/possdk/v68"; + expect(service.testCreateBaseUrl(url)).toBe( + "https://mycompany-checkout-live.adyenpayments.com/possdk/v68" + ); + }); + + it("should replace -test with -live for other URLs", () => { + const config = new Config({ + apiKey: "test_key", + environment: EnvironmentEnum.LIVE, + liveEndpointUrlPrefix: "mycompany" + }); + client = new Client(config); + + const service = new TestService(client); + const url = "https://some-test.adyen.com/api/"; + expect(service.testCreateBaseUrl(url)).toBe("https://some-live.adyen.com/api/"); + }); +}); \ No newline at end of file diff --git a/src/client.ts b/src/client.ts index 65d405511..9b2db546e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,35 +1,21 @@ -import Config from "./config"; +import Config, { EnvironmentEnum } from "./config"; +import { TERMINAL_API_ENDPOINT_TEST } from "./config"; + import HttpURLConnectionClient from "./httpClient/httpURLConnectionClient"; import ClientInterface from "./httpClient/clientInterface"; -type ClientParametersOverload = -| { config: Config } -| { config: Config; httpClient: ClientInterface } -| { username: string; password: string; environment: Environment} -| { username: string; password: string; environment: Environment; httpClient: ClientInterface } -| { username: string; password: string; environment: Environment; liveEndpointUrlPrefix: string } -| { username: string; password: string; environment: Environment; liveEndpointUrlPrefix: string; httpClient: ClientInterface } -| { username: string; password: string; environment: Environment; applicationName: string } -| { username: string; password: string; environment: Environment; applicationName: string; httpClient: ClientInterface } -| { username: string; password: string; environment: Environment; applicationName: string; liveEndpointUrlPrefix: string } -| { username: string; password: string; environment: Environment; applicationName: string; liveEndpointUrlPrefix: string; httpClient: ClientInterface } -| { apiKey: string; environment: Environment } -| { apiKey: string; environment: Environment; httpClient: ClientInterface } -| { apiKey: string; environment: Environment; liveEndpointUrlPrefix: string } -| { apiKey: string; environment: Environment; liveEndpointUrlPrefix: string; httpClient: ClientInterface }; - -interface ClientParameters { - config?: Config; - username?: string; - password?: string; - environment?: Environment; - applicationName?: string; - liveEndpointUrlPrefix?: string; - apiKey?: string; - httpClient?: ClientInterface; -} +/** + * Main Adyen API Client class. + * Handles configuration, authentication, and HTTP client setup for API requests. + */ class Client { + // Static endpoints and API version constants + // @deprecated: use Config.TERMINAL_API_ENDPOINT_TEST instead + public static TERMINAL_API_ENDPOINT_TEST = "https://terminal-api-test.adyen.com"; + // @deprecated: use Config.TERMINAL_API_ENDPOINT_LIVE instead + public static TERMINAL_API_ENDPOINT_LIVE = "https://terminal-api-live.adyen.com"; + // legacy support for marketPayEndpoint public static MARKETPAY_ENDPOINT_TEST = "https://cal-test.adyen.com/cal/services"; public static MARKETPAY_ENDPOINT_LIVE = "https://cal-live.adyen.com/cal/services"; public static MARKETPAY_ACCOUNT_API_VERSION = "v6"; @@ -37,58 +23,59 @@ class Client { public static MARKETPAY_HOP_API_VERSION = "v6"; public static MARKETPAY_NOTIFICATION_API_VERSION = "v5"; public static MARKETPAY_NOTIFICATION_CONFIGURATION_API_VERSION = "v6"; - public static TERMINAL_API_ENDPOINT_TEST = "https://terminal-api-test.adyen.com"; - public static TERMINAL_API_ENDPOINT_LIVE = "https://terminal-api-live.adyen.com"; + private _httpClient!: ClientInterface; public config: Config; - public liveEndpointUrlPrefix: string; - - public constructor(clientParameters: ClientParametersOverload); - public constructor(options: ClientParameters) { - if (options.config) { - this.config = options.config; - } else { - this.config = new Config(); + + /** + * Constructs a new Client instance. + * @param options - Configuration object + */ + public constructor(options: Config, httpClient?: ClientInterface) { + + this.config = options; + + if (!this.config.environment) { + throw new Error("Environment must be defined"); } - this.liveEndpointUrlPrefix = options.liveEndpointUrlPrefix ?? ""; - - const environment = options.environment ?? this.config.environment; - if (environment) { - this.setEnvironment(environment, options.liveEndpointUrlPrefix); - if (options.username && options.password) { - this.config.username = options.username; - this.config.password = options.password; - } - if (options.apiKey) { - this.config.apiKey = options.apiKey; + // set Terminal API endpoints + if (this.config.environment === EnvironmentEnum.TEST) { + // one TEST endpoint for all regions + this.config.terminalApiCloudEndpoint = TERMINAL_API_ENDPOINT_TEST; + } else if (this.config.environment === EnvironmentEnum.LIVE) { + // region-based LIVE endpoints + if(this.config.region) { + if (!Config.isRegionValid(this.config.region)) { + throw new Error(`Invalid region provided: ${this.config.region}`); + } + this.config.terminalApiCloudEndpoint = Config.getTerminalApiEndpoint(this.config.region); } } + // legacy support for marketPayEndpoint + if (this.config.environment === EnvironmentEnum.TEST) { + this.config.marketPayEndpoint = Client.MARKETPAY_ENDPOINT_TEST; + } else if (this.config.environment === EnvironmentEnum.LIVE) { + this.config.marketPayEndpoint = Client.MARKETPAY_ENDPOINT_LIVE; + } + + // Set application name if provided if(options.applicationName) { this.config.applicationName = options.applicationName; } - if (options.httpClient) { - this._httpClient = options.httpClient; + // Set custom HTTP client if provided + if (httpClient) { + this._httpClient = httpClient; } - } - public setEnvironment(environment: Environment, liveEndpointUrlPrefix?: string): void { - // ensure environment and liveUrlPrefix is set in config - this.config.environment = environment; - this.liveEndpointUrlPrefix = liveEndpointUrlPrefix ?? ""; - - if (environment === "TEST") { - this.config.marketPayEndpoint = Client.MARKETPAY_ENDPOINT_TEST; - this.config.terminalApiCloudEndpoint = Client.TERMINAL_API_ENDPOINT_TEST; - } else if (environment === "LIVE") { - this.config.marketPayEndpoint = Client.MARKETPAY_ENDPOINT_LIVE; - this.config.terminalApiCloudEndpoint = Client.TERMINAL_API_ENDPOINT_LIVE; - } } + /** + * Gets the HTTP client instance, creating a default one if not set. + */ public get httpClient(): ClientInterface { if (!this._httpClient) { this._httpClient = new HttpURLConnectionClient(); @@ -97,17 +84,30 @@ class Client { return this._httpClient; } + /** + * Sets a custom HTTP client. + * @param httpClient - The HTTP client to use. + */ public set httpClient(httpClient: ClientInterface) { this._httpClient = httpClient; } + /** + * Sets the application name in the config. + * @param applicationName - The application name. + */ public setApplicationName(applicationName: string): void { this.config.applicationName = applicationName; } + /** + * Sets the connection timeout in milliseconds. + * @param connectionTimeoutMillis - Timeout in milliseconds. + */ public setTimeouts(connectionTimeoutMillis: number): void { this.config.connectionTimeoutMillis = connectionTimeoutMillis; } } export default Client; + diff --git a/src/config.ts b/src/config.ts index 5a1f20363..e6ce94aa7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,45 @@ + +// Test endpoint for Terminal API +export const TERMINAL_API_ENDPOINT_TEST = "https://terminal-api-test.adyen.com"; + +// Live endpoints for Terminal API +const TERMINAL_API_ENDPOINT_LIVE = "https://terminal-api-live.adyen.com"; +const TERMINAL_API_ENDPOINT_AU_LIVE = "https://terminal-api-au.adyen.com"; +const TERMINAL_API_ENDPOINT_US_LIVE = "https://terminal-api-us.adyen.com"; +const TERMINAL_API_ENDPOINT_APSE_LIVE = "https://terminal-api-apse.adyen.com"; + + +/** + * Supported environments for the Adyen APIs. + */ +export enum EnvironmentEnum { + LIVE = "LIVE", + TEST = "TEST" +} + +/** + * Supported Regions for Terminal API integration. + */ +export enum RegionEnum { + EU = "EU", + AU = "AU", + US = "US", + APSE = "APSE" +} + +// Terminal API Endpoints Map +export const TERMINAL_API_ENDPOINTS_MAP: Record = { + [RegionEnum.EU]: TERMINAL_API_ENDPOINT_LIVE, + [RegionEnum.AU]: TERMINAL_API_ENDPOINT_AU_LIVE, + [RegionEnum.US]: TERMINAL_API_ENDPOINT_US_LIVE, + [RegionEnum.APSE]: TERMINAL_API_ENDPOINT_APSE_LIVE +}; + + interface ConfigConstructor { username?: string; password?: string; - environment?: Environment; + environment?: EnvironmentEnum; marketPayEndpoint?: string; applicationName?: string; apiKey?: string; @@ -9,14 +47,17 @@ interface ConfigConstructor { certificatePath?: string; terminalApiCloudEndpoint?: string; terminalApiLocalEndpoint?: string; + liveEndpointUrlPrefix?: string; // must be provided for LIVE integration + region?: RegionEnum; // must be provided for Terminal API integration } const DEFAULT_TIMEOUT = 30000; // Default timeout value (30 sec) class Config { + public username?: string; public password?: string; - public environment?: Environment; + public environment?: EnvironmentEnum; public marketPayEndpoint?: string; public applicationName?: string; public apiKey?: string; @@ -24,7 +65,8 @@ class Config { public certificatePath?: string; public terminalApiCloudEndpoint?: string; public terminalApiLocalEndpoint?: string; - + public liveEndpointUrlPrefix?: string; + public region?: RegionEnum; public constructor(options: ConfigConstructor = {}) { if (options.username) this.username = options.username; @@ -38,7 +80,38 @@ class Config { if (options.certificatePath) this.certificatePath = options.certificatePath; if (options.terminalApiCloudEndpoint) this.terminalApiCloudEndpoint = options.terminalApiCloudEndpoint; if (options.terminalApiLocalEndpoint) this.terminalApiLocalEndpoint = options.terminalApiLocalEndpoint; + if (options.liveEndpointUrlPrefix) this.liveEndpointUrlPrefix = options.liveEndpointUrlPrefix; + if (options.region) this.region = options.region; } + + /** + * Checks if the provided environment is valid. + * @param environment - The environment to validate. + * @returns true if the environment exists in EnvironmentEnum, false otherwise. + */ + public static isEnvironmentValid(environment: EnvironmentEnum): boolean { + return Object.values(EnvironmentEnum).includes(environment); + } + + /** + * Checks if the provided region is a valid supported. + * @param region - The region to validate. + * @returns true if the region exists in RegionEnum, false otherwise. + */ + public static isRegionValid(region: RegionEnum): boolean { + return Object.values(RegionEnum).includes(region); + } + + /** + * Returns the Terminal API endpoint for the given region. + * If the region is not valid, returns the EU endpoint. + * @param region - The region to get the endpoint for. + * @returns The Terminal API endpoint URL. + */ + public static getTerminalApiEndpoint(region: RegionEnum): string { + return TERMINAL_API_ENDPOINTS_MAP[region] || TERMINAL_API_ENDPOINTS_MAP[RegionEnum.EU]; + } + } export default Config; diff --git a/src/service.ts b/src/service.ts index 0ab47b447..2aad6c4f6 100644 --- a/src/service.ts +++ b/src/service.ts @@ -18,9 +18,12 @@ */ import Client from "./client"; -import Config from "./config"; - +import Config, { EnvironmentEnum } from "./config"; +/** + * Base Service class for all API services. + * Handles the setup of the endpoint URL for the API requests. + */ class Service { public apiKeyRequired = false; public client: Client; @@ -29,35 +32,45 @@ class Service { this.client = client; } + /** + * Constructs the base URL for API requests based on environment and endpoint type. + * - For non-LIVE environments, replaces '-live' with '-test'. + * - For LIVE environment, requires a liveEndpointUrlPrefix. + * - Handles special cases for 'pal-' and 'checkout-' endpoints. + * @param url - The original endpoint URL. + * @returns The formatted endpoint URL. + * @throws Error if url is not provided or liveEndpointUrlPrefix is missing for LIVE environment. + */ protected createBaseUrl(url: string): string { const config: Config = this.client.config; - if (config.environment !== "LIVE") { + if(!url) { + throw new Error("Endpoint URL must be provided."); + } + + if (config.environment !== EnvironmentEnum.LIVE) { return url.replace("-live", "-test"); } - if (url.includes("pal-")) { - if (this.client.liveEndpointUrlPrefix === "") - { - throw new Error("Please provide your unique live url prefix on the setEnvironment() call on the Client."); + if(config.environment === EnvironmentEnum.LIVE) { + if(!config?.liveEndpointUrlPrefix) { + throw new Error("Live endpoint URL prefix must be provided for LIVE environment."); } + } + + if (url.includes("pal-")) { return url.replace("https://pal-test.adyen.com/pal/servlet/", - `https://${this.client.liveEndpointUrlPrefix}-pal-live.adyenpayments.com/pal/servlet/`); + `https://${this.client.config.liveEndpointUrlPrefix}-pal-live.adyenpayments.com/pal/servlet/`); } if (url.includes("checkout-")) { - if (this.client.liveEndpointUrlPrefix === "") - { - throw new Error("Please provide your unique live url prefix on the setEnvironment() call on the Client."); - } - if (url.includes("/possdk/v68")) { return url.replace("https://checkout-test.adyen.com/", - `https://${this.client.liveEndpointUrlPrefix}-checkout-live.adyenpayments.com/`); + `https://${this.client.config.liveEndpointUrlPrefix}-checkout-live.adyenpayments.com/`); } return url.replace("https://checkout-test.adyen.com/", - `https://${this.client.liveEndpointUrlPrefix}-checkout-live.adyenpayments.com/checkout/`); + `https://${this.client.config.liveEndpointUrlPrefix}-checkout-live.adyenpayments.com/checkout/`); } return url.replace("-test", "-live"); diff --git a/src/typings/enums/environment.ts b/src/typings/enums/environment.ts index 5f072bf52..42e52a43b 100644 --- a/src/typings/enums/environment.ts +++ b/src/typings/enums/environment.ts @@ -17,5 +17,8 @@ * See the LICENSE file for more info. */ +/** + * @deprecated Use `EnvironmentEnum` instead. + */ declare type Environment = "LIVE" | "TEST";