Page MenuHomePhorge

D2383.1775278554.diff
No OneTemporary

Authored By
Unknown
Size
28 KB
Referenced Files
None
Subscribers
None

D2383.1775278554.diff

diff --git a/src/app/Backends/OpenExchangeRates.php b/src/app/Backends/OpenExchangeRates.php
new file mode 100644
--- /dev/null
+++ b/src/app/Backends/OpenExchangeRates.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace App\Backends;
+
+class OpenExchangeRates
+{
+ /**
+ * Import exchange rates from openexchangerates.org
+ *
+ * @param string Base currency
+ *
+ * @return array exchange rates
+ */
+ public static function retrieveRates($baseCurrency)
+ {
+ $baseCurrency = strtoupper($baseCurrency);
+ $apiKey = \config('services.openexchangerates.api_key');
+ $query = http_build_query(['app_id' => $apiKey, 'base' => 'USD']);
+ $url = 'https://openexchangerates.org/api/latest.json?' . $query;
+ $html = file_get_contents($url, false);
+ $rates = [];
+
+ if ($html && ($result = json_decode($html, true)) && !empty($result['rates'])) {
+ foreach ($result['rates'] as $code => $rate) {
+ $rates[strtoupper($code)] = $rate;
+ }
+
+ if ($baseCurrency != 'USD') {
+ if ($base = $rates[$baseCurrency]) {
+ foreach ($rates as $code => $rate) {
+ $rates[$code] = $rate / $base;
+ }
+ } else {
+ $rates = [];
+ }
+ }
+
+ foreach ($rates as $code => $rate) {
+ \Log::debug(sprintf("Update %s: %0.8f", $code, $rate));
+ }
+ } else {
+ throw new \Exception("Failed to parse exchange rates");
+ }
+
+ if (count($rates) > 1) {
+ $rates[$baseCurrency] = 1;
+ return $rates;
+ }
+
+ throw new \Exception("Failed to retrieve exchange rates");
+ }
+}
diff --git a/src/app/Console/Commands/Data/Import/ExchangerateCommand.php b/src/app/Console/Commands/Data/Import/ExchangerateCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/ExchangerateCommand.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\Console\Command;
+
+class ExchangerateCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:exchangerates {sourceCurrency}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Fetches exchangerates from openexchangerates.org';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $sourceCurrency = strtoupper($this->argument('sourceCurrency'));
+ $rates = \App\Backends\OpenExchangeRates::retrieveRates($sourceCurrency);
+
+ //
+ // export
+ //
+ $file = resource_path("exchangerates-$sourceCurrency.php");
+
+ $out = "<?php return [\n";
+
+ foreach ($rates as $countryCode => $rate) {
+ $out .= sprintf(" '%s' => '%s',\n", $countryCode, $rate);
+ }
+
+ $out .= "];\n";
+
+ file_put_contents($file, $out);
+ }
+}
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -562,17 +562,17 @@
$providerMethods = array_merge(
// Fallback to EUR methods (later provider methods will override earlier ones)
- //mollie()->methods()->allActive(
- // [
- // 'sequenceType' => $type,
- // 'amount' => [
- // 'value' => '1.00',
- // 'currency' => 'EUR'
- // ]
- // ]
- //),
+ (array) mollie()->methods()->allActive(
+ [
+ 'sequenceType' => $type,
+ 'amount' => [
+ 'value' => '1.00',
+ 'currency' => 'EUR'
+ ]
+ ]
+ ),
// Prefer CHF methods
- (array)mollie()->methods()->allActive(
+ (array) mollie()->methods()->allActive(
[
'sequenceType' => $type,
'amount' => [
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -188,23 +188,6 @@
return $db_payment;
}
- /**
- * Retrieve an exchange rate.
- *
- * @param string $sourceCurrency Currency from which to convert
- * @param string $targetCurrency Currency to convert to
- *
- * @return float Exchange rate
- */
- protected function exchangeRate(string $sourceCurrency, string $targetCurrency): float
- {
- if (strcasecmp($sourceCurrency, $targetCurrency)) {
- throw new \Exception("Currency conversion is not yet implemented.");
- //FIXME Not yet implemented
- }
- return 1.0;
- }
-
/**
* Convert a value from $sourceCurrency to $targetCurrency
*
@@ -216,7 +199,7 @@
*/
protected function exchange(int $amount, string $sourceCurrency, string $targetCurrency): int
{
- return intval(round($amount * $this->exchangeRate($sourceCurrency, $targetCurrency)));
+ return intval(round($amount * \App\Utils::exchangeRate($sourceCurrency, $targetCurrency)));
}
/**
@@ -235,7 +218,7 @@
}
// Preserve originally refunded amount
- $refund['currency_amount'] = $refund['amount'];
+ $refund['currency_amount'] = $refund['amount'] * -1;
// Convert amount to wallet currency
// TODO We should possibly be using the same exchange rate as for the original payment?
@@ -301,34 +284,19 @@
*/
protected static function paymentMethodsWhitelist($type): array
{
+ $methods = [];
switch ($type) {
case self::TYPE_ONEOFF:
- return [
- self::METHOD_CREDITCARD => [
- 'id' => self::METHOD_CREDITCARD,
- 'icon' => self::$paymentMethodIcons[self::METHOD_CREDITCARD]
- ],
- self::METHOD_PAYPAL => [
- 'id' => self::METHOD_PAYPAL,
- 'icon' => self::$paymentMethodIcons[self::METHOD_PAYPAL]
- ],
- // TODO Enable once we're ready to offer them
- // self::METHOD_BANKTRANSFER => [
- // 'id' => self::METHOD_BANKTRANSFER,
- // 'icon' => self::$paymentMethodIcons[self::METHOD_BANKTRANSFER]
- // ]
- ];
+ $methods = explode(',', \config('app.payment.methods_oneoff'));
+ break;
case PaymentProvider::TYPE_RECURRING:
- return [
- self::METHOD_CREDITCARD => [
- 'id' => self::METHOD_CREDITCARD,
- 'icon' => self::$paymentMethodIcons[self::METHOD_CREDITCARD]
- ]
- ];
+ $methods = explode(',', \config('app.payment.methods_recurring'));
+ break;
+ default:
+ \Log::error("Unknown payment type: " . $type);
}
-
- \Log::error("Unknown payment type: " . $type);
- return [];
+ $methods = array_map('strtolower', array_map('trim', $methods));
+ return $methods;
}
/**
@@ -344,9 +312,11 @@
// Use only whitelisted methods, and apply values from whitelist (overriding the backend)
$whitelistMethods = self::paymentMethodsWhitelist($type);
- foreach ($whitelistMethods as $id => $whitelistMethod) {
+ foreach ($whitelistMethods as $id) {
if (array_key_exists($id, $availableMethods)) {
- $methods[] = array_merge($availableMethods[$id], $whitelistMethod);
+ $method = $availableMethods[$id];
+ $method['icon'] = self::$paymentMethodIcons[$id];
+ $methods[] = $method;
}
}
@@ -381,6 +351,8 @@
$provider = PaymentProvider::factory($providerName);
$methods = self::applyMethodWhitelist($type, $provider->providerPaymentMethods($type));
+ \Log::debug("Loaded payment methods" . var_export($methods, true));
+
Cache::put($cacheKey, $methods, now()->addHours(1));
return $methods;
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -5,6 +5,7 @@
use Carbon\Carbon;
use Illuminate\Support\Facades\Auth;
use Ramsey\Uuid\Uuid;
+use Illuminate\Support\Facades\Cache;
/**
* Small utility functions for App.
@@ -406,4 +407,37 @@
return $env;
}
+
+ /**
+ * Retrieve an exchange rate.
+ *
+ * @param string $sourceCurrency: Currency from which to convert
+ * @param string $targetCurrency: Currency to convert to
+ *
+ * @return float Exchange rate
+ */
+ public static function exchangeRate(string $sourceCurrency, string $targetCurrency): float
+ {
+ if (strcasecmp($sourceCurrency, $targetCurrency) == 0) {
+ return 1.0;
+ }
+
+ $currencyFile = resource_path("exchangerates-$sourceCurrency.php");
+
+ //Attempt to find the reverse exchange rate, if we don't have the file for the source currency
+ if (!file_exists($currencyFile)) {
+ $rates = include resource_path("exchangerates-$targetCurrency.php");
+ if (!isset($rates[$sourceCurrency])) {
+ throw new \Exception("Failed to find the reverse exchange rate for " . $sourceCurrency);
+ }
+ return 1.0 / floatval($rates[$sourceCurrency]);
+ }
+
+ $rates = include $currencyFile;
+ if (!isset($rates[$targetCurrency])) {
+ throw new \Exception("Failed to find exchange rate for " . $targetCurrency);
+ }
+
+ return floatval($rates[$targetCurrency]);
+ }
}
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -266,4 +266,9 @@
'countries' => env('VAT_COUNTRIES'),
'rate' => (float) env('VAT_RATE'),
],
+
+ 'payment' => [
+ 'methods_oneoff' => env('PAYMENT_METHODS_ONEOFF', "creditcard,paypal,banktransfer"),
+ 'methods_recurring' => env('PAYMENT_METHODS_RECURRING', "creditcard"),
+ ],
];
diff --git a/src/config/currency.php b/src/config/currency.php
deleted file mode 100644
--- a/src/config/currency.php
+++ /dev/null
@@ -1,116 +0,0 @@
-<?php
-
-return [
-
- /*
- |--------------------------------------------------------------------------
- | Application Currency
- |--------------------------------------------------------------------------
- |
- | The application currency determines the default currency that will be
- | used by the currency service provider. You are free to set this value
- | to any of the currencies which will be supported by the application.
- |
- */
-
- 'default' => 'USD',
-
- /*
- |--------------------------------------------------------------------------
- | API Key for OpenExchangeRates.org
- |--------------------------------------------------------------------------
- |
- | Only required if you with to use the Open Exchange Rates api. You can
- | always just use Yahoo, the current default.
- |
- */
-
- 'api_key' => env('OPENEXCHANGERATES_API_KEY', null),
-
- /*
- |--------------------------------------------------------------------------
- | Default Storage Driver
- |--------------------------------------------------------------------------
- |
- | Here you may specify the default storage driver that should be used
- | by the framework.
- |
- | Supported: "database", "filesystem"
- |
- */
-
- 'driver' => 'filesystem',
-
- /*
- |--------------------------------------------------------------------------
- | Default Storage Driver
- |--------------------------------------------------------------------------
- |
- | Here you may specify the default cache driver that should be used
- | by the framework.
- |
- | Supported: all cache drivers supported by Laravel
- |
- */
-
- 'cache_driver' => null,
-
- /*
- |--------------------------------------------------------------------------
- | Storage Specific Configuration
- |--------------------------------------------------------------------------
- |
- | Here you may configure as many storage drivers as you wish.
- |
- */
-
- 'drivers' => [
-
- 'database' => [
- 'class' => \Torann\Currency\Drivers\Database::class,
- 'connection' => null,
- 'table' => 'currencies',
- ],
-
- 'filesystem' => [
- 'class' => \Torann\Currency\Drivers\Filesystem::class,
- 'disk' => null,
- 'path' => 'currencies.json',
- ],
-
- ],
-
- /*
- |--------------------------------------------------------------------------
- | Currency Formatter
- |--------------------------------------------------------------------------
- |
- | Here you may configure a custom formatting of currencies. The reason for
- | this is to help further internationalize the formatting past the basic
- | format column in the table. When set to `null` the package will use the
- | format from storage.
- |
- | More info:
- | http://lyften.com/projects/laravel-currency/doc/formatting.html
- |
- */
-
- 'formatter' => null,
-
- /*
- |--------------------------------------------------------------------------
- | Currency Formatter Specific Configuration
- |--------------------------------------------------------------------------
- |
- | Here you may configure as many currency formatters as you wish.
- |
- */
-
- 'formatters' => [
-
- 'php_intl' => [
- 'class' => \Torann\Currency\Formatters\PHPIntl::class,
- ],
-
- ],
-];
diff --git a/src/config/services.php b/src/config/services.php
--- a/src/config/services.php
+++ b/src/config/services.php
@@ -46,4 +46,9 @@
'webhook_secret' => env('STRIPE_WEBHOOK_SECRET'),
],
+
+ 'openexchangerates' => [
+ 'api_key' => env('OPENEXCHANGERATES_API_KEY', null),
+ ]
+
];
diff --git a/src/resources/exchangerates-CHF.php b/src/resources/exchangerates-CHF.php
new file mode 100644
--- /dev/null
+++ b/src/resources/exchangerates-CHF.php
@@ -0,0 +1,172 @@
+<?php return [
+ 'AED' => '3.9552959825289',
+ 'AFN' => '83.613671484215',
+ 'ALL' => '111.47078618895',
+ 'AMD' => '568.26008190657',
+ 'ANG' => '1.9326009913646',
+ 'AOA' => '674.30453178322',
+ 'ARS' => '98.608363602295',
+ 'AUD' => '1.4005248654184',
+ 'AWG' => '1.9383622346732',
+ 'AZN' => '1.8315423225241',
+ 'BAM' => '1.765440939718',
+ 'BBD' => '2.1537358163036',
+ 'BDT' => '91.161248046831',
+ 'BGN' => '1.76640258276',
+ 'BHD' => '0.40598781631649',
+ 'BIF' => '2095.7472160272',
+ 'BMD' => '1.0768679081518',
+ 'BND' => '1.4428166987753',
+ 'BOB' => '7.4343600550926',
+ 'BRL' => '5.9304203338506',
+ 'BSD' => '1.0768679081518',
+ 'BTC' => '1.9792923685602E-5',
+ 'BTN' => '77.837962608992',
+ 'BWP' => '11.786340792079',
+ 'BYN' => '2.7980743448066',
+ 'BZD' => '2.1702905066556',
+ 'CAD' => '1.3518138224611',
+ 'CDF' => '2116.9551990644',
+ 'CHF' => '1',
+ 'CLF' => '0.027974874517967',
+ 'CLP' => '771.89865273056',
+ 'CNH' => '7.0074023900006',
+ 'CNY' => '7.0104100820681',
+ 'COP' => '3842.7175763149',
+ 'CRC' => '657.98680298379',
+ 'CUC' => '1.0768679081518',
+ 'CUP' => '27.729348634908',
+ 'CVE' => '61.047641713125',
+ 'CZK' => '23.665033775962',
+ 'DJF' => '191.67469005049',
+ 'DKK' => '6.7297783052037',
+ 'DOP' => '61.257419889104',
+ 'DZD' => '143.75354477994',
+ 'EGP' => '16.95528521385',
+ 'ERN' => '16.154426088633',
+ 'ETB' => '44.007619917318',
+ 'EUR' => '0.90503424978382',
+ 'FJD' => '2.1941722062547',
+ 'FKP' => '0.77972343878383',
+ 'GBP' => '0.77972343878383',
+ 'GEL' => '3.5805857946047',
+ 'GGP' => '0.77972343878383',
+ 'GHS' => '6.1747045882111',
+ 'GIP' => '0.77972343878383',
+ 'GMD' => '55.189480292779',
+ 'GNF' => '10817.922694883',
+ 'GTQ' => '8.3118318707672',
+ 'GYD' => '225.03202066725',
+ 'HKD' => '8.3636130641307',
+ 'HNL' => '25.899206240665',
+ 'HRK' => '6.85609491083',
+ 'HTG' => '85.509264833048',
+ 'HUF' => '331.7893366386',
+ 'IDR' => '15495.725407298',
+ 'ILS' => '3.5472890388846',
+ 'IMP' => '0.77972343878383',
+ 'INR' => '77.954048969491',
+ 'IQD' => '1570.6716338994',
+ 'IRR' => '45341.523272731',
+ 'ISK' => '135.22230322662',
+ 'JEP' => '0.77972343878383',
+ 'JMD' => '157.70066841191',
+ 'JOD' => '0.76349934687961',
+ 'JPY' => '117.11369248314',
+ 'KES' => '118.29393971047',
+ 'KGS' => '91.318753977681',
+ 'KHR' => '4376.275274359',
+ 'KMF' => '444.07379452714',
+ 'KPW' => '969.1811173366',
+ 'KRW' => '1215.4069376138',
+ 'KWD' => '0.32537563844806',
+ 'KYD' => '0.89714942296033',
+ 'KZT' => '453.27712011062',
+ 'LAK' => '10084.767400839',
+ 'LBP' => '1627.9546875522',
+ 'LKR' => '213.44889346438',
+ 'LRD' => '186.83658529494',
+ 'LSL' => '15.830499914389',
+ 'LYD' => '4.8516237552753',
+ 'MAD' => '9.6543512463131',
+ 'MDL' => '19.238468090789',
+ 'MGA' => '4077.6026098971',
+ 'MKD' => '55.639268634391',
+ 'MMK' => '1518.1264813664',
+ 'MNT' => '3063.4631479649',
+ 'MOP' => '8.6120551054846',
+ 'MRO' => '384.44165798891',
+ 'MRU' => '38.706188436808',
+ 'MUR' => '43.397775621649',
+ 'MVR' => '16.648377860027',
+ 'MWK' => '845.13715851173',
+ 'MXN' => '22.260479270831',
+ 'MYR' => '4.4366957815853',
+ 'MZN' => '77.618485083764',
+ 'NAD' => '15.851495607994',
+ 'NGN' => '443.32165505983',
+ 'NIO' => '37.576054334447',
+ 'NOK' => '9.1987650478829',
+ 'NPR' => '124.54219760741',
+ 'NZD' => '1.5235527164531',
+ 'OMR' => '0.41463183501522',
+ 'PAB' => '1.0768679081518',
+ 'PEN' => '3.9984923849286',
+ 'PGK' => '3.7984060201224',
+ 'PHP' => '52.354625524569',
+ 'PKR' => '167.28288781513',
+ 'PLN' => '4.1605933111427',
+ 'PYG' => '7037.5479933105',
+ 'QAR' => '3.9208760535806',
+ 'RON' => '4.4244625621487',
+ 'RSD' => '106.13422512354',
+ 'RUB' => '81.443654502008',
+ 'RWF' => '1070.2236170055',
+ 'SAR' => '4.0385831002812',
+ 'SBD' => '8.5811651495393',
+ 'SCR' => '22.836425918488',
+ 'SDG' => '409.74823905175',
+ 'SEK' => '9.2106892062299',
+ 'SGD' => '1.4442252419991',
+ 'SHP' => '0.77972343878383',
+ 'SLL' => '11009.305450352',
+ 'SOS' => '622.83776231156',
+ 'SRD' => '15.24198837198',
+ 'SSP' => '140.27281371585',
+ 'STD' => '22204.04937224',
+ 'STN' => '22.291165698742',
+ 'SVC' => '9.4201120157998',
+ 'SYP' => '1086.8852521863',
+ 'SZL' => '15.823320436045',
+ 'THB' => '33.332123292761',
+ 'TJS' => '12.273947657758',
+ 'TMT' => '3.7690376785312',
+ 'TND' => '2.9457721627492',
+ 'TOP' => '2.4455788649597',
+ 'TRY' => '8.3189327377536',
+ 'TTD' => '7.3114183534905',
+ 'TWD' => '30.643246584444',
+ 'TZS' => '2497.256679004',
+ 'UAH' => '29.808460735781',
+ 'UGX' => '3945.9795987375',
+ 'USD' => '1.0768679081518',
+ 'UYU' => '47.707876965688',
+ 'UZS' => '11308.539064999',
+ 'VES' => '1941046.2073111',
+ 'VND' => '24912.524365752',
+ 'VUV' => '116.88367026735',
+ 'WST' => '2.7063122766172',
+ 'XAF' => '593.66326771259',
+ 'XAG' => '0.042055320858178',
+ 'XAU' => '0.0006191236664337',
+ 'XCD' => '2.9102893651756',
+ 'XDR' => '0.75382584246069',
+ 'XOF' => '593.66326771259',
+ 'XPD' => '0.00041475567482466',
+ 'XPF' => '107.99926234548',
+ 'XPT' => '0.00091222557367446',
+ 'YER' => '269.64770697132',
+ 'ZAR' => '15.946586274888',
+ 'ZMW' => '23.740867890922',
+ 'ZWL' => '346.75146642487',
+];
diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php
--- a/src/tests/Browser/PaymentMollieTest.php
+++ b/src/tests/Browser/PaymentMollieTest.php
@@ -58,7 +58,7 @@
$browser->assertSeeIn('@title', 'Top up your wallet')
->waitFor('#payment-method-selection #creditcard')
->waitFor('#payment-method-selection #paypal')
- ->assertMissing('#payment-method-selection #banktransfer')
+ ->waitFor('#payment-method-selection #banktransfer')
->click('#creditcard');
})
->with(new Dialog('@payment-dialog'), function (Browser $browser) {
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieTest.php
@@ -374,6 +374,8 @@
$this->assertCount(1, $payments);
$payment = $payments[0];
$this->assertSame(1234, $payment->amount);
+ $this->assertSame(1234, $payment->currency_amount);
+ $this->assertSame('CHF', $payment->currency);
$this->assertSame(\config('app.name') . ' Payment', $payment->description);
$this->assertSame('open', $payment->status);
$this->assertEquals(0, $wallet->balance);
@@ -468,6 +470,51 @@
Bus::assertDispatchedTimes(\App\Jobs\PaymentEmail::class, 0);
}
+ /**
+ * Test creating a payment and receiving a status via webhook using a foreign currency
+ *
+ * @group mollie
+ */
+ public function testStoreAndWebhookForeignCurrency(): void
+ {
+ Bus::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
+
+ // Successful payment in EUR
+ $post = ['amount' => '12.34', 'currency' => 'EUR', 'methodId' => 'banktransfer'];
+ $response = $this->actingAs($user)->post("api/v4/payments", $post);
+ $response->assertStatus(200);
+
+ $payment = $wallet->payments()
+ ->where('currency', 'EUR')->get()->last();
+
+ $this->assertSame(1234, $payment->amount);
+ $this->assertSame(1117, $payment->currency_amount);
+ $this->assertSame('EUR', $payment->currency);
+ $this->assertEquals(0, $wallet->balance);
+
+ $mollie_response = [
+ "resource" => "payment",
+ "id" => $payment->id,
+ "status" => "paid",
+ // Status is not enough, paidAt is used to distinguish the state
+ "paidAt" => date('c'),
+ "mode" => "test",
+ ];
+
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $post = ['id' => $payment->id];
+ $response = $this->post("api/webhooks/payment/mollie", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame(PaymentProvider::STATUS_PAID, $payment->fresh()->status);
+ $this->assertEquals(1234, $wallet->fresh()->balance);
+ }
+
/**
* Test automatic payment charges
*
@@ -496,7 +543,9 @@
$payments = $wallet->payments()->orderBy('amount')->get();
$this->assertCount(2, $payments);
$this->assertSame(0, $payments[0]->amount);
+ $this->assertSame(0, $payments[0]->currency_amount);
$this->assertSame(2010, $payments[1]->amount);
+ $this->assertSame(2010, $payments[1]->currency_amount);
$payment = $payments[1];
// In mollie we don't have to wait for a webhook, the response to
@@ -734,6 +783,7 @@
$this->assertCount(1, $payments);
$this->assertSame(-101, $payments[0]->amount);
+ $this->assertSame(-101, $payments[0]->currency_amount);
$this->assertSame(PaymentProvider::STATUS_PAID, $payments[0]->status);
$this->assertSame(PaymentProvider::TYPE_REFUND, $payments[0]->type);
$this->assertSame("mollie", $payments[0]->provider);
@@ -801,6 +851,95 @@
$this->unmockMollie();
}
+ /**
+ * Test refund/chargeback handling by the webhook in a foreign currency
+ *
+ * @group mollie
+ */
+ public function testRefundAndChargebackForeignCurrency(): void
+ {
+ Bus::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
+ $wallet->transactions()->delete();
+
+ $mollie = PaymentProvider::factory('mollie');
+
+ // Create a paid payment
+ $payment = Payment::create([
+ 'id' => 'tr_123456',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'amount' => 1234,
+ 'currency_amount' => 1117,
+ 'currency' => 'EUR',
+ 'type' => PaymentProvider::TYPE_ONEOFF,
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'mollie',
+ 'description' => 'test',
+ ]);
+
+ // Test handling a refund by the webhook
+
+ $mollie_response1 = [
+ "resource" => "payment",
+ "id" => $payment->id,
+ "status" => "paid",
+ // Status is not enough, paidAt is used to distinguish the state
+ "paidAt" => date('c'),
+ "mode" => "test",
+ "_links" => [
+ "refunds" => [
+ "href" => "https://api.mollie.com/v2/payments/{$payment->id}/refunds",
+ "type" => "application/hal+json"
+ ]
+ ]
+ ];
+
+ $mollie_response2 = [
+ "count" => 1,
+ "_links" => [],
+ "_embedded" => [
+ "refunds" => [
+ [
+ "resource" => "refund",
+ "id" => "re_123456",
+ "status" => \Mollie\Api\Types\RefundStatus::STATUS_REFUNDED,
+ "paymentId" => $payment->id,
+ "description" => "refund desc",
+ "amount" => [
+ "currency" => "EUR",
+ "value" => "1.01",
+ ],
+ ]
+ ]
+ ]
+ ];
+
+ // We'll trigger the webhook with payment id and use mocking for
+ // requests to the Mollie payments API.
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response1)));
+ $responseStack->append(new Response(200, [], json_encode($mollie_response2)));
+
+ $post = ['id' => $payment->id];
+ $response = $this->post("api/webhooks/payment/mollie", $post);
+ $response->assertStatus(200);
+
+ $wallet->refresh();
+
+ $this->assertEquals(-112, $wallet->balance);
+
+ $payments = $wallet->payments()->where('id', 're_123456')->get();
+
+ $this->assertCount(1, $payments);
+ $this->assertSame(-112, $payments[0]->amount);
+ $this->assertSame(-101, $payments[0]->currency_amount);
+ $this->assertSame('EUR', $payments[0]->currency);
+
+ $this->unmockMollie();
+ }
+
/**
* Create Mollie's auto-payment mandate using our API and Chrome browser
*/
@@ -906,9 +1045,10 @@
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(2, $json);
+ $this->assertCount(3, $json);
$this->assertSame('creditcard', $json[0]['id']);
$this->assertSame('paypal', $json[1]['id']);
+ $this->assertSame('banktransfer', $json[2]['id']);
$response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING);
$response->assertStatus(200);
diff --git a/src/tests/Unit/UtilsTest.php b/src/tests/Unit/UtilsTest.php
--- a/src/tests/Unit/UtilsTest.php
+++ b/src/tests/Unit/UtilsTest.php
@@ -101,4 +101,18 @@
$this->assertTrue(strlen($result) === 36);
$this->assertTrue(preg_match('/[^a-f0-9-]/i', $result) === 0);
}
+
+ /**
+ * Test for Utils::exchangeRate()
+ */
+ public function testExchangeRate(): void
+ {
+ $this->assertSame(1.0, Utils::exchangeRate("DUMMY", "dummy"));
+ $this->assertEqualsWithDelta(0.90503424978382, Utils::exchangeRate("CHF", "EUR"), PHP_FLOAT_EPSILON);
+ $this->assertEqualsWithDelta(1.1049305595217682, Utils::exchangeRate("EUR", "CHF"), PHP_FLOAT_EPSILON);
+ $this->expectException(\Exception::class);
+ $this->assertSame(1.0, Utils::exchangeRate("CHF", "FOO"));
+ $this->expectException(\Exception::class);
+ $this->assertSame(1.0, Utils::exchangeRate("FOO", "CHF"));
+ }
}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 4:55 AM (21 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822080
Default Alt Text
D2383.1775278554.diff (28 KB)

Event Timeline