Initial commit
diff --git a/includes/class-wc-stripe-api.php b/includes/class-wc-stripe-api.php
new file mode 100644
index 0000000..6abaddb
--- /dev/null
+++ b/includes/class-wc-stripe-api.php
@@ -0,0 +1,275 @@
+<?php
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+/**
+ * WC_Stripe_API class.
+ *
+ * Communicates with Stripe API.
+ */
+class WC_Stripe_API {
+
+	/**
+	 * Stripe API Endpoint
+	 */
+	const ENDPOINT           = 'https://api.stripe.com/v1/';
+	const STRIPE_API_VERSION = '2019-09-09';
+
+	/**
+	 * Secret API Key.
+	 *
+	 * @var string
+	 */
+	private static $secret_key = '';
+
+	/**
+	 * Set secret API Key.
+	 *
+	 * @param string $key
+	 */
+	public static function set_secret_key( $secret_key ) {
+		self::$secret_key = $secret_key;
+	}
+
+	/**
+	 * Get secret key.
+	 *
+	 * @return string
+	 */
+	public static function get_secret_key() {
+		if ( ! self::$secret_key ) {
+			$options         = get_option( 'woocommerce_stripe_settings' );
+			$secret_key      = $options['secret_key'] ?? '';
+			$test_secret_key = $options['test_secret_key'] ?? '';
+
+			if ( isset( $options['testmode'] ) ) {
+				self::set_secret_key( 'yes' === $options['testmode'] ? $test_secret_key : $secret_key );
+			}
+		}
+		return self::$secret_key;
+	}
+
+	/**
+	 * Generates the user agent we use to pass to API request so
+	 * Stripe can identify our application.
+	 *
+	 * @since 4.0.0
+	 * @version 4.0.0
+	 */
+	public static function get_user_agent() {
+		$app_info = [
+			'name'       => 'WooCommerce Stripe Gateway',
+			'version'    => WC_STRIPE_VERSION,
+			'url'        => 'https://woocommerce.com/products/stripe/',
+			'partner_id' => 'pp_partner_EYuSt9peR0WTMg',
+		];
+
+		return [
+			'lang'         => 'php',
+			'lang_version' => phpversion(),
+			'publisher'    => 'woocommerce',
+			'uname'        => 'Linux server 5.10.0-18-amd64 #1 SMP Debian 5.10.140-1 (2022-09-02) x86_64',
+			'application'  => $app_info,
+		];
+	}
+
+	/**
+	 * Generates the headers to pass to API request.
+	 *
+	 * @since 4.0.0
+	 * @version 4.0.0
+	 */
+	public static function get_headers() {
+		$user_agent = self::get_user_agent();
+		$app_info   = $user_agent['application'];
+
+		$headers = apply_filters(
+			'woocommerce_stripe_request_headers',
+			[
+				'Authorization'  => 'Basic ' . base64_encode( self::get_secret_key() . ':' ),
+				'Stripe-Version' => self::STRIPE_API_VERSION,
+			]
+		);
+
+		// These headers should not be overridden for this gateway.
+		$headers['User-Agent']                 = $app_info['name'] . '/' . $app_info['version'] . ' (' . $app_info['url'] . ')';
+		$headers['X-Stripe-Client-User-Agent'] = wp_json_encode( $user_agent );
+
+		return $headers;
+	}
+
+	/**
+	 * Send the request to Stripe's API
+	 *
+	 * @since 3.1.0
+	 * @version 4.0.6
+	 * @param array  $request
+	 * @param string $api
+	 * @param string $method
+	 * @param bool   $with_headers To get the response with headers.
+	 * @return stdClass|array
+	 * @throws WC_Stripe_Exception
+	 */
+	public static function request( $request, $api = 'charges', $method = 'POST', $with_headers = false ) {
+		WC_Stripe_Logger::log( "{$api} request: " . print_r( $request, true ) );
+
+		$headers         = self::get_headers();
+		$idempotency_key = '';
+
+		if ( 'charges' === $api && 'POST' === $method ) {
+			$customer        = ! empty( $request['customer'] ) ? $request['customer'] : '';
+			$source          = ! empty( $request['source'] ) ? $request['source'] : $customer;
+			$idempotency_key = apply_filters( 'wc_stripe_idempotency_key', $request['metadata']['order_id'] . '-' . $source, $request );
+
+			$headers['Idempotency-Key'] = $idempotency_key;
+		}
+
+		$response = wp_safe_remote_post(
+			self::ENDPOINT . $api,
+			[
+				'method'  => $method,
+				'headers' => $headers,
+				'body'    => apply_filters( 'woocommerce_stripe_request_body', $request, $api ),
+				'timeout' => 70,
+			]
+		);
+
+		if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
+			WC_Stripe_Logger::log(
+				'Error Response: ' . print_r( $response, true ) . PHP_EOL . PHP_EOL . 'Failed request: ' . print_r(
+					[
+						'api'             => $api,
+						'request'         => $request,
+						'idempotency_key' => $idempotency_key,
+					],
+					true
+				)
+			);
+
+			throw new WC_Stripe_Exception( print_r( $response, true ), __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );
+		}
+
+		if ( $with_headers ) {
+			return [
+				'headers' => wp_remote_retrieve_headers( $response ),
+				'body'    => json_decode( $response['body'] ),
+			];
+		}
+
+		return json_decode( $response['body'] );
+	}
+
+	/**
+	 * Retrieve API endpoint.
+	 *
+	 * @since 4.0.0
+	 * @version 4.0.0
+	 * @param string $api
+	 */
+	public static function retrieve( $api ) {
+		WC_Stripe_Logger::log( "{$api}" );
+
+		$response = wp_safe_remote_get(
+			self::ENDPOINT . $api,
+			[
+				'method'  => 'GET',
+				'headers' => self::get_headers(),
+				'timeout' => 70,
+			]
+		);
+
+		if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
+			WC_Stripe_Logger::log( 'Error Response: ' . print_r( $response, true ) );
+			return new WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'woocommerce-gateway-stripe' ) );
+		}
+
+		return json_decode( $response['body'] );
+	}
+
+	/**
+	 * Send the request to Stripe's API with level 3 data generated
+	 * from the order. If the request fails due to an error related
+	 * to level3 data, make the request again without it to allow
+	 * the payment to go through.
+	 *
+	 * @since 4.3.2
+	 * @version 5.1.0
+	 *
+	 * @param array    $request     Array with request parameters.
+	 * @param string   $api         The API path for the request.
+	 * @param array    $level3_data The level 3 data for this request.
+	 * @param WC_Order $order       The order associated with the payment.
+	 *
+	 * @return stdClass|array The response
+	 */
+	public static function request_with_level3_data( $request, $api, $level3_data, $order ) {
+		// 1. Do not add level3 data if the array is empty.
+		// 2. Do not add level3 data if there's a transient indicating that level3 was
+		// not accepted by Stripe in the past for this account.
+		// 3. Do not try to add level3 data if merchant is not based in the US.
+		// https://stripe.com/docs/level3#level-iii-usage-requirements
+		// (Needs to be authenticated with a level3 gated account to see above docs).
+		if (
+			empty( $level3_data ) ||
+			get_transient( 'wc_stripe_level3_not_allowed' ) ||
+			'US' !== WC()->countries->get_base_country()
+		) {
+			return self::request(
+				$request,
+				$api
+			);
+		}
+
+		// Add level 3 data to the request.
+		$request['level3'] = $level3_data;
+
+		$result = self::request(
+			$request,
+			$api
+		);
+
+		$is_level3_param_not_allowed = (
+			isset( $result->error )
+			&& isset( $result->error->code )
+			&& 'parameter_unknown' === $result->error->code
+			&& isset( $result->error->param )
+			&& 'level3' === $result->error->param
+		);
+
+		$is_level_3data_incorrect = (
+			isset( $result->error )
+			&& isset( $result->error->type )
+			&& 'invalid_request_error' === $result->error->type
+		);
+
+		if ( $is_level3_param_not_allowed ) {
+			// Set a transient so that future requests do not add level 3 data.
+			// Transient is set to expire in 3 months, can be manually removed if needed.
+			set_transient( 'wc_stripe_level3_not_allowed', true, 3 * MONTH_IN_SECONDS );
+		} elseif ( $is_level_3data_incorrect ) {
+			// Log the issue so we could debug it.
+			WC_Stripe_Logger::log(
+				'Level3 data sum incorrect: ' . PHP_EOL
+				. print_r( $result->error->message, true ) . PHP_EOL
+				. print_r( 'Order line items: ', true ) . PHP_EOL
+				. print_r( $order->get_items(), true ) . PHP_EOL
+				. print_r( 'Order shipping amount: ', true ) . PHP_EOL
+				. print_r( $order->get_shipping_total(), true ) . PHP_EOL
+				. print_r( 'Order currency: ', true ) . PHP_EOL
+				. print_r( $order->get_currency(), true )
+			);
+		}
+
+		// Make the request again without level 3 data.
+		if ( $is_level3_param_not_allowed || $is_level_3data_incorrect ) {
+			unset( $request['level3'] );
+			return self::request(
+				$request,
+				$api
+			);
+		}
+
+		return $result;
+	}
+}