mirror of
https://github.com/FOSSBilling/FOSSBilling.git
synced 2026-10-23 03:37:01 +09:00
Document UGX exclusion and hardcode Stripe decimal lists (#4542)
* Address review comments on #4541: document UGX exclusion, hardcode Stripe decimal lists - Rename STRIPE_ZERO_DECIMAL_CURRENCIES to STRIPE_ZERO_DECIMAL_CHARGE_CURRENCIES and document why UGX is excluded (zero-decimal per Stripe's list, but its special-case row requires charge amounts x100), plus ISK/HUF/TWD notes. - Replace the CLDR-based three-decimal check with a hardcoded STRIPE_THREE_DECIMAL_CURRENCIES list (BHD, JOD, KWD, OMR, TND); drop the Symfony Intl dependency from this helper. - Reword the StripeTest comment to state the UGX exception explicitly. * Extend Stripe currency test coverage in #4541 follow-up - getAmountInMinorUnits: add BHD (second three-decimal code), AFN/YER (more CLDR-zero codes Stripe charges x100), TWD (payout-zero/charge-two special case), unknown-code fallback (XXX) and lowercase inputs. - Add getAmountFromMinorUnits round-trip test via invokePrivateMethod so the read-back path is covered too.
This commit is contained in:
1 parent
304b731f4e
commit
8c50583a91
2 files changed
+41
-10
No files matched your search
@@ -22,11 +22,22 @@ use FOSSBilling\Doctrine\EntityManagerFactory;
|
||||
use FOSSBilling\Doctrine\NamedLock;
|
||||
use FOSSBilling\Period;
|
||||
use Stripe\StripeClient;
|
||||
use Symfony\Component\Intl\Currencies;
|
||||
|
||||
class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface, FOSSBilling\Payment\JsonWebhookAuthenticatorInterface
|
||||
{
|
||||
private const array STRIPE_ZERO_DECIMAL_CURRENCIES = ['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'];
|
||||
/**
|
||||
* Stripe's zero-decimal currencies for charges (https://docs.stripe.com/currencies).
|
||||
* UGX is on Stripe's published zero-decimal list, but its special-case row requires charge amounts ×100,
|
||||
* so it is deliberately excluded here. ISK was never on the list and likewise uses two decimals for
|
||||
* charges; HUF and TWD are zero-decimal for payouts only, so they stay ×100 here. This helper is
|
||||
* charge-side only (PaymentIntents, Prices) and must not be reused for payouts.
|
||||
*/
|
||||
private const array STRIPE_ZERO_DECIMAL_CHARGE_CURRENCIES = ['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'];
|
||||
|
||||
/**
|
||||
* Stripe's three-decimal currencies for charges (https://docs.stripe.com/currencies).
|
||||
*/
|
||||
private const array STRIPE_THREE_DECIMAL_CURRENCIES = ['BHD', 'JOD', 'KWD', 'OMR', 'TND'];
|
||||
|
||||
protected ?Pimple\Container $di = null;
|
||||
|
||||
@@ -262,17 +273,18 @@ class Payment_Adapter_Stripe implements FOSSBilling\InjectionAwareInterface, FOS
|
||||
}
|
||||
|
||||
/**
|
||||
* Stripe's own decimal rules, not CLDR's: Stripe's zero-decimal currencies are sent as-is, three-decimal ones
|
||||
* keep three digits, and every other currency (including COP, HUF, IDR, ISK, PKR and UGX, which CLDR lists with
|
||||
* no decimals) is sent ×100. See https://docs.stripe.com/currencies.
|
||||
* Stripe's own decimal rules, not CLDR's: the currencies in STRIPE_ZERO_DECIMAL_CHARGE_CURRENCIES are sent
|
||||
* as-is, the ones in STRIPE_THREE_DECIMAL_CURRENCIES keep three digits, and every other currency
|
||||
* (including COP, HUF, IDR, ISK, PKR and UGX, which CLDR lists with no decimals) is sent ×100.
|
||||
* See https://docs.stripe.com/currencies.
|
||||
*/
|
||||
private function getCurrencyFractionDigits(string $currency): int
|
||||
{
|
||||
$currency = strtoupper($currency);
|
||||
if (in_array($currency, self::STRIPE_ZERO_DECIMAL_CURRENCIES, true)) {
|
||||
if (in_array($currency, self::STRIPE_ZERO_DECIMAL_CHARGE_CURRENCIES, true)) {
|
||||
return 0;
|
||||
}
|
||||
if (Currencies::exists($currency) && Currencies::getFractionDigits($currency) === 3) {
|
||||
if (in_array($currency, self::STRIPE_THREE_DECIMAL_CURRENCIES, true)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
|
||||
@@ -2664,9 +2664,9 @@ describe('processTransaction payload handling', function (): void {
|
||||
});
|
||||
|
||||
describe('getAmountInMinorUnits follows Stripe currency rules', function (): void {
|
||||
// Stripe's zero-decimal list is "BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF", and ISK and UGX must
|
||||
// be sent as two-decimal values (https://docs.stripe.com/currencies). Symfony Intl reports COP, HUF, IDR, ISK, PKR, UGX
|
||||
// and others with no decimals, but Stripe expects them ×100.
|
||||
// Stripe lists "BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF UGX VND VUV XAF XOF XPF" as zero-decimal, but its
|
||||
// special-cases row requires UGX charges ×100, like ISK (https://docs.stripe.com/currencies). Symfony Intl
|
||||
// reports COP, HUF, IDR, ISK, PKR, UGX and others with no decimals, but Stripe expects them ×100.
|
||||
test('sends the invoice total in the unit Stripe expects', function (string $currency, int $expected): void {
|
||||
$invoice = createEntity(Invoice::class);
|
||||
$invoice->id = 5;
|
||||
@@ -2683,11 +2683,30 @@ describe('getAmountInMinorUnits follows Stripe currency rules', function (): voi
|
||||
'USD' => ['USD', 100000],
|
||||
'JPY (zero-decimal)' => ['JPY', 1000],
|
||||
'KWD (three-decimal)' => ['KWD', 1000000],
|
||||
'BHD (three-decimal)' => ['BHD', 1000000],
|
||||
'COP' => ['COP', 100000],
|
||||
'HUF' => ['HUF', 100000],
|
||||
'IDR' => ['IDR', 100000],
|
||||
'ISK (two-decimal representation)' => ['ISK', 100000],
|
||||
'PKR' => ['PKR', 100000],
|
||||
'UGX (two-decimal representation)' => ['UGX', 100000],
|
||||
'AFN' => ['AFN', 100000],
|
||||
'YER' => ['YER', 100000],
|
||||
'TWD (two-decimal for charges)' => ['TWD', 100000],
|
||||
'unknown code falls back to two decimals' => ['XXX', 100000],
|
||||
'lowercase USD' => ['usd', 100000],
|
||||
'lowercase JPY' => ['jpy', 1000],
|
||||
]);
|
||||
|
||||
test('reads Stripe amounts back in the same unit', function (string $currency, int $minorUnits, float $expected): void {
|
||||
expect(invokePrivateMethod($this->adapter, 'getAmountFromMinorUnits', [$minorUnits, $currency]))->toBe($expected);
|
||||
})->with([
|
||||
'USD' => ['USD', 100000, 1000.0],
|
||||
'JPY (zero-decimal)' => ['JPY', 1000, 1000.0],
|
||||
'KWD (three-decimal)' => ['KWD', 1000000, 1000.0],
|
||||
'HUF' => ['HUF', 100000, 1000.0],
|
||||
'UGX (two-decimal representation)' => ['UGX', 100000, 1000.0],
|
||||
'unknown code falls back to two decimals' => ['XXX', 100000, 1000.0],
|
||||
'lowercase currency' => ['usd', 100000, 1000.0],
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user