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

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 +
3 + namespace Ollyo\PaymentHub\Payments\Stripe;
4 +
5 + use Stripe\Webhook;
6 + use RuntimeException;
7 + use Brick\Money\Money;
8 + use Brick\Math\RoundingMode;
9 + use Brick\Money\CurrencyConverter;
10 + use Ollyo\PaymentHub\Core\Support\System;
11 + use Ollyo\PaymentHub\Exceptions\NotFoundException;
12 + use Ollyo\PaymentHub\Exceptions\InvalidDataException;
13 + use Ollyo\PaymentHub\Contracts\Config\RepositoryContract;
14 + use Brick\Money\ExchangeRateProvider\ConfigurableProvider;
15 + use Ollyo\PaymentHub\Exceptions\InvalidSignatureException;
16 +
17 +
18 + final class Helper {
19 +
20 + const ZERO_DECIMAL_CURRENCIES = array(
21 + 'BIF',
22 + 'CLP',
23 + 'DJF',
24 + 'GNF',
25 + 'JPY',
26 + 'KMF',
27 + 'KRW',
28 + 'MGA',
29 + 'PYG',
30 + 'RWF',
31 + 'UGX',
32 + 'VND',
33 + 'VUV',
34 + 'XAF',
35 + 'XOF',
36 + 'XPF',
37 + );
38 +
39 + const SPECIAL_CASE_CURRENCIES = array(
40 + 'ISK',
41 + 'HUF',
42 + 'TWD',
43 + 'UGX',
44 + );
45 +
46 + /**
47 + * Array containing the minimum charge amounts for various currencies.
48 + *
49 + * @since 1.0.0
50 + */
51 + public static array $minimumCharges = array(
52 + 'USD' => 0.50,
53 + 'AED' => 2.00,
54 + 'AUD' => 0.50,
55 + 'BGN' => 1.00,
56 + 'BRL' => 0.50,
57 + 'CAD' => 0.50,
58 + 'CHF' => 0.50,
59 + 'CZK' => 15.00,
60 + 'DKK' => 2.50,
61 + 'EUR' => 0.50,
62 + 'GBP' => 0.30,
63 + 'HKD' => 4.00,
64 + 'HUF' => 175.00,
65 + 'INR' => 0.50,
66 + 'JPY' => 50.00,
67 + 'MXN' => 10.00,
68 + 'MYR' => 2.00,
69 + 'NOK' => 3.00,
70 + 'NZD' => 0.50,
71 + 'PLN' => 2.00,
72 + 'RON' => 2.00,
73 + 'SEK' => 3.00,
74 + 'SGD' => 0.50,
75 + 'THB' => 10.00,
76 + );
77 +
78 +
79 + /**
80 + * Converts a given amount from one currency to another based on the provided exchange rate.
81 + *
82 + * @param float $amount The amount to convert.
83 + * @param string $currency The target currency to convert the amount to.
84 + * @param object $balanceTransaction The balance transaction object containing currency and exchange rate information.
85 + *
86 + * @return float|null The converted amount in the target currency or null.
87 + * @since 1.0.0
88 + */
89 + public static function convertAmountByCurrency( $amount, $currency, $balanceTransaction ) {
90 + if ( is_null( $amount ) ) {
91 + return;
92 + }
93 +
94 + $settlementCurrency = strtoupper( $balanceTransaction->currency );
95 + $exchangeRate = 1 / $balanceTransaction->exchange_rate;
96 + $exchangeRateProvider = new ConfigurableProvider();
97 + $exchangeRateProvider->setExchangeRate( $settlementCurrency, $currency, $exchangeRate );
98 +
99 + $money = Money::of( $amount, $settlementCurrency );
100 + $converter = new CurrencyConverter( $exchangeRateProvider );
101 + $convertedAmount = $converter->convert( $money, $currency, null, RoundingMode::HALF_UP );
102 +
103 + return $convertedAmount->getAmount()->toFloat();
104 + }
105 +
106 + public static function getAmountInMinorUnit( $amount, string $currencyCode ) {
107 +
108 + if ( empty( $currencyCode ) ) {
109 + throw new InvalidDataException( esc_html__( 'Invalid currency Or Amount', 'tutor-stripe' ) );
110 + }
111 +
112 + if ( in_array( $currencyCode, self::ZERO_DECIMAL_CURRENCIES ) ) {
113 + return floatval( $amount );
114 + }
115 +
116 + return System::getMinorAmountBasedOnCurrency( $amount, $currencyCode );
117 + }
118 +
119 + /**
120 + * Prepares Stripe-compatible line items from provided purchase data.
121 + *
122 + * @since 1.0.0
123 + *
124 + * @param object $data The purchase data object.
125 + *
126 + * @return array List of formatted line items for Stripe Checkout.
127 + *
128 + * @throws NotFoundException If no items are provided in the $data object.
129 + */
130 + public static function getLineItems( $data ): array {
131 +
132 + if ( empty( $data->items ) ) {
133 + throw new NotFoundException( 'Items Not Found.', 1 );
134 + }
135 +
136 + $data->calculated_subtotal = 0;
137 +
138 + $lineItems = array_filter(
139 + array_map(
140 + function ( $item ) use ( $data ) {
141 +
142 + $price = is_null( $item['discounted_price'] ) ? $item['regular_price'] : $item['discounted_price'];
143 + $data->calculated_subtotal += $price;
144 +
145 + if ( floatval( ! empty( $price ) ) ) {
146 + return array(
147 + 'price_data' => array(
148 + 'product_data' => array( 'name' => html_entity_decode( $item['item_name'], ENT_QUOTES ) ),
149 + 'unit_amount' => self::getAmountInMinorUnit( $price, $data->currency->code ),
150 + 'currency' => strtolower( $data->currency->code ),
151 + ),
152 + 'quantity' => $item['quantity'],
153 + );
154 + }
155 + },
156 + (array) $data->items
157 + )
158 + );
159 +
160 + // If Tax amount is given.
161 + if ( isset( $data->tax ) && $data->tax > 0 ) {
162 +
163 + $lineItems[] = array(
164 + 'price_data' => array(
165 + 'product_data' => array( 'name' => 'Tax' ),
166 + 'unit_amount' => self::getAmountInMinorUnit( $data->tax, $data->currency->code ),
167 + 'currency' => strtolower( $data->currency->code ),
168 + ),
169 + 'quantity' => 1,
170 + );
171 + }
172 +
173 + $totalAmount = floatval( $data->calculated_subtotal ?? 0 ) + floatval( $data->tax ?? 0 ) - floatval( $data->coupon_discount ?? 0 );
174 +
175 + if ( $totalAmount <= 0 ) {
176 +
177 + $minimumCharge = self::calculateMinimumChargeDifference( $data );
178 +
179 + $lineItems[] = array(
180 + 'price_data' => array(
181 + 'product_data' => array(
182 + 'name' => 'Minimum Charge',
183 + 'description' => 'Minimum charge to process the payment',
184 + ),
185 + 'unit_amount' => self::getAmountInMinorUnit( $minimumCharge, $data->currency->code ),
186 + 'currency' => strtolower( $data->currency->code ),
187 + ),
188 + 'quantity' => 1,
189 + );
190 + }
191 +
192 + return $lineItems;
193 + }
194 +
195 + /**
196 + * Make the shipping options for the payment.
197 + *
198 + * @param object $data
199 + * @return array
200 + */
201 + public static function getShippingOptions( $data ) {
202 + $shipping = array();
203 +
204 + if ( $data->shipping_charge <= 0 || empty( $data->currency->code ) ) {
205 + return array();
206 + }
207 +
208 + $shipping[] = array(
209 + 'shipping_rate_data' => array(
210 + 'display_name' => 'Shipping Charge',
211 + 'type' => 'fixed_amount',
212 + 'fixed_amount' => array(
213 + 'amount' => self::getAmountInMinorUnit( $data->shipping_charge, $data->currency->code ),
214 + 'currency' => strtolower( $data->currency->code ),
215 + ),
216 + ),
217 + );
218 +
219 + return $shipping;
220 + }
221 +
222 +
223 + public static function createWebhookEvent( object $data, RepositoryContract $config ) {
224 +
225 + $webhookSecretKey = $config->get( 'webhook_secret_key' );
226 + $event = null;
227 +
228 + if ( empty( $webhookSecretKey ) ) {
229 + http_response_code( 400 );
230 + throw new NotFoundException( sprintf( 'The webhook secret key is missing.' ) );
231 + }
232 +
233 + try {
234 + $signature = $data->server['HTTP_STRIPE_SIGNATURE'];
235 +
236 + if ( empty( $signature ) ) {
237 + http_response_code( 400 );
238 + throw new InvalidSignatureException( 'HTTP_STRIPE_SIGNATURE is missing.' );
239 + }
240 +
241 + $event = Webhook::constructEvent( $data->stream, $signature, $webhookSecretKey );
242 +
243 + $statusMap = array(
244 + 'payment_intent.payment_failed' => 'failed',
245 + 'charge.updated' => 'paid',
246 + 'payment_intent.canceled' => 'canceled',
247 + 'charge.refunded' => 'refunded',
248 + );
249 +
250 + if ( empty( $event ) ) {
251 + http_response_code( 400 );
252 + throw new RuntimeException( sprintf( 'Webhook event is not created.' ) );
253 + }
254 +
255 + if ( ! isset( $statusMap[ $event->type ] ) ) {
256 + http_response_code( 400 );
257 + return null;
258 + }
259 +
260 + $status = $statusMap[ $event->type ];
261 +
262 + http_response_code( 200 );
263 +
264 + return (object) array(
265 + 'payment_data' => $event->data,
266 + 'status' => $status,
267 + );
268 + } catch ( \Throwable $error ) {
269 + throw $error;
270 + }
271 + }
272 +
273 +
274 + /**
275 + * Retrieves the shipping information for a recurring payment.
276 + *
277 + * @param object $shipping The shipping data object.
278 + * @return array The formatted shipping information array, or an empty array if no shipping data is
279 + * provided.
280 + * @since 1.0.0
281 + */
282 + public static function getShippingInfoForRecurring( $shipping ): array {
283 + if ( empty( $shipping ) ) {
284 + return array();
285 + }
286 +
287 + return array(
288 + 'address' => array(
289 + 'city' => $shipping->city,
290 + 'country' => $shipping->country->alpha_2,
291 + 'line1' => $shipping->address1,
292 + 'line2' => $shipping->address2,
293 + 'postal_code' => $shipping->postal_code,
294 + 'state' => $shipping->state,
295 + ),
296 + 'name' => $shipping->name,
297 + 'phone' => $shipping->phone_number,
298 + );
299 + }
300 +
301 + /**
302 + * Determines the refund status based on the captured and refunded amounts.
303 + *
304 + * @param object $data The data object containing information about the captured and refunded amounts.
305 + * @return string The refund status, which can be 'refunded', 'partially_refunded', or an empty string if no conditions
306 + * are met.
307 + * @since 1.0.0
308 + */
309 + public static function getRefundStatus( object $data ): string {
310 +
311 + $remainingAmount = $data->object->amount_captured - $data->object->amount_refunded;
312 + $previousAmount = $data->previous_attributes->amount_refunded;
313 + $status = '';
314 +
315 + if ( $remainingAmount > 0 ) {
316 + $status = 'partially_refunded';
317 +
318 + } elseif ( $remainingAmount === 0 && $previousAmount === 0 ) {
319 + $status = 'refunded';
320 +
321 + } elseif ( $remainingAmount === 0 && $previousAmount !== 0 ) {
322 + $status = 'partially_refunded';
323 + }
324 +
325 + return $status;
326 + }
327 +
328 + /**
329 + * Calculates the extra charges, based on a charge ID.
330 + *
331 + * @param string $chargeID The ID of the charge for which extra charges are to be calculated.
332 + * @return array An associative array containing extra charges, or null if not applicable.
333 + * @since 1.0.0
334 + */
335 + public static function calculateExtraCharges( $chargeDetails, $currency ): array {
336 + $processingFee = $earnings = null;
337 +
338 + if ( ! empty( $chargeDetails->balance_transaction ) ) {
339 +
340 + $balanceTransaction = Api::getBalanceTransaction( $chargeDetails->balance_transaction );
341 +
342 + $processingFee = $balanceTransaction->fee_details[0]->amount ?? null;
343 + $earnings = $balanceTransaction->net ?? null;
344 +
345 + if ( strtoupper( $currency ) !== strtoupper( $balanceTransaction->currency ) ) {
346 + $processingFee = self::convertAmountByCurrency( $processingFee, $currency, $balanceTransaction );
347 + $earnings = self::convertAmountByCurrency( $earnings, $currency, $balanceTransaction );
348 + }
349 + }
350 +
351 + return array(
352 + 'fees' => $processingFee,
353 + 'earnings' => $earnings,
354 + );
355 + }
356 +
357 + /**
358 + * Retrieves the minimum charge amount for the specified currency.
359 + *
360 + * @param object $previousPayload The payload from a previous transaction containing currency details.
361 + * @param string $currencyCode The current currency code for which the minimum charge amount is requested.
362 + *
363 + * @return float The minimum charge amount, either in the original or converted currency.
364 + *
365 + * @throws InvalidDataException If the currency is invalid or a minimum charge can't be determined.
366 + * @since 1.0.0
367 + */
368 + public static function getMinimumChargeAmount( $previousPayload, $currencyCode ) {
369 + $currency = strtoupper( $currencyCode );
370 + $previousPayloadCurrency = strtoupper( $previousPayload->currency );
371 +
372 + if ( isset( self::$minimumCharges[ $currency ] ) ) {
373 + return self::$minimumCharges[ $currency ];
374 + }
375 +
376 + if ( isset( self::$minimumCharges[ $previousPayloadCurrency ] ) ) {
377 + return self::$minimumCharges[ $previousPayloadCurrency ];
378 + }
379 +
380 + $chargeDetails = Api::getCharge( $previousPayload->latest_charge );
381 +
382 + if ( $chargeDetails->balance_transaction ) {
383 + $balanceTransaction = Api::getBalanceTransaction( $chargeDetails->balance_transaction );
384 +
385 + $settlementCurrency = strtoupper( $balanceTransaction->currency );
386 +
387 + if ( isset( self::$minimumCharges[ $settlementCurrency ] ) ) {
388 + $minimumAmount = self::$minimumCharges[ $settlementCurrency ];
389 + return self::convertAmountByCurrency( $minimumAmount, $currency, $balanceTransaction );
390 + }
391 + }
392 +
393 + throw new InvalidDataException( 'Invalid Currency' );
394 + }
395 +
396 + /**
397 + * Calculates the difference between the total price and the minimum allowed charge.
398 + *
399 + * @param object $data An object containing the necessary data.
400 + * @return float The difference between the total amount and the minimum charge, or 0 if the total
401 + * is sufficient.
402 + * @throws InvalidDataException If the minimum charge for the provided currency code is not available.
403 + * @since 1.0.4
404 + */
405 + public static function calculateMinimumChargeDifference( $data ): float {
406 +
407 + $subtotal = floatval( $data->calculated_subtotal ?? 0 );
408 + $tax = floatval( $data->tax ?? 0 );
409 + $shipping = floatval( $data->shipping_charge ?? 0 );
410 + $discount = floatval( $data->coupon_discount ?? 0 );
411 +
412 + $totalAmount = $subtotal + $tax + $shipping - $discount;
413 +
414 + if ( $totalAmount > 0 ) {
415 + return $totalAmount;
416 + }
417 +
418 + $currencyCode = strtoupper( $data->currency->code ?? '' );
419 +
420 + if ( isset( self::$minimumCharges[ strtoupper( $currencyCode ) ] ) ) {
421 + return self::$minimumCharges[ $currencyCode ];
422 + }
423 +
424 + $defaultCurrency = Api::getDefaultCurrency();
425 +
426 + if ( empty( $defaultCurrency ) ) {
427 + throw new \Exception( __( 'Stripe Default Currency Not Found.', 'tutor-pro' ) ); // phpcs:ignore
428 + }
429 +
430 + $exchangeRateInfo = Api::createFXQuote( $defaultCurrency, $data->currency->code );
431 + $exchangeRate = $exchangeRateInfo->json['rates'][ strtolower( $currencyCode ) ]['exchange_rate'] ?? null;
432 +
433 + if ( empty( $exchangeRate ) ) {
434 + return self::$minimumCharges[ strtoupper( $defaultCurrency ) ];
435 + }
436 +
437 + return floatval( ( 1 / $exchangeRate ) * self::$minimumCharges[ strtoupper( $defaultCurrency ) ] );
438 + }
439 +
440 + /**
441 + * Retrieves the tax amount for a one-time payment.
442 + *
443 + * @param object $data The data object containing the payment intent ID.
444 + * @return int|null The tax amount for the session, or null if not found.
445 + * @since 1.0.0
446 + */
447 + private static function getTaxAmountForOneTime( $data ) {
448 + $checkoutSessionData = Api::getCheckoutSession( $data->id );
449 + $tax = $checkoutSessionData->data[0]->total_details->amount_tax ?? null;
450 +
451 + return System::convertMinorAmountToMajor( $tax, strtoupper( $data->currency ) );
452 + }
453 +
454 + /**
455 + * Retrieves the tax amount based on the payment type.
456 + *
457 + * @param object $data The data object containing metadata about the payment type.
458 + * @return int|null The tax amount, or null if not applicable.
459 + * @since 1.0.0
460 + */
461 + public static function getTaxAmount( $data ) {
462 + $type = $data->metadata->type;
463 +
464 + switch ( $type ) {
465 +
466 + case 'one-time':
467 + return self::getTaxAmountForOneTime( $data );
468 +
469 + case 'recurring':
470 + return $data->metadata->tax;
471 + }
472 + }
473 + }
474 +