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/Admin/StatsController.php b/src/app/Http/Controllers/API/V4/Admin/StatsController.php --- a/src/app/Http/Controllers/API/V4/Admin/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Admin/StatsController.php @@ -6,7 +6,6 @@ use App\User; use Carbon\Carbon; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; class StatsController extends \App\Http\Controllers\Controller @@ -128,34 +127,48 @@ $labels = array_reverse($labels); $start->startOfWeek(Carbon::MONDAY); - $payments = DB::table('payments') - ->selectRaw("date_format(updated_at, '%Y-%v') as period, sum(amount) as amount") + // FIXME: We're using wallets.currency instead of payments.currency and payments.currency_amount + // as I believe this way we have more precise amounts for this use-case (and default currency) + + $query = DB::table('payments') + ->selectRaw("date_format(updated_at, '%Y-%v') as period, sum(amount) as amount, wallets.currency") + ->join('wallets', 'wallets.id', '=', 'wallet_id') ->where('updated_at', '>=', $start->toDateString()) ->where('status', PaymentProvider::STATUS_PAID) ->whereIn('type', [PaymentProvider::TYPE_ONEOFF, PaymentProvider::TYPE_RECURRING]) - ->groupByRaw('1'); + ->groupByRaw('period, wallets.currency'); $addTenantScope = function ($builder, $tenantId) { - $where = '`wallet_id` IN (' - . 'select `id` from `wallets` ' - . 'join `users` on (`wallets`.`user_id` = `users`.`id`) ' - . 'where `payments`.`wallet_id` = `wallets`.`id` ' - . 'and `users`.`tenant_id` = ' . intval($tenantId) - . ')'; + $where = sprintf( + '`wallets`.`user_id` IN (select `id` from `users` where `tenant_id` = %d)', + $tenantId + ); return $builder->whereRaw($where); }; - $payments = $this->applyTenantScope($payments, $addTenantScope) - ->pluck('amount', 'period') - ->map(function ($amount) { - return $amount / 100; + $currency = $this->currency(); + $payments = []; + + $this->applyTenantScope($query, $addTenantScope) + ->get() + ->each(function ($record) use (&$payments, $currency) { + $amount = $record->amount; + if ($record->currency != $currency) { + $amount = intval(round($amount * \App\Utils::exchangeRate($record->currency, $currency))); + } + + if (isset($payments[$record->period])) { + $payments[$record->period] += $amount / 100; + } else { + $payments[$record->period] = $amount / 100; + } }); // TODO: exclude refunds/chargebacks $empty = array_fill_keys($labels, 0); - $payments = array_values(array_merge($empty, $payments->all())); + $payments = array_values(array_merge($empty, $payments)); // $payments = [1000, 1200.25, 3000, 1897.50, 2000, 1900, 2134, 3330]; @@ -164,7 +177,7 @@ // See https://frappe.io/charts/docs for format/options description return [ - 'title' => 'Income in CHF - last 8 weeks', + 'title' => "Income in {$currency} - last 8 weeks", 'type' => 'bar', 'colors' => [self::COLOR_BLUE], 'axisOptions' => [ @@ -352,16 +365,26 @@ */ protected function applyTenantScope($query, $addQuery = null) { - $user = Auth::guard()->user(); + // TODO: Per-tenant stats for admins + + return $query; + } + + /** + * Get the currency for stats + * + * @return string Currency code + */ + protected function currency() + { + $user = $this->guard()->user(); + // For resellers return their wallet currency if ($user->role == 'reseller') { - if ($addQuery) { - $query = $addQuery($query, \config('app.tenant_id')); - } else { - $query = $query->withEnvTenantContext(); - } + $currency = $user->wallet()->currency; } - return $query; + // System currency for others + return \config('app.currency'); } } 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/Reseller/StatsController.php b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php @@ -24,9 +24,8 @@ */ protected function applyTenantScope($query, $addQuery = null) { - $user = Auth::guard()->user(); - if ($addQuery) { + $user = Auth::guard()->user(); $query = $addQuery($query, $user->tenant_id); } else { $query = $query->withSubjectTenantContext(); 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) }}

@@ -146,8 +146,8 @@
@@ -393,7 +393,7 @@
- {{ oneoff_currency }} + {{ wallet.currency }}
@@ -453,7 +453,6 @@ data() { return { oneoff_amount: '', - oneoff_currency: 'CHF', oneoff_description: '', oneoff_negative: false, discount: 0, @@ -684,10 +683,6 @@ post.amount *= -1 } - // TODO: We maybe should use system currency not wallet currency, - // or have a selector so the operator does not have to calculate - // exchange rates - this.$root.clearFormValidation(this.dialog) axios.post('/api/v4/wallets/' + wallet_id + '/one-off', post) diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue --- a/src/resources/vue/Dashboard.vue +++ b/src/resources/vue/Dashboard.vue @@ -17,7 +17,7 @@ {{ $t('dashboard.wallet') }} - {{ $root.price(balance) }} + {{ $root.price(balance, currency) }} {{ $t('dashboard.chat') }} @@ -41,6 +41,7 @@ return { status: {}, balance: 0, + currency: '', webmailURL: window.config['app.webmail_url'] } }, @@ -55,6 +56,7 @@ // TODO: currencies, multi-wallets, accounts authInfo.wallets.forEach(wallet => { this.balance += wallet.balance + this.currency = wallet.currency }) }, statusUpdate(user) { diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue --- a/src/resources/vue/Wallet.vue +++ b/src/resources/vue/Wallet.vue @@ -26,7 +26,7 @@ {{ $t('wallet.auto-payment-disabled') }}
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/Admin/StatsTest.php b/src/tests/Feature/Controller/Admin/StatsTest.php --- a/src/tests/Feature/Controller/Admin/StatsTest.php +++ b/src/tests/Feature/Controller/Admin/StatsTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Controller\Admin; +use App\Payment; +use App\Providers\PaymentProvider; use Tests\TestCase; class StatsTest extends TestCase @@ -13,6 +15,8 @@ { parent::setUp(); self::useAdminUrl(); + + Payment::truncate(); } /** @@ -20,6 +24,8 @@ */ public function tearDown(): void { + Payment::truncate(); + parent::tearDown(); } @@ -85,4 +91,103 @@ $this->assertCount(54, $json['data']['labels']); $this->assertCount(1, $json['data']['datasets']); } + + /** + * Test income chart currency handling + */ + public function testChartIncomeCurrency(): void + { + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + $john = $this->getTestUser('john@kolab.org'); + $user = $this->getTestUser('test-stats@' . \config('app.domain')); + $wallet = $user->wallets()->first(); + $wallet->currency = 'EUR'; + $wallet->save(); + $johns_wallet = $john->wallets()->first(); + + // Create some test payments + Payment::create([ + 'id' => 'test1', + 'description' => '', + 'status' => PaymentProvider::STATUS_PAID, + 'amount' => 1000, // EUR + 'type' => PaymentProvider::TYPE_ONEOFF, + 'wallet_id' => $wallet->id, + 'provider' => 'mollie', + 'currency' => 'EUR', + 'currency_amount' => 1000, + ]); + Payment::create([ + 'id' => 'test2', + 'description' => '', + 'status' => PaymentProvider::STATUS_PAID, + 'amount' => 2000, // EUR + 'type' => PaymentProvider::TYPE_RECURRING, + 'wallet_id' => $wallet->id, + 'provider' => 'mollie', + 'currency' => 'EUR', + 'currency_amount' => 2000, + ]); + Payment::create([ + 'id' => 'test3', + 'description' => '', + 'status' => PaymentProvider::STATUS_PAID, + 'amount' => 3000, // CHF + 'type' => PaymentProvider::TYPE_ONEOFF, + 'wallet_id' => $johns_wallet->id, + 'provider' => 'mollie', + 'currency' => 'EUR', + 'currency_amount' => 2800, + ]); + Payment::create([ + 'id' => 'test4', + 'description' => '', + 'status' => PaymentProvider::STATUS_PAID, + 'amount' => 4000, // CHF + 'type' => PaymentProvider::TYPE_RECURRING, + 'wallet_id' => $johns_wallet->id, + 'provider' => 'mollie', + 'currency' => 'CHF', + 'currency_amount' => 4000, + ]); + Payment::create([ + 'id' => 'test5', + 'description' => '', + 'status' => PaymentProvider::STATUS_OPEN, + 'amount' => 5000, // CHF + 'type' => PaymentProvider::TYPE_ONEOFF, + 'wallet_id' => $johns_wallet->id, + 'provider' => 'mollie', + 'currency' => 'CHF', + 'currency_amount' => 5000, + ]); + Payment::create([ + 'id' => 'test6', + 'description' => '', + 'status' => PaymentProvider::STATUS_FAILED, + 'amount' => 6000, // CHF + 'type' => PaymentProvider::TYPE_ONEOFF, + 'wallet_id' => $johns_wallet->id, + 'provider' => 'mollie', + 'currency' => 'CHF', + 'currency_amount' => 6000, + ]); + + // 'income' chart + $response = $this->actingAs($admin)->get("api/v4/stats/chart/income"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame('Income in CHF - last 8 weeks', $json['title']); + $this->assertSame('bar', $json['type']); + $this->assertCount(8, $json['data']['labels']); + $this->assertSame(date('Y-W'), $json['data']['labels'][7]); + + // 7000 CHF + 3000 EUR = + $expected = 7000 + intval(round(3000 * \App\Utils::exchangeRate('EUR', 'CHF'))); + + $this->assertCount(1, $json['data']['datasets']); + $this->assertSame($expected / 100, $json['data']['datasets'][0]['values'][7]); + } } 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); @@ -471,51 +456,6 @@ } /** - * 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 * * @group mollie @@ -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", ], ] @@ -852,97 +796,6 @@ } /** - * 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 */ protected function createMandate(Wallet $wallet, array $params) @@ -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); } }