127 lines
4.4 KiB
PHP
127 lines
4.4 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace Modules\Core\Payment\Support;
|
||
|
|
|
||
|
|
use Lunar\Models\Contracts\Currency as CurrencyContract;
|
||
|
|
use Stripe\Charge;
|
||
|
|
use Stripe\StripeClient;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* First-party replacement for Lunar\Stripe\Facades\Stripe +
|
||
|
|
* Lunar\Stripe\Managers\StripeManager — lunarphp/stripe was removed once
|
||
|
|
* Modules\Core\Payment\Drivers\StripePaymentDriver already replaced every
|
||
|
|
* bit of Lunar's own Stripe payment flow (see that class's own docblock);
|
||
|
|
* all that remained load-bearing from the package was raw API-client
|
||
|
|
* access and amount conversion, neither of which is Lunar-specific. Only
|
||
|
|
* the methods StripePaymentDriver actually called are kept — no
|
||
|
|
* fetchOrCreateIntent()/cart-bound helpers, which belonged to Lunar's own
|
||
|
|
* (unused) checkout flow.
|
||
|
|
*
|
||
|
|
* getClient()/getCharge() call the Stripe SDK directly rather than going
|
||
|
|
* through a facade — StripePaymentDriver resolves this class via the
|
||
|
|
* container instead, same as every other dependency it takes.
|
||
|
|
*/
|
||
|
|
class StripeManager
|
||
|
|
{
|
||
|
|
public function getClient(): StripeClient
|
||
|
|
{
|
||
|
|
return new StripeClient([
|
||
|
|
'api_key' => config('services.stripe.key'),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function getCharge(string $chargeId): Charge
|
||
|
|
{
|
||
|
|
return $this->getClient()->charges->retrieve($chargeId);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Zero-decimal currencies, per Stripe. The amount sent to Stripe is the
|
||
|
|
* major unit amount as-is.
|
||
|
|
*
|
||
|
|
* @see https://docs.stripe.com/currencies#zero-decimal
|
||
|
|
*/
|
||
|
|
protected const ZERO_DECIMAL_CURRENCIES = [
|
||
|
|
'bif', 'clp', 'djf', 'gnf', 'jpy', 'kmf', 'krw', 'mga', 'pyg',
|
||
|
|
'rwf', 'ugx', 'vnd', 'vuv', 'xaf', 'xof', 'xpf',
|
||
|
|
];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Three-decimal currencies, per Stripe. The amount sent to Stripe is the
|
||
|
|
* major unit amount multiplied by 1000.
|
||
|
|
*
|
||
|
|
* @see https://docs.stripe.com/currencies#three-decimal
|
||
|
|
*/
|
||
|
|
protected const THREE_DECIMAL_CURRENCIES = ['bhd', 'jod', 'kwd', 'omr', 'tnd'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* HUF, TWD and UGX are ISO zero-decimal currencies, but Stripe still
|
||
|
|
* requires amounts to be sent as if they had two decimal places.
|
||
|
|
*
|
||
|
|
* @see https://docs.stripe.com/currencies#special-cases
|
||
|
|
*/
|
||
|
|
protected const SPECIAL_ZERO_DECIMAL_CURRENCIES = ['huf', 'twd', 'ugx'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert a Lunar price value to the amount expected by Stripe.
|
||
|
|
*
|
||
|
|
* Lunar stores prices as integers scaled by `Currency::decimal_places`,
|
||
|
|
* which merchants can set independently of what Stripe expects for a
|
||
|
|
* given currency. This converts back to the major unit amount first,
|
||
|
|
* then re-scales it to whatever sub-unit Stripe requires for the
|
||
|
|
* currency, so the result is correct regardless of how the merchant has
|
||
|
|
* configured `Currency::decimal_places`.
|
||
|
|
*
|
||
|
|
* @see https://docs.stripe.com/currencies
|
||
|
|
*/
|
||
|
|
public static function toStripeAmount(int $value, CurrencyContract $currency): int
|
||
|
|
{
|
||
|
|
return self::rescale($value, max($currency->decimal_places, 0), self::stripeDecimalPlaces($currency));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert an amount received from Stripe back to a Lunar price value,
|
||
|
|
* scaled by `Currency::decimal_places`. Inverse of `toStripeAmount()`.
|
||
|
|
*/
|
||
|
|
public static function fromStripeAmount(int $amount, CurrencyContract $currency): int
|
||
|
|
{
|
||
|
|
return self::rescale($amount, self::stripeDecimalPlaces($currency), max($currency->decimal_places, 0));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The number of decimal places Stripe expects amounts in for a currency.
|
||
|
|
*/
|
||
|
|
protected static function stripeDecimalPlaces(CurrencyContract $currency): int
|
||
|
|
{
|
||
|
|
$code = strtolower($currency->code);
|
||
|
|
|
||
|
|
// UGX is also in the zero-decimal list; the special case takes precedence.
|
||
|
|
if (in_array($code, self::SPECIAL_ZERO_DECIMAL_CURRENCIES, true)) {
|
||
|
|
return 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (in_array($code, self::ZERO_DECIMAL_CURRENCIES, true)) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (in_array($code, self::THREE_DECIMAL_CURRENCIES, true)) {
|
||
|
|
return 3;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
protected static function rescale(int $value, int $fromDecimalPlaces, int $toDecimalPlaces): int
|
||
|
|
{
|
||
|
|
$exponent = $toDecimalPlaces - $fromDecimalPlaces;
|
||
|
|
|
||
|
|
if ($exponent >= 0) {
|
||
|
|
return $value * (10 ** $exponent);
|
||
|
|
}
|
||
|
|
|
||
|
|
$divisor = 10 ** (-$exponent);
|
||
|
|
|
||
|
|
return intdiv(abs($value) + intdiv($divisor, 2), $divisor) * ($value < 0 ? -1 : 1);
|
||
|
|
}
|
||
|
|
}
|