diff --git a/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php b/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php
--- a/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php
+++ b/src/app/Console/Commands/Data/Import/OpenExchangeRatesCommand.php
@@ -27,23 +27,20 @@
*/
public function handle()
{
- $sourceCurrency = 'CHF';
+ foreach (['CHF', 'EUR'] as $sourceCurrency) {
+ $rates = \App\Backends\OpenExchangeRates::retrieveRates($sourceCurrency);
- $rates = \App\Backends\OpenExchangeRates::retrieveRates($sourceCurrency);
+ $file = resource_path("exchangerates-$sourceCurrency.php");
- //
- // export
- //
- $file = resource_path("exchangerates-$sourceCurrency.php");
+ $out = " $rate) {
+ $out .= sprintf(" '%s' => '%s',\n", $countryCode, $rate);
+ }
- foreach ($rates as $countryCode => $rate) {
- $out .= sprintf(" '%s' => '%s',\n", $countryCode, $rate);
- }
-
- $out .= "];\n";
+ $out .= "];\n";
- file_put_contents($file, $out);
+ file_put_contents($file, $out);
+ }
}
}
diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php
--- a/src/app/Documents/Receipt.php
+++ b/src/app/Documents/Receipt.php
@@ -50,7 +50,7 @@
{
$wallet = new Wallet();
$wallet->id = \App\Utils::uuidStr();
- $wallet->owner = new User(['id' => 123456789]); // @phpstan-ignore-line
+ $wallet->owner = new User(['id' => 123456789]);
$receipt = new self($wallet, date('Y'), date('n'));
diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php
--- a/src/app/Http/Controllers/API/V4/PaymentsController.php
+++ b/src/app/Http/Controllers/API/V4/PaymentsController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Providers\PaymentProvider;
+use App\Tenant;
use App\Wallet;
use App\Payment;
use Illuminate\Http\Request;
@@ -53,8 +54,8 @@
]);
$mandate = [
- 'currency' => 'CHF',
- 'description' => \config('app.name') . ' Auto-Payment Setup',
+ 'currency' => $wallet->currency,
+ 'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Auto-Payment Setup',
'methodId' => $request->methodId
];
@@ -173,7 +174,7 @@
}
if ($amount < PaymentProvider::MIN_AMOUNT) {
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
return ['amount' => \trans('validation.minamount', ['amount' => $min])];
}
@@ -211,7 +212,7 @@
// Validate the minimum value
if ($amount < PaymentProvider::MIN_AMOUNT) {
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$errors = ['amount' => \trans('validation.minamount', ['amount' => $min])];
return response()->json(['status' => 'error', 'errors' => $errors], 422);
}
@@ -221,7 +222,7 @@
'currency' => $request->currency,
'amount' => $amount,
'methodId' => $request->methodId,
- 'description' => \config('app.name') . ' Payment',
+ 'description' => Tenant::getConfig($user->tenant_id, 'app.name') . ' Payment',
];
$provider = PaymentProvider::factory($wallet);
@@ -327,10 +328,10 @@
$request = [
'type' => PaymentProvider::TYPE_RECURRING,
- 'currency' => 'CHF',
+ 'currency' => $wallet->currency,
'amount' => $amount,
'methodId' => PaymentProvider::METHOD_CREDITCARD,
- 'description' => \config('app.name') . ' Recurring Payment',
+ 'description' => Tenant::getConfig($wallet->owner->tenant_id, 'app.name') . ' Recurring Payment',
];
$result = $provider->payment($wallet, $request);
@@ -449,7 +450,7 @@
$hasMore = true;
}
- $result = $result->map(function ($item) {
+ $result = $result->map(function ($item) use ($wallet) {
$provider = PaymentProvider::factory($item->provider);
$payment = $provider->getPayment($item->id);
$entry = [
@@ -458,6 +459,8 @@
'type' => $item->type,
'description' => $item->description,
'amount' => $item->amount,
+ 'currency' => $wallet->currency,
+ // note: $item->currency/$item->currency_amount might be different
'status' => $item->status,
'isCancelable' => $payment['isCancelable'],
'checkoutUrl' => $payment['checkoutUrl']
diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/WalletsController.php
@@ -256,13 +256,14 @@
}
}
- $result = $result->map(function ($item) use ($isAdmin) {
+ $result = $result->map(function ($item) use ($isAdmin, $wallet) {
$entry = [
'id' => $item->id,
'createdAt' => $item->created_at->format('Y-m-d H:i'),
'type' => $item->type,
'description' => $item->shortDescription(),
'amount' => $item->amount,
+ 'currency' => $wallet->currency,
'hasDetails' => !empty($item->cnt),
];
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -56,7 +56,7 @@
{
$settings = [
'country' => \App\Utils::countryForRequest(),
- 'currency' => 'CHF',
+ 'currency' => \config('app.currency'),
/*
'first_name' => '',
'last_name' => '',
diff --git a/src/app/Observers/WalletObserver.php b/src/app/Observers/WalletObserver.php
--- a/src/app/Observers/WalletObserver.php
+++ b/src/app/Observers/WalletObserver.php
@@ -25,6 +25,8 @@
break;
}
}
+
+ $wallet->currency = \config('app.currency');
}
/**
diff --git a/src/app/Payment.php b/src/app/Payment.php
--- a/src/app/Payment.php
+++ b/src/app/Payment.php
@@ -7,7 +7,7 @@
/**
* A payment operation on a wallet.
*
- * @property int $amount Amount of money in cents of CHF
+ * @property int $amount Amount of money in cents of system currency
* @property string $description Payment description
* @property string $id Mollie's Payment ID
* @property \App\Wallet $wallet The wallet
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
@@ -547,6 +547,7 @@
* List supported payment methods.
*
* @param string $type The payment type for which we require a method (oneoff/recurring).
+ * @param string $currency Currency code
*
* @return array Array of array with available payment methods:
* - id: id of the method
@@ -556,30 +557,34 @@
* - exchangeRate: The projected exchange rate (actual rate is determined during payment)
* - icon: An icon (icon name) representing the method
*/
- public function providerPaymentMethods($type): array
+ public function providerPaymentMethods(string $type, string $currency): array
{
- $providerMethods = array_merge(
- // Fallback to EUR methods (later provider methods will override earlier ones)
- (array) mollie()->methods()->allActive(
- [
- 'sequenceType' => $type,
- 'amount' => [
- 'value' => '1.00',
- 'currency' => 'EUR'
- ]
+ // Prefer methods in the system currency
+ $providerMethods = (array) mollie()->methods()->allActive(
+ [
+ 'sequenceType' => $type,
+ 'amount' => [
+ 'value' => '1.00',
+ 'currency' => $currency
]
- ),
- // Prefer CHF methods
- (array) mollie()->methods()->allActive(
+ ]
+ );
+
+ // Get EUR methods (e.g. bank transfers are in EUR only)
+ if ($currency != 'EUR') {
+ $eurMethods = (array) mollie()->methods()->allActive(
[
'sequenceType' => $type,
'amount' => [
'value' => '1.00',
- 'currency' => 'CHF'
+ 'currency' => 'EUR'
]
]
- )
- );
+ );
+
+ // Later provider methods will override earlier ones
+ $providerMethods = array_merge($eurMethods, $providerMethods);
+ }
$availableMethods = [];
@@ -589,7 +594,7 @@
'name' => $method->description,
'minimumAmount' => round(floatval($method->minimumAmount->value) * 100), // Converted to cents
'currency' => $method->minimumAmount->currency,
- 'exchangeRate' => \App\Utils::exchangeRate('CHF', $method->minimumAmount->currency)
+ 'exchangeRate' => \App\Utils::exchangeRate($currency, $method->minimumAmount->currency)
];
}
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -483,7 +483,8 @@
/**
* List supported payment methods.
*
- * @param string $type The payment type for which we require a method (oneoff/recurring).
+ * @param string $type The payment type for which we require a method (oneoff/recurring).
+ * @param string $currency Currency code
*
* @return array Array of array with available payment methods:
* - id: id of the method
@@ -493,7 +494,7 @@
* - exchangeRate: The projected exchange rate (actual rate is determined during payment)
* - icon: An icon (icon name) representing the method
*/
- public function providerPaymentMethods($type): array
+ public function providerPaymentMethods(string $type, string $currency): array
{
//TODO get this from the stripe API?
$availableMethods = [];
@@ -504,14 +505,14 @@
'id' => self::METHOD_CREDITCARD,
'name' => "Credit Card",
'minimumAmount' => self::MIN_AMOUNT,
- 'currency' => 'CHF',
+ 'currency' => $currency,
'exchangeRate' => 1.0
],
self::METHOD_PAYPAL => [
'id' => self::METHOD_PAYPAL,
'name' => "PayPal",
'minimumAmount' => self::MIN_AMOUNT,
- 'currency' => 'CHF',
+ 'currency' => $currency,
'exchangeRate' => 1.0
]
];
@@ -522,7 +523,7 @@
'id' => self::METHOD_CREDITCARD,
'name' => "Credit Card",
'minimumAmount' => self::MIN_AMOUNT, // Converted to cents,
- 'currency' => 'CHF',
+ 'currency' => $currency,
'exchangeRate' => 1.0
]
];
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
@@ -254,7 +254,8 @@
/**
* List supported payment methods from this provider
*
- * @param string $type The payment type for which we require a method (oneoff/recurring).
+ * @param string $type The payment type for which we require a method (oneoff/recurring).
+ * @param string $currency Currency code
*
* @return array Array of array with available payment methods:
* - id: id of the method
@@ -264,7 +265,7 @@
* - exchangeRate: The projected exchange rate (actual rate is determined during payment)
* - icon: An icon (icon name) representing the method
*/
- abstract public function providerPaymentMethods($type): array;
+ abstract public function providerPaymentMethods(string $type, string $currency): array;
/**
* Get a payment.
@@ -345,7 +346,7 @@
{
$providerName = self::providerName($wallet);
- $cacheKey = "methods-" . $providerName . '-' . $type;
+ $cacheKey = "methods-{$providerName}-{$type}-{$wallet->currency}";
if ($methods = Cache::get($cacheKey)) {
\Log::debug("Using payment method cache" . var_export($methods, true));
@@ -353,7 +354,8 @@
}
$provider = PaymentProvider::factory($providerName);
- $methods = self::applyMethodWhitelist($type, $provider->providerPaymentMethods($type));
+ $methods = $provider->providerPaymentMethods($type, $wallet->currency);
+ $methods = self::applyMethodWhitelist($type, $methods);
\Log::debug("Loaded payment methods" . var_export($methods, true));
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -14,7 +14,12 @@
*
* A wallet is owned by an {@link \App\User}.
*
- * @property integer $balance
+ * @property int $balance Current balance in cents
+ * @property string $currency Currency code
+ * @property ?string $description Description
+ * @property string $id Unique identifier
+ * @property ?\App\User $owner Owner (can be null when owner is deleted)
+ * @property int $user_id Owner's identifier
*/
class Wallet extends Model
{
@@ -26,19 +31,39 @@
public $timestamps = false;
+ /**
+ * The attributes' default values.
+ *
+ * @var array
+ */
protected $attributes = [
'balance' => 0,
- 'currency' => 'CHF'
];
+ /**
+ * The attributes that are mass assignable.
+ *
+ * @var array
+ */
protected $fillable = [
- 'currency'
+ 'currency',
+ 'description'
];
+ /**
+ * The attributes that can be not set.
+ *
+ * @var array
+ */
protected $nullable = [
'description',
];
+ /**
+ * The types of attributes to which its values will be cast
+ *
+ * @var array
+ */
protected $casts = [
'balance' => 'integer',
];
@@ -345,15 +370,10 @@
{
$amount = round($amount / 100, 2);
- // Prefer intl extension's number formatter
- if (class_exists('NumberFormatter')) {
- $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
- $result = $nf->formatCurrency($amount, $this->currency);
- // Replace non-breaking space
- return str_replace("\xC2\xA0", " ", $result);
- }
-
- return sprintf('%.2f %s', $amount, $this->currency);
+ $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+ $result = $nf->formatCurrency($amount, $this->currency);
+ // Replace non-breaking space
+ return str_replace("\xC2\xA0", " ", $result);
}
/**
diff --git a/src/config/app.php b/src/config/app.php
--- a/src/config/app.php
+++ b/src/config/app.php
@@ -69,6 +69,8 @@
'tenant_id' => env('APP_TENANT_ID', null),
+ 'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')),
+
/*
|--------------------------------------------------------------------------
| Application Domain
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -331,7 +331,7 @@
'add-penalty' => "Add penalty",
'add-penalty-title' => "Add a penalty to the wallet",
'auto-payment' => "Auto-payment",
- 'auto-payment-text' => "Fill up by {amount} CHF when under {balance} CHF using {method}",
+ 'auto-payment-text' => "Fill up by {amount} when under {balance} using {method}",
'country' => "Country",
'create' => "Create user",
'custno' => "Customer No.",
@@ -390,7 +390,7 @@
. " You can cancel or change the auto-payment option at any time.",
'auto-payment-setup' => "Set up auto-payment",
'auto-payment-disabled' => "The configured auto-payment has been disabled. Top up your wallet or raise the auto-payment amount.",
- 'auto-payment-info' => "Auto-payment is set to fill up your account by {amount} CHF every time your account balance gets under {balance} CHF.",
+ 'auto-payment-info' => "Auto-payment is set to fill up your account by {amount} every time your account balance gets under {balance}.",
'auto-payment-inprogress' => "The setup of the automatic payment is still in progress.",
'auto-payment-next' => "Next, you will be redirected to the checkout page, where you can provide your credit card details.",
'auto-payment-disabled-next' => "The auto-payment is disabled. Immediately after you submit new settings we'll enable it and attempt to top up your wallet.",
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -128,7 +128,7 @@
{{ $t('wallet.title') }}
- {{ $root.price(wallet.balance) }}
+ {{ $root.price(wallet.balance, wallet.currency) }}
-
+
{{ $t('wallet.payment-method', { method: mandate.method }) }}
diff --git a/src/resources/vue/Widgets/PaymentLog.vue b/src/resources/vue/Widgets/PaymentLog.vue
--- a/src/resources/vue/Widgets/PaymentLog.vue
+++ b/src/resources/vue/Widgets/PaymentLog.vue
@@ -69,7 +69,7 @@
})
},
amount(payment) {
- return this.$root.price(payment.amount)
+ return this.$root.price(payment.amount, payment.currency)
}
}
}
diff --git a/src/resources/vue/Widgets/TransactionLog.vue b/src/resources/vue/Widgets/TransactionLog.vue
--- a/src/resources/vue/Widgets/TransactionLog.vue
+++ b/src/resources/vue/Widgets/TransactionLog.vue
@@ -103,7 +103,7 @@
})
},
amount(transaction) {
- return this.$root.price(transaction.amount)
+ return this.$root.price(transaction.amount, transaction.currency)
},
className(transaction) {
return transaction.amount < 0 ? 'text-danger' : 'text-success';
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
@@ -78,7 +78,7 @@
->click('@button-action');
})
->on(new PaymentMollie())
- ->assertSeeIn('@title', \config('app.name') . ' Payment')
+ ->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34');
$this->assertSame(1, $user->wallets()->first()->payments()->count());
@@ -168,7 +168,7 @@
->click('@button-action');
})
->on(new PaymentMollie())
- ->assertSeeIn('@title', \config('app.name') . ' Auto-Payment Setup')
+ ->assertSeeIn('@title', $user->tenant->title . ' Auto-Payment Setup')
->assertMissing('@amount')
->submitValidCreditCard()
->waitForLocation('/wallet')
diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php
--- a/src/tests/Browser/PaymentStripeTest.php
+++ b/src/tests/Browser/PaymentStripeTest.php
@@ -78,7 +78,7 @@
->click('@button-action');
})
->on(new PaymentStripe())
- ->assertSeeIn('@title', \config('app.name') . ' Payment')
+ ->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34')
->assertValue('@email-input', $user->email)
->submitValidCreditCard();
diff --git a/src/tests/Browser/Reseller/PaymentMollieTest.php b/src/tests/Browser/Reseller/PaymentMollieTest.php
--- a/src/tests/Browser/Reseller/PaymentMollieTest.php
+++ b/src/tests/Browser/Reseller/PaymentMollieTest.php
@@ -83,7 +83,7 @@
->click('@button-action');
})
->on(new PaymentMollie())
- ->assertSeeIn('@title', \config('app.name') . ' Payment')
+ ->assertSeeIn('@title', $user->tenant->title . ' Payment')
->assertSeeIn('@amount', 'CHF 12.34');
$this->assertSame(1, $wallet->payments()->count());
diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
copy from src/tests/Feature/Controller/PaymentsMollieTest.php
copy to src/tests/Feature/Controller/PaymentsMollieEuroTest.php
--- a/src/tests/Feature/Controller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/PaymentsMollieEuroTest.php
@@ -14,7 +14,7 @@
use Tests\BrowserAddonTrait;
use Tests\MollieMocksTrait;
-class PaymentsMollieTest extends TestCase
+class PaymentsMollieEuroTest extends TestCase
{
use MollieMocksTrait;
use BrowserAddonTrait;
@@ -28,18 +28,6 @@
// All tests in this file use Mollie
\config(['services.payment_provider' => 'mollie']);
-
- $john = $this->getTestUser('john@kolab.org');
- $wallet = $john->wallets()->first();
- Payment::where('wallet_id', $wallet->id)->delete();
- Wallet::where('id', $wallet->id)->update(['balance' => 0]);
- WalletSetting::where('wallet_id', $wallet->id)->delete();
- $types = [
- Transaction::WALLET_CREDIT,
- Transaction::WALLET_REFUND,
- Transaction::WALLET_CHARGEBACK,
- ];
- Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
}
/**
@@ -47,17 +35,7 @@
*/
public function tearDown(): void
{
- $john = $this->getTestUser('john@kolab.org');
- $wallet = $john->wallets()->first();
- Payment::where('wallet_id', $wallet->id)->delete();
- Wallet::where('id', $wallet->id)->update(['balance' => 0]);
- WalletSetting::where('wallet_id', $wallet->id)->delete();
- $types = [
- Transaction::WALLET_CREDIT,
- Transaction::WALLET_REFUND,
- Transaction::WALLET_CHARGEBACK,
- ];
- Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete();
+ $this->deleteTestUser('euro@' . \config('app.domain'));
parent::tearDown();
}
@@ -79,8 +57,10 @@
$response = $this->delete("api/v4/payments/mandate");
$response->assertStatus(401);
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
$wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
// Test creating a mandate (invalid input)
$post = [];
@@ -114,8 +94,9 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
+ $this->assertMatchesRegularExpression("/[0-9.,]+ €\.$/", $json['errors']['amount']);
// Test creating a mandate (negative balance, amount too small)
Wallet::where('id', $wallet->id)->update(['balance' => -2000]);
@@ -143,7 +124,7 @@
$payment = Payment::where('id', $json['id'])->first();
$this->assertSame(2010, $payment->amount);
$this->assertSame($wallet->id, $payment->wallet_id);
- $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description);
+ $this->assertSame($user->tenant->title . " Auto-Payment Setup", $payment->description);
$this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
// Test fetching the mandate information
@@ -220,6 +201,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
+ $this->assertMatchesRegularExpression("/[0-9.,]+ €\.$/", $json['errors']['amount']);
// Test updating a mandate (valid input)
$responseStack->append(new Response(200, [], json_encode($mollie_response)));
@@ -339,9 +321,12 @@
$response = $this->post("api/v4/payments", []);
$response->assertStatus(401);
- // Invalid amount
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
+ // Invalid amount
$post = ['amount' => -1];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
$response->assertStatus(422);
@@ -350,8 +335,9 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
+ $this->assertMatchesRegularExpression("/[0-9.,]+ €\.$/", $json['errors']['amount']);
// Invalid currency
$post = ['amount' => '12.34', 'currency' => 'FOO', 'methodId' => 'creditcard'];
@@ -359,7 +345,7 @@
$response->assertStatus(500);
// Successful payment
- $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard'];
+ $post = ['amount' => '12.34', 'currency' => 'EUR', 'methodId' => 'creditcard'];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
$response->assertStatus(200);
@@ -368,15 +354,14 @@
$this->assertSame('success', $json['status']);
$this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']);
- $wallet = $user->wallets()->first();
$payments = Payment::where('wallet_id', $wallet->id)->get();
$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('EUR', $payment->currency);
+ $this->assertSame($user->tenant->title . ' Payment', $payment->description);
$this->assertSame('open', $payment->status);
$this->assertEquals(0, $wallet->balance);
@@ -470,51 +455,6 @@
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
*
@@ -524,8 +464,10 @@
{
Bus::fake();
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
$wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
// Create a valid mandate first (balance=0, so there's no extra payment yet)
$this->createMandate($wallet, ['amount' => 20.10, 'balance' => 0]);
@@ -702,8 +644,10 @@
{
Bus::fake();
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
$wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
$wallet->transactions()->delete();
$mollie = PaymentProvider::factory('mollie');
@@ -714,7 +658,7 @@
'status' => PaymentProvider::STATUS_PAID,
'amount' => 123,
'currency_amount' => 123,
- 'currency' => 'CHF',
+ 'currency' => 'EUR',
'type' => PaymentProvider::TYPE_ONEOFF,
'wallet_id' => $wallet->id,
'provider' => 'mollie',
@@ -750,7 +694,7 @@
"paymentId" => $payment->id,
"description" => "refund desc",
"amount" => [
- "currency" => "CHF",
+ "currency" => "EUR",
"value" => "1.01",
],
]
@@ -808,7 +752,7 @@
"id" => "chb_123456",
"paymentId" => $payment->id,
"amount" => [
- "currency" => "CHF",
+ "currency" => "EUR",
"value" => "0.15",
],
]
@@ -851,97 +795,6 @@
$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->assertTrue($wallet->balance <= -108);
- $this->assertTrue($wallet->balance >= -114);
-
- $payments = $wallet->payments()->where('id', 're_123456')->get();
-
- $this->assertCount(1, $payments);
- $this->assertTrue($payments[0]->amount <= -108);
- $this->assertTrue($payments[0]->amount >= -114);
- $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
*/
@@ -963,7 +816,6 @@
$this->stopBrowser();
}
-
/**
* Test listing a pending payment
*
@@ -973,7 +825,10 @@
{
Bus::fake();
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
//Empty response
$response = $this->actingAs($user)->get("api/v4/payments/pending");
@@ -989,10 +844,8 @@
$json = $response->json();
$this->assertSame(false, $json['hasPending']);
- $wallet = $user->wallets()->first();
-
// Successful payment
- $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard'];
+ $post = ['amount' => '12.34', 'currency' => 'EUR', 'methodId' => 'creditcard'];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
$response->assertStatus(200);
@@ -1006,6 +859,9 @@
$this->assertSame(false, $json['hasMore']);
$this->assertCount(1, $json['list']);
$this->assertSame(PaymentProvider::STATUS_OPEN, $json['list'][0]['status']);
+ $this->assertSame('EUR', $json['list'][0]['currency']);
+ $this->assertSame(PaymentProvider::TYPE_ONEOFF, $json['list'][0]['type']);
+ $this->assertSame(1234, $json['list'][0]['amount']);
$response = $this->actingAs($user)->get("api/v4/payments/has-pending");
$json = $response->json();
@@ -1041,7 +897,10 @@
{
Bus::fake();
- $user = $this->getTestUser('john@kolab.org');
+ $user = $this->getTestUser('euro@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $wallet->currency = 'EUR';
+ $wallet->save();
$response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_ONEOFF);
$response->assertStatus(200);
@@ -1051,6 +910,12 @@
$this->assertSame('creditcard', $json[0]['id']);
$this->assertSame('paypal', $json[1]['id']);
$this->assertSame('banktransfer', $json[2]['id']);
+ $this->assertSame('EUR', $json[0]['currency']);
+ $this->assertSame('EUR', $json[1]['currency']);
+ $this->assertSame('EUR', $json[2]['currency']);
+ $this->assertSame(1, $json[2]['exchangeRate']);
+ $this->assertSame(1, $json[2]['exchangeRate']);
+ $this->assertSame(1, $json[2]['exchangeRate']);
$response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING);
$response->assertStatus(200);
@@ -1058,5 +923,6 @@
$this->assertCount(1, $json);
$this->assertSame('creditcard', $json[0]['id']);
+ $this->assertSame('EUR', $json[0]['currency']);
}
}
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
@@ -114,7 +114,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
// Test creating a mandate (negative balance, amount too small)
@@ -143,7 +143,7 @@
$payment = Payment::where('id', $json['id'])->first();
$this->assertSame(2010, $payment->amount);
$this->assertSame($wallet->id, $payment->wallet_id);
- $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description);
+ $this->assertSame($user->tenant->title . " Auto-Payment Setup", $payment->description);
$this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
// Test fetching the mandate information
@@ -339,9 +339,10 @@
$response = $this->post("api/v4/payments", []);
$response->assertStatus(401);
- // Invalid amount
$user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
+ // Invalid amount
$post = ['amount' => -1];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
$response->assertStatus(422);
@@ -350,7 +351,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
// Invalid currency
@@ -368,7 +369,6 @@
$this->assertSame('success', $json['status']);
$this->assertMatchesRegularExpression('|^https://www.mollie.com|', $json['redirectUrl']);
- $wallet = $user->wallets()->first();
$payments = Payment::where('wallet_id', $wallet->id)->get();
$this->assertCount(1, $payments);
@@ -376,7 +376,7 @@
$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($user->tenant->title . ' Payment', $payment->description);
$this->assertSame('open', $payment->status);
$this->assertEquals(0, $wallet->balance);
@@ -1006,6 +1006,9 @@
$this->assertSame(false, $json['hasMore']);
$this->assertCount(1, $json['list']);
$this->assertSame(PaymentProvider::STATUS_OPEN, $json['list'][0]['status']);
+ $this->assertSame('CHF', $json['list'][0]['currency']);
+ $this->assertSame(PaymentProvider::TYPE_ONEOFF, $json['list'][0]['type']);
+ $this->assertSame(1234, $json['list'][0]['amount']);
$response = $this->actingAs($user)->get("api/v4/payments/has-pending");
$json = $response->json();
@@ -1051,6 +1054,9 @@
$this->assertSame('creditcard', $json[0]['id']);
$this->assertSame('paypal', $json[1]['id']);
$this->assertSame('banktransfer', $json[2]['id']);
+ $this->assertSame('CHF', $json[0]['currency']);
+ $this->assertSame('CHF', $json[1]['currency']);
+ $this->assertSame('EUR', $json[2]['currency']);
$response = $this->actingAs($user)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_RECURRING);
$response->assertStatus(200);
@@ -1058,5 +1064,6 @@
$this->assertCount(1, $json);
$this->assertSame('creditcard', $json[0]['id']);
+ $this->assertSame('CHF', $json[0]['currency']);
}
}
diff --git a/src/tests/Feature/Controller/PaymentsStripeTest.php b/src/tests/Feature/Controller/PaymentsStripeTest.php
--- a/src/tests/Feature/Controller/PaymentsStripeTest.php
+++ b/src/tests/Feature/Controller/PaymentsStripeTest.php
@@ -106,7 +106,7 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
// Test creating a mandate (negative balance, amount too small)
@@ -135,7 +135,7 @@
// Stripe in 'setup' mode does not allow to set the amount
$payment = Payment::where('wallet_id', $wallet->id)->first();
$this->assertSame(0, $payment->amount);
- $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description);
+ $this->assertSame($user->tenant->title . " Auto-Payment Setup", $payment->description);
$this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
// Test fetching the mandate information
@@ -294,6 +294,7 @@
$response->assertStatus(401);
$user = $this->getTestUser('john@kolab.org');
+ $wallet = $user->wallets()->first();
$post = ['amount' => -1];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
@@ -303,10 +304,9 @@
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
- $min = intval(PaymentProvider::MIN_AMOUNT / 100) . ' CHF';
+ $min = $wallet->money(PaymentProvider::MIN_AMOUNT);
$this->assertSame("Minimum amount for a single payment is {$min}.", $json['errors']['amount']);
-
// Invalid currency
$post = ['amount' => '12.34', 'currency' => 'FOO', 'methodId' => 'creditcard'];
$response = $this->actingAs($user)->post("api/v4/payments", $post);
@@ -322,13 +322,12 @@
$this->assertSame('success', $json['status']);
$this->assertMatchesRegularExpression('|^cs_test_|', $json['id']);
- $wallet = $user->wallets()->first();
$payments = Payment::where('wallet_id', $wallet->id)->get();
$this->assertCount(1, $payments);
$payment = $payments[0];
$this->assertSame(1234, $payment->amount);
- $this->assertSame(\config('app.name') . ' Payment', $payment->description);
+ $this->assertSame($user->tenant->title . ' Payment', $payment->description);
$this->assertSame('open', $payment->status);
$this->assertEquals(0, $wallet->balance);
@@ -538,7 +537,7 @@
"created" => 123456789,
"amount" => 2010,
"currency" => "chf",
- "description" => "Kolab Recurring Payment"
+ "description" => $user->tenant->title . " Recurring Payment"
]);
$client = $this->mockStripe();
@@ -559,7 +558,7 @@
$this->assertCount(1, $wallet->payments()->get());
$payment = $wallet->payments()->first();
$this->assertSame(2010, $payment->amount);
- $this->assertSame(\config('app.name') . " Recurring Payment", $payment->description);
+ $this->assertSame($user->tenant->title . " Recurring Payment", $payment->description);
$this->assertSame("pi_XX", $payment->id);
// Expect no payment if the mandate is disabled
diff --git a/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php
--- a/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php
+++ b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php
@@ -89,7 +89,7 @@
$this->assertSame(2010, $payment->amount);
$this->assertSame($wallet->id, $payment->wallet_id);
- $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description);
+ $this->assertSame($reseller->tenant->title . " Auto-Payment Setup", $payment->description);
$this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
// Test fetching the mandate information
diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php
--- a/src/tests/Feature/Controller/WalletsTest.php
+++ b/src/tests/Feature/Controller/WalletsTest.php
@@ -281,6 +281,7 @@
foreach ($pages[0] as $idx => $transaction) {
$this->assertSame($transaction->id, $json['list'][$idx]['id']);
$this->assertSame($transaction->type, $json['list'][$idx]['type']);
+ $this->assertSame(\config('app.currency'), $json['list'][$idx]['currency']);
$this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']);
$this->assertFalse($json['list'][$idx]['hasDetails']);
$this->assertFalse(array_key_exists('user', $json['list'][$idx]));
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -159,6 +159,8 @@
$user = $this->getTestUser('UserWallet1@UserWallet.com');
$this->assertCount(1, $user->wallets);
+ $this->assertSame(\config('app.currency'), $user->wallets[0]->currency);
+ $this->assertSame(0, $user->wallets[0]->balance);
}
/**
@@ -179,6 +181,9 @@
$this->assertEquals(0, $wallet->balance);
}
);
+
+ // For now all wallets use system currency
+ $this->assertFalse($user->wallets()->where('currency', 'USD')->exists());
}
/**
@@ -228,6 +233,9 @@
new Wallet(['currency' => 'USD'])
);
+ // For now additional wallets with a different currency is not allowed
+ $this->assertFalse($user->wallets()->where('currency', 'USD')->exists());
+/*
$user->wallets()->each(
function ($wallet) {
if ($wallet->currency == 'USD') {
@@ -235,6 +243,7 @@
}
}
);
+*/
}
/**
diff --git a/src/tests/Unit/WalletTest.php b/src/tests/Unit/WalletTest.php
--- a/src/tests/Unit/WalletTest.php
+++ b/src/tests/Unit/WalletTest.php
@@ -14,19 +14,17 @@
*/
public function testMoney()
{
- $wallet = new Wallet([
- 'currency' => 'CHF',
- ]);
+ // This test is here to remind us that the method will give
+ // different results for different locales
+ $wallet = new Wallet(['currency' => 'CHF']);
$money = $wallet->money(-123);
+
$this->assertSame('-1,23 CHF', $money);
- // This test is here to remind us that the method will give
- // different results for different locales, but also depending
- // if NumberFormatter (intl extension) is installed or not.
- // NumberFormatter also returns some surprising output for
- // some locales and e.g. negative numbers.
- // We'd have to improve on that as soon as we'd want to use
- // other locale than the default de_DE.
+ $wallet = new Wallet(['currency' => 'EUR']);
+ $money = $wallet->money(-123);
+
+ $this->assertSame('-1,23 €', $money);
}
}