STRATO-apps/wordpress_03/app/wp-content/plugins/tutor-stripe/payments/Helper.php

SHA-256: aa1b9ce58deb30c126ccfd06270330ed453bef37652ab05f8aae2867bc13d9da
<?php

namespace Ollyo\PaymentHub\Payments\Stripe;

use Stripe\Webhook;
use RuntimeException;
use Brick\Money\Money;
use Brick\Math\RoundingMode;
use Brick\Money\CurrencyConverter;
use Ollyo\PaymentHub\Core\Support\System;
use Ollyo\PaymentHub\Exceptions\NotFoundException;
use Ollyo\PaymentHub\Exceptions\InvalidDataException;
use Ollyo\PaymentHub\Contracts\Config\RepositoryContract;
use Brick\Money\ExchangeRateProvider\ConfigurableProvider;
use Ollyo\PaymentHub\Exceptions\InvalidSignatureException;


final class Helper {

	const ZERO_DECIMAL_CURRENCIES = array(
		'BIF',
		'CLP',
		'DJF',
		'GNF',
		'JPY',
		'KMF',
		'KRW',
		'MGA',
		'PYG',
		'RWF',
		'UGX',
		'VND',
		'VUV',
		'XAF',
		'XOF',
		'XPF',
	);

	const SPECIAL_CASE_CURRENCIES = array(
		'ISK',
		'HUF',
		'TWD',
		'UGX',
	);

	/**
	 * Array containing the minimum charge amounts for various currencies.
	 *
	 * @since 1.0.0
	 */
	public static array $minimumCharges = array(
		'USD' => 0.50,
		'AED' => 2.00,
		'AUD' => 0.50,
		'BGN' => 1.00,
		'BRL' => 0.50,
		'CAD' => 0.50,
		'CHF' => 0.50,
		'CZK' => 15.00,
		'DKK' => 2.50,
		'EUR' => 0.50,
		'GBP' => 0.30,
		'HKD' => 4.00,
		'HUF' => 175.00,
		'INR' => 0.50,
		'JPY' => 50.00,
		'MXN' => 10.00,
		'MYR' => 2.00,
		'NOK' => 3.00,
		'NZD' => 0.50,
		'PLN' => 2.00,
		'RON' => 2.00,
		'SEK' => 3.00,
		'SGD' => 0.50,
		'THB' => 10.00,
	);


	/**
	 * Converts a given amount from one currency to another based on the provided exchange rate.
	 *
	 * @param  float  $amount               The amount to convert.
	 * @param  string $currency             The target currency to convert the amount to.
	 * @param  object $balanceTransaction   The balance transaction object containing currency and exchange rate information.
	 *
	 * @return float|null                   The converted amount in the target currency or null.
	 * @since 1.0.0
	 */
	public static function convertAmountByCurrency( $amount, $currency, $balanceTransaction ) {
		if ( is_null( $amount ) ) {
			return;
		}

		$settlementCurrency   = strtoupper( $balanceTransaction->currency );
		$exchangeRate         = 1 / $balanceTransaction->exchange_rate;
		$exchangeRateProvider = new ConfigurableProvider();
		$exchangeRateProvider->setExchangeRate( $settlementCurrency, $currency, $exchangeRate );

		$money           = Money::of( $amount, $settlementCurrency );
		$converter       = new CurrencyConverter( $exchangeRateProvider );
		$convertedAmount = $converter->convert( $money, $currency, null, RoundingMode::HALF_UP );

		return $convertedAmount->getAmount()->toFloat();
	}

	public static function getAmountInMinorUnit( $amount, string $currencyCode ) {

		if ( empty( $currencyCode ) ) {
			throw new InvalidDataException( esc_html__( 'Invalid currency Or Amount', 'tutor-stripe' ) );
		}

		if ( in_array( $currencyCode, self::ZERO_DECIMAL_CURRENCIES ) ) {
			return floatval( $amount );
		}

		return System::getMinorAmountBasedOnCurrency( $amount, $currencyCode );
	}

	/**
	 * Prepares Stripe-compatible line items from provided purchase data.
	 *
	 * @since 1.0.0
	 *
	 * @param object $data The purchase data object.
	 *
	 * @return array List of formatted line items for Stripe Checkout.
	 *
	 * @throws NotFoundException If no items are provided in the $data object.
	 */
	public static function getLineItems( $data ): array {

		if ( empty( $data->items ) ) {
			throw new NotFoundException( 'Items Not Found.', 1 );
		}

		$data->calculated_subtotal = 0;

		$lineItems = array_filter(
			array_map(
				function ( $item ) use ( $data ) {

					$price                      = is_null( $item['discounted_price'] ) ? $item['regular_price'] : $item['discounted_price'];
					$data->calculated_subtotal += $price;

					if ( floatval( ! empty( $price ) ) ) {
						return array(
							'price_data' => array(
								'product_data' => array( 'name' => html_entity_decode( $item['item_name'], ENT_QUOTES ) ),
								'unit_amount'  => self::getAmountInMinorUnit( $price, $data->currency->code ),
								'currency'     => strtolower( $data->currency->code ),
							),
							'quantity'   => $item['quantity'],
						);
					}
				},
				(array) $data->items
			)
		);

		// If Tax amount is given.
		if ( isset( $data->tax ) && $data->tax > 0 ) {

			$lineItems[] = array(
				'price_data' => array(
					'product_data' => array( 'name' => 'Tax' ),
					'unit_amount'  => self::getAmountInMinorUnit( $data->tax, $data->currency->code ),
					'currency'     => strtolower( $data->currency->code ),
				),
				'quantity'   => 1,
			);
		}

		$totalAmount = floatval( $data->calculated_subtotal ?? 0 ) + floatval( $data->tax ?? 0 ) - floatval( $data->coupon_discount ?? 0 );

		if ( $totalAmount <= 0 ) {

			$minimumCharge = self::calculateMinimumChargeDifference( $data );

			$lineItems[] = array(
				'price_data' => array(
					'product_data' => array(
						'name'        => 'Minimum Charge',
						'description' => 'Minimum charge to process the payment',
					),
					'unit_amount'  => self::getAmountInMinorUnit( $minimumCharge, $data->currency->code ),
					'currency'     => strtolower( $data->currency->code ),
				),
				'quantity'   => 1,
			);
		}

		return $lineItems;
	}

	/**
	 * Make the shipping options for the payment.
	 *
	 * @param object $data
	 * @return array
	 */
	public static function getShippingOptions( $data ) {
		$shipping = array();

		if ( $data->shipping_charge <= 0 || empty( $data->currency->code ) ) {
			return array();
		}

		$shipping[] = array(
			'shipping_rate_data' => array(
				'display_name' => 'Shipping Charge',
				'type'         => 'fixed_amount',
				'fixed_amount' => array(
					'amount'   => self::getAmountInMinorUnit( $data->shipping_charge, $data->currency->code ),
					'currency' => strtolower( $data->currency->code ),
				),
			),
		);

		return $shipping;
	}


	public static function createWebhookEvent( object $data, RepositoryContract $config ) {

		$webhookSecretKey = $config->get( 'webhook_secret_key' );
		$event            = null;

		if ( empty( $webhookSecretKey ) ) {
			http_response_code( 400 );
			throw new NotFoundException( sprintf( 'The webhook secret key is missing.' ) );
		}

		try {
			$signature = $data->server['HTTP_STRIPE_SIGNATURE'];

			if ( empty( $signature ) ) {
				http_response_code( 400 );
				throw new InvalidSignatureException( 'HTTP_STRIPE_SIGNATURE is missing.' );
			}

			$event = Webhook::constructEvent( $data->stream, $signature, $webhookSecretKey );

			$statusMap = array(
				'payment_intent.payment_failed' => 'failed',
				'charge.updated'                => 'paid',
				'payment_intent.canceled'       => 'canceled',
				'charge.refunded'               => 'refunded',
			);

			if ( empty( $event ) ) {
				http_response_code( 400 );
				throw new RuntimeException( sprintf( 'Webhook event is not created.' ) );
			}

			if ( ! isset( $statusMap[ $event->type ] ) ) {
				http_response_code( 400 );
				return null;
			}

			$status = $statusMap[ $event->type ];

			http_response_code( 200 );

			return (object) array(
				'payment_data' => $event->data,
				'status'       => $status,
			);
		} catch ( \Throwable $error ) {
			throw $error;
		}
	}


	/**
	 * Retrieves the shipping information for a recurring payment.
	 *
	 * @param   object $shipping    The shipping data object.
	 * @return  array               The formatted shipping information array, or an empty array if no shipping data is
	 * provided.
	 * @since   1.0.0
	 */
	public static function getShippingInfoForRecurring( $shipping ): array {
		if ( empty( $shipping ) ) {
			return array();
		}

		return array(
			'address' => array(
				'city'        => $shipping->city,
				'country'     => $shipping->country->alpha_2,
				'line1'       => $shipping->address1,
				'line2'       => $shipping->address2,
				'postal_code' => $shipping->postal_code,
				'state'       => $shipping->state,
			),
			'name'    => $shipping->name,
			'phone'   => $shipping->phone_number,
		);
	}

	/**
	 * Determines the refund status based on the captured and refunded amounts.
	 *
	 * @param  object $data The data object containing information about the captured and refunded amounts.
	 * @return string       The refund status, which can be 'refunded', 'partially_refunded', or an empty string if no conditions
	 * are met.
	 * @since  1.0.0
	 */
	public static function getRefundStatus( object $data ): string {

		$remainingAmount = $data->object->amount_captured - $data->object->amount_refunded;
		$previousAmount  = $data->previous_attributes->amount_refunded;
		$status          = '';

		if ( $remainingAmount > 0 ) {
			$status = 'partially_refunded';

		} elseif ( $remainingAmount === 0 && $previousAmount === 0 ) {
			$status = 'refunded';

		} elseif ( $remainingAmount === 0 && $previousAmount !== 0 ) {
			$status = 'partially_refunded';
		}

		return $status;
	}

	/**
	 * Calculates the extra charges, based on a charge ID.
	 *
	 * @param   string $chargeID    The ID of the charge for which extra charges are to be calculated.
	 * @return  array               An associative array containing extra charges, or null if not applicable.
	 * @since   1.0.0
	 */
	public static function calculateExtraCharges( $chargeDetails, $currency ): array {
		$processingFee = $earnings = null;

		if ( ! empty( $chargeDetails->balance_transaction ) ) {

			$balanceTransaction = Api::getBalanceTransaction( $chargeDetails->balance_transaction );

			$processingFee = $balanceTransaction->fee_details[0]->amount ?? null;
			$earnings      = $balanceTransaction->net ?? null;

			if ( strtoupper( $currency ) !== strtoupper( $balanceTransaction->currency ) ) {
				$processingFee = self::convertAmountByCurrency( $processingFee, $currency, $balanceTransaction );
				$earnings      = self::convertAmountByCurrency( $earnings, $currency, $balanceTransaction );
			}
		}

		return array(
			'fees'     => $processingFee,
			'earnings' => $earnings,
		);
	}

	/**
	 * Retrieves the minimum charge amount for the specified currency.
	 *
	 * @param  object $previousPayload      The payload from a previous transaction containing currency details.
	 * @param  string $currencyCode         The current currency code for which the minimum charge amount is requested.
	 *
	 * @return float                        The minimum charge amount, either in the original or converted currency.
	 *
	 * @throws InvalidDataException         If the currency is invalid or a minimum charge can't be determined.
	 * @since  1.0.0
	 */
	public static function getMinimumChargeAmount( $previousPayload, $currencyCode ) {
		$currency                = strtoupper( $currencyCode );
		$previousPayloadCurrency = strtoupper( $previousPayload->currency );

		if ( isset( self::$minimumCharges[ $currency ] ) ) {
			return self::$minimumCharges[ $currency ];
		}

		if ( isset( self::$minimumCharges[ $previousPayloadCurrency ] ) ) {
			return self::$minimumCharges[ $previousPayloadCurrency ];
		}

		$chargeDetails = Api::getCharge( $previousPayload->latest_charge );

		if ( $chargeDetails->balance_transaction ) {
			$balanceTransaction = Api::getBalanceTransaction( $chargeDetails->balance_transaction );

			$settlementCurrency = strtoupper( $balanceTransaction->currency );

			if ( isset( self::$minimumCharges[ $settlementCurrency ] ) ) {
				$minimumAmount = self::$minimumCharges[ $settlementCurrency ];
				return self::convertAmountByCurrency( $minimumAmount, $currency, $balanceTransaction );
			}
		}

		throw new InvalidDataException( 'Invalid Currency' );
	}

	/**
	 * Calculates the difference between the total price and the minimum allowed charge.
	 *
	 * @param  object $data         An object containing the necessary data.
	 * @return float                The difference between the total amount and the minimum charge, or 0 if the total
	 *                              is sufficient.
	 * @throws InvalidDataException If the minimum charge for the provided currency code is not available.
	 * @since  1.0.4
	 */
	public static function calculateMinimumChargeDifference( $data ): float {

		$subtotal = floatval( $data->calculated_subtotal ?? 0 );
		$tax      = floatval( $data->tax ?? 0 );
		$shipping = floatval( $data->shipping_charge ?? 0 );
		$discount = floatval( $data->coupon_discount ?? 0 );

		$totalAmount = $subtotal + $tax + $shipping - $discount;

		if ( $totalAmount > 0 ) {
			return $totalAmount;
		}

		$currencyCode = strtoupper( $data->currency->code ?? '' );

		if ( isset( self::$minimumCharges[ strtoupper( $currencyCode ) ] ) ) {
			return self::$minimumCharges[ $currencyCode ];
		}

		$defaultCurrency = Api::getDefaultCurrency();

		if ( empty( $defaultCurrency ) ) {
			throw new \Exception( __( 'Stripe Default Currency Not Found.', 'tutor-pro' ) ); // phpcs:ignore
		}

		$exchangeRateInfo = Api::createFXQuote( $defaultCurrency, $data->currency->code );
		$exchangeRate     = $exchangeRateInfo->json['rates'][ strtolower( $currencyCode ) ]['exchange_rate'] ?? null;

		if ( empty( $exchangeRate ) ) {
			return self::$minimumCharges[ strtoupper( $defaultCurrency ) ];
		}

		return floatval( ( 1 / $exchangeRate ) * self::$minimumCharges[ strtoupper( $defaultCurrency ) ] );
	}

	/**
	 * Retrieves the tax amount for a one-time payment.
	 *
	 * @param  object $data   The data object containing the payment intent ID.
	 * @return int|null         The tax amount for the session, or null if not found.
	 * @since  1.0.0
	 */
	private static function getTaxAmountForOneTime( $data ) {
		$checkoutSessionData = Api::getCheckoutSession( $data->id );
		$tax                 = $checkoutSessionData->data[0]->total_details->amount_tax ?? null;

		return System::convertMinorAmountToMajor( $tax, strtoupper( $data->currency ) );
	}

	/**
	 * Retrieves the tax amount based on the payment type.
	 *
	 * @param   object $data    The data object containing metadata about the payment type.
	 * @return  int|null        The tax amount, or null if not applicable.
	 * @since   1.0.0
	 */
	public static function getTaxAmount( $data ) {
		$type = $data->metadata->type;

		switch ( $type ) {

			case 'one-time':
				return self::getTaxAmountForOneTime( $data );

			case 'recurring':
				return $data->metadata->tax;
		}
	}
}