| 1 |
<?php |
| 2 |
|
| 3 |
namespace IvyForms\Services\Payment; |
| 4 |
|
| 5 |
/** |
| 6 |
* Currency-aware conversion between major units and integer minor units. |
| 7 |
* |
| 8 |
* Do not use for parsing form money strings — that is MoneyParser's job. |
| 9 |
*/ |
| 10 |
final class MoneyMinorUnits |
| 11 |
{ |
| 12 |
/** |
| 13 |
* ISO currencies that use zero decimal places (amount already in minor units as whole numbers). |
| 14 |
* |
| 15 |
* @see https://docs.stripe.com/currencies#zero-decimal |
| 16 |
* |
| 17 |
* @var list<string> |
| 18 |
*/ |
| 19 |
private const ZERO_DECIMAL = [ |
| 20 |
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', |
| 21 |
'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF', |
| 22 |
]; |
| 23 |
|
| 24 |
/** |
| 25 |
* PayPal rejects decimal `value` strings for these codes (JPY already in ZERO_DECIMAL). |
| 26 |
* |
| 27 |
* @see https://developer.paypal.com/api/rest/reference/currency-codes/ |
| 28 |
* |
| 29 |
* @var list<string> |
| 30 |
*/ |
| 31 |
private const PAYPAL_ZERO_DECIMAL = ['HUF', 'TWD']; |
| 32 |
|
| 33 |
/** |
| 34 |
* Convert a major-unit amount to the integer minor units for the currency. |
| 35 |
*/ |
| 36 |
public static function toMinor(float $amount, string $currency): int |
| 37 |
{ |
| 38 |
$code = strtoupper(trim($currency)); |
| 39 |
|
| 40 |
if (in_array($code, self::ZERO_DECIMAL, true)) { |
| 41 |
return (int) round($amount); |
| 42 |
} |
| 43 |
|
| 44 |
return (int) round($amount * 100); |
| 45 |
} |
| 46 |
|
| 47 |
/** |
| 48 |
* Convert integer minor units back to a major-unit float for persistence/display. |
| 49 |
*/ |
| 50 |
public static function fromMinor(int $minor, string $currency): float |
| 51 |
{ |
| 52 |
$code = strtoupper(trim($currency)); |
| 53 |
|
| 54 |
if (in_array($code, self::ZERO_DECIMAL, true)) { |
| 55 |
return (float) $minor; |
| 56 |
} |
| 57 |
|
| 58 |
return $minor / 100; |
| 59 |
} |
| 60 |
|
| 61 |
/** |
| 62 |
* Format a major-unit amount for PayPal Orders (`value` string). |
| 63 |
*/ |
| 64 |
public static function formatMajor(float $amount, string $currency): string |
| 65 |
{ |
| 66 |
$code = strtoupper(trim($currency)); |
| 67 |
|
| 68 |
if ( |
| 69 |
in_array($code, self::ZERO_DECIMAL, true) |
| 70 |
|| in_array($code, self::PAYPAL_ZERO_DECIMAL, true) |
| 71 |
) { |
| 72 |
return (string) (int) round($amount); |
| 73 |
} |
| 74 |
|
| 75 |
return number_format($amount, 2, '.', ''); |
| 76 |
} |
| 77 |
|
| 78 |
/** |
| 79 |
* Lowest charge we will send to a processor (minor units). |
| 80 |
* |
| 81 |
* Matches the common card-network floor (~0.50 in two-decimal currencies) |
| 82 |
* so $0.01 card-testing charges are rejected before they hit the gateway. |
| 83 |
*/ |
| 84 |
public static function minimumChargeMinor(string $currency): int |
| 85 |
{ |
| 86 |
unset($currency); |
| 87 |
|
| 88 |
return 50; |
| 89 |
} |
| 90 |
|
| 91 |
public static function isChargeable(float $amount, string $currency): bool |
| 92 |
{ |
| 93 |
return self::toMinor($amount, $currency) >= self::minimumChargeMinor($currency); |
| 94 |
} |
| 95 |
} |
| 96 |
|