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;
}
}
}