Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/tutor-stripe/payments/Helper.php
Keine Baseline-Datei – Diff nur gegen leer.
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
+