Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117832316
D1372.1775303477.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None
D1372.1775303477.diff
View Options
diff --git a/src/app/Console/Commands/WalletUntil.php b/src/app/Console/Commands/WalletUntil.php
--- a/src/app/Console/Commands/WalletUntil.php
+++ b/src/app/Console/Commands/WalletUntil.php
@@ -43,8 +43,8 @@
return 1;
}
- $lastsUntil = $wallet->balanceLastsUntil();
+ $until = $wallet->balanceLastsUntil();
- $this->info("Lasts until: {$lastsUntil}");
+ $this->info("Lasts until: " . ($until ? $until->toDateString() : 'unknown'));
}
}
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
@@ -6,6 +6,7 @@
use App\Wallet;
use App\Http\Controllers\Controller;
use App\Providers\PaymentProvider;
+use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -47,15 +48,33 @@
}
/**
- * Display the specified resource.
+ * Return data of the specified wallet.
*
- * @param string $id
+ * @param string $id A wallet identifier
*
- * @return \Illuminate\Http\JsonResponse
+ * @return \Illuminate\Http\JsonResponse The response
*/
public function show($id)
{
- return $this->errorResponse(404);
+ $wallet = Wallet::find($id);
+
+ if (empty($wallet)) {
+ return $this->errorResponse(404);
+ }
+
+ // Only owner (or admin) has access to the wallet
+ if (!Auth::guard()->user()->canRead($wallet)) {
+ return $this->errorResponse(403);
+ }
+
+ $result = $wallet->toArray();
+
+ $provider = \App\Providers\PaymentProvider::factory($wallet);
+
+ $result['provider'] = $provider->name();
+ $result['notice'] = $this->getWalletNotice($wallet);
+
+ return response()->json($result);
}
/**
@@ -257,4 +276,43 @@
'page' => $page,
]);
}
+
+ /**
+ * Returns human readable notice about the wallet state.
+ *
+ * @param \App\Wallet $wallet The wallet
+ */
+ protected function getWalletNotice(Wallet $wallet): ?string
+ {
+ if ($wallet->balance < 0) {
+ return \trans('app.wallet-notice-nocredit');
+ }
+
+ if ($wallet->discount && $wallet->discount->discount == 100) {
+ return null;
+ }
+
+ if ($wallet->owner->created_at > Carbon::now()->subDays(14)) {
+ return \trans('app.wallet-notice-trial');
+ }
+
+ if ($until = $wallet->balanceLastsUntil()) {
+ if ($until->isToday()) {
+ if ($wallet->owner->created_at > Carbon::now()->subDays(30)) {
+ return \trans('app.wallet-notice-trial-end');
+ }
+
+ return \trans('app.wallet-notice-today');
+ }
+
+ $params = [
+ 'date' => $until->toDateString(),
+ 'days' => Carbon::now()->diffForHumans($until, Carbon::DIFF_ABSOLUTE),
+ ];
+
+ return \trans('app.wallet-notice-date', $params);
+ }
+
+ return null;
+ }
}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -119,11 +119,15 @@
/**
* Calculate for how long the current balance will last.
*
- * @return \Carbon\Carbon Date
+ * Returns NULL for balance < 0 or discount = 100% or on a fresh account
+ *
+ * @return \Carbon\Carbon|null Date
*/
public function balanceLastsUntil()
{
- $balance = $this->balance;
+ if ($this->balance < 0 || $this->getDiscount() == 100) {
+ return null;
+ }
// retrieve any expected charges
$expectedCharge = $this->expectedCharges();
@@ -131,37 +135,24 @@
// get the costs per day for all entitlements billed against this wallet
$costsPerDay = $this->costsPerDay();
+ if (!$costsPerDay) {
+ return null;
+ }
+
// the number of days this balance, minus the expected charges, would last
- $daysDelta = ($balance - $expectedCharge) / $costsPerDay;
+ $daysDelta = ($this->balance - $expectedCharge) / $costsPerDay;
// calculate from the last entitlement billed
$entitlement = $this->entitlements()->orderBy('updated_at', 'desc')->first();
- return $entitlement->updated_at->copy()->addDays($daysDelta);
- }
+ $until = $entitlement->updated_at->copy()->addDays($daysDelta);
- /**
- * A helper to display human-readable amount of money using
- * the wallet currency and specified locale.
- *
- * @param int $amount A amount of money (in cents)
- * @param string $locale A locale for the output
- *
- * @return string String representation, e.g. "9.99 CHF"
- */
- public function money(int $amount, $locale = 'de_DE')
- {
- $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);
+ // Don't return dates from the past
+ if ($until < Carbon::now() && !$until->isToday()) {
+ return null;
}
- return sprintf('%.2f %s', $amount, $this->currency);
+ return $until;
}
/**
@@ -307,6 +298,30 @@
}
/**
+ * A helper to display human-readable amount of money using
+ * the wallet currency and specified locale.
+ *
+ * @param int $amount A amount of money (in cents)
+ * @param string $locale A locale for the output
+ *
+ * @return string String representation, e.g. "9.99 CHF"
+ */
+ public function money(int $amount, $locale = 'de_DE')
+ {
+ $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);
+ }
+
+ /**
* The owner of the wallet -- the wallet is in his/her back pocket.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -145,8 +145,8 @@
link.click()
})
},
- price(price) {
- return (price/100).toLocaleString('de-DE', { style: 'currency', currency: 'CHF' })
+ price(price, currency) {
+ return (price/100).toLocaleString('de-DE', { style: 'currency', currency: currency || 'CHF' })
},
priceLabel(cost, units = 1, discount) {
let index = ''
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -44,4 +44,10 @@
'wallet-award-success' => 'The bonus has been added to the wallet successfully.',
'wallet-penalty-success' => 'The penalty has been added to the wallet successfully.',
'wallet-update-success' => 'User wallet updated successfully.',
+
+ 'wallet-notice-date' => 'With your current subscriptions your account balance will last until about :date (:days).',
+ 'wallet-notice-nocredit' => 'You are out of credit, top up your balance now.',
+ 'wallet-notice-today' => 'You will run out of credit today, top up your balance now.',
+ 'wallet-notice-trial' => 'You are in your free trial period.',
+ 'wallet-notice-trial-end' => 'Your free trial is about to end, top up to continue.',
];
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
@@ -1,12 +1,11 @@
<template>
<div class="container" dusk="wallet-component">
- <div id="wallet" class="card">
+ <div v-if="wallet.id" id="wallet" class="card">
<div class="card-body">
- <div class="card-title">Account balance</div>
+ <div class="card-title">Account balance <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
<div class="card-text">
- <p>Current account balance is
- <span :class="balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(balance) }}</strong></span>
- </p>
+ <p v-if="wallet.notice" id="wallet-notice">{{ wallet.notice }}</p>
+ <p>Add credit to your account or setup an automatic payment by using the button below.</p>
<button type="button" class="btn btn-primary" @click="paymentDialog()">Add credit</button>
</div>
</div>
@@ -73,7 +72,7 @@
<div class="input-group">
<input type="text" class="form-control" id="amount" v-model="amount" required>
<span class="input-group-append">
- <span class="input-group-text">{{ wallet_currency }}</span>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</span>
</div>
</div>
@@ -117,7 +116,7 @@
<div class="input-group col-sm-6">
<input type="text" class="form-control" id="mandate_amount" v-model="mandate.amount" required>
<span class="input-group-append">
- <span class="input-group-text">{{ wallet_currency }}</span>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</span>
</div>
</div>
@@ -127,7 +126,7 @@
<div class="input-group">
<input type="text" class="form-control" id="mandate_balance" v-model="mandate.balance" required>
<span class="input-group-append">
- <span class="input-group-text">{{ wallet_currency }}</span>
+ <span class="input-group-text">{{ wallet.currency }}</span>
</span>
</div>
</div>
@@ -168,41 +167,44 @@
data() {
return {
amount: '',
- balance: 0,
mandate: { amount: 10, balance: 0 },
paymentDialogTitle: null,
paymentForm: 'init',
- provider: window.config.paymentProvider,
receipts: [],
stripe: null,
loadTransactions: false,
- walletId: null,
- wallet_currency: 'CHF'
+ wallet: {},
+ walletId: null
}
},
mounted() {
$('#wallet button').focus()
- this.balance = 0
- // TODO: currencies, multi-wallets, accounts
- this.$store.state.authInfo.wallets.forEach(wallet => {
- this.balance += wallet.balance
- this.provider = wallet.provider
- })
-
this.walletId = this.$store.state.authInfo.wallets[0].id
- const receiptsTab = $('#wallet-receipts')
-
- this.$root.addLoader(receiptsTab)
- axios.get('/api/v4/wallets/' + this.walletId + '/receipts')
+ this.$root.startLoading()
+ axios.get('/api/v4/wallets/' + this.walletId)
.then(response => {
- this.$root.removeLoader(receiptsTab)
- this.receipts = response.data.list
- })
- .catch(error => {
- this.$root.removeLoader(receiptsTab)
+ this.$root.stopLoading()
+ this.wallet = response.data
+
+ const receiptsTab = $('#wallet-receipts')
+
+ this.$root.addLoader(receiptsTab)
+ axios.get('/api/v4/wallets/' + this.walletId + '/receipts')
+ .then(response => {
+ this.$root.removeLoader(receiptsTab)
+ this.receipts = response.data.list
+ })
+ .catch(error => {
+ this.$root.removeLoader(receiptsTab)
+ })
+
+ if (this.wallet.provider == 'stripe') {
+ this.stripeInit()
+ }
})
+ .catch(this.$root.errorHandler)
$(this.$el).find('ul.nav-tabs a').on('click', e => {
e.preventDefault()
@@ -211,10 +213,6 @@
this.loadTransactions = true
}
})
-
- if (this.provider == 'stripe') {
- this.stripeInit()
- }
},
methods: {
paymentDialog() {
diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php
--- a/src/tests/Browser/WalletTest.php
+++ b/src/tests/Browser/WalletTest.php
@@ -78,8 +78,9 @@
$this->browse(function (Browser $browser) {
$browser->click('@links .link-wallet')
->on(new WalletPage())
- ->assertSeeIn('#wallet .card-title', 'Account balance')
- ->assertSeeIn('#wallet .card-text', 'Current account balance is -12,34 CHF');
+ ->assertSeeIn('#wallet .card-title', 'Account balance -12,34 CHF')
+ ->assertSeeIn('#wallet .card-title .text-danger', '-12,34 CHF')
+ ->assertSeeIn('#wallet .card-text', 'You are out of credit');
});
}
@@ -105,6 +106,9 @@
$browser->on(new Dashboard())
->click('@links .link-wallet')
->on(new WalletPage())
+ ->assertSeeIn('#wallet .card-title', 'Account balance 0,00 CHF')
+ ->assertSeeIn('#wallet .card-title .text-success', '0,00 CHF')
+ ->assertSeeIn('#wallet .card-text', 'You are in your free trial period.')
->assertSeeIn('@nav #tab-receipts', 'Receipts')
->with('@receipts-tab', function (Browser $browser) {
$browser->waitUntilMissing('.app-loader')
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
@@ -31,6 +31,55 @@
parent::tearDown();
}
+
+ /**
+ * Test for getWalletNotice() method
+ */
+ public function testGetWalletNotice(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $package = \App\Package::where('title', 'kolab')->first();
+ $user->assignPackage($package);
+ $wallet = $user->wallets()->first();
+
+ $controller = new WalletsController();
+ $method = new \ReflectionMethod($controller, 'getWalletNotice');
+ $method->setAccessible(true);
+
+ // User/entitlements created today, balance=0
+ $notice = $method->invoke($controller, $wallet);
+
+ $this->assertSame('You are in your free trial period.', $notice);
+
+ $wallet->owner->created_at = Carbon::now()->subDays(15);
+ $wallet->owner->save();
+
+ $notice = $method->invoke($controller, $wallet);
+
+ $this->assertSame('Your free trial is about to end, top up to continue.', $notice);
+
+ // User/entitlements created today, balance=-10 CHF
+ $wallet->balance = -1000;
+ $notice = $method->invoke($controller, $wallet);
+
+ $this->assertSame('You are out of credit, top up your balance now.', $notice);
+
+ // User/entitlements created today, balance=-9,99 CHF (monthly cost)
+ $wallet->balance = 999;
+ $notice = $method->invoke($controller, $wallet);
+
+ $this->assertTrue(strpos($notice, '(1 month)') !== false);
+
+ // Old entitlements, 100% discount
+ $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40));
+ $discount = \App\Discount::where('discount', 100)->first();
+ $wallet->discount()->associate($discount);
+
+ $notice = $method->invoke($controller, $wallet->refresh());
+
+ $this->assertSame(null, $notice);
+ }
+
/**
* Test fetching pdf receipt
*/
@@ -131,6 +180,36 @@
}
/**
+ * Test fetching a wallet (GET /api/v4/wallets/:id)
+ */
+ public function testShow(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $wallet = $john->wallets()->first();
+
+ // Accessing a wallet of someone else
+ $response = $this->actingAs($jack)->get("api/v4/wallets/{$wallet->id}");
+ $response->assertStatus(403);
+
+ // Accessing non-existing wallet
+ $response = $this->actingAs($jack)->get("api/v4/wallets/aaa");
+ $response->assertStatus(404);
+
+ // Wallet owner
+ $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($wallet->id, $json['id']);
+ $this->assertSame('CHF', $json['currency']);
+ $this->assertSame($wallet->balance, $json['balance']);
+ $this->assertTrue(empty($json['description']));
+ $this->assertTrue(!empty($json['notice']));
+ }
+
+ /**
* Test fetching wallet transactions
*/
public function testTransactions(): void
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
@@ -6,6 +6,7 @@
use App\User;
use App\Sku;
use App\Wallet;
+use Carbon\Carbon;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -43,24 +44,63 @@
parent::tearDown();
}
+ /**
+ * Test for Wallet::balanceLastsUntil()
+ */
public function testBalanceLastsUntil(): void
{
+ // Monthly cost of all entitlements: 999
+ // 28 days: 35.68 per day
+ // 31 days: 32.22 per day
+
$user = $this->getTestUser('jane@kolabnow.com');
$package = Package::where('title', 'kolab')->first();
- $mailbox = Sku::where('title', 'mailbox')->first();
-
$user->assignPackage($package);
-
$wallet = $user->wallets()->first();
+ // User/entitlements created today, balance=0
+ $until = $wallet->balanceLastsUntil();
+
+ $this->assertSame(Carbon::now()->toDateString(), $until->toDateString());
+
+ // User/entitlements created today, balance=-10 CHF
+ $wallet->balance = -1000;
+ $until = $wallet->balanceLastsUntil();
+
+ $this->assertSame(null, $until);
+
+ // User/entitlements created today, balance=-9,99 CHF (monthly cost)
+ $wallet->balance = 999;
$until = $wallet->balanceLastsUntil();
- // TODO: Test this for typical cases
- // TODO: Test this for a user with no entitlements
- // TODO: Test this for a user with 100% discount
- $this->markTestIncomplete();
+ $daysInLastMonth = \App\Utils::daysInLastMonth();
+
+ $this->assertSame(
+ Carbon::now()->addDays($daysInLastMonth)->toDateString(),
+ $until->toDateString()
+ );
+
+ // Old entitlements, 100% discount
+ $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40));
+ $discount = \App\Discount::where('discount', 100)->first();
+ $wallet->discount()->associate($discount);
+
+ $until = $wallet->refresh()->balanceLastsUntil();
+
+ $this->assertSame(null, $until);
+
+ // User with no entitlements
+ $wallet->discount()->dissociate($discount);
+ $wallet->entitlements()->delete();
+
+ $until = $wallet->refresh()->balanceLastsUntil();
+
+ $this->assertSame(null, $until);
}
+ /**
+ * Test for Wallet::costsPerDay()
+ */
public function testCostsPerDay(): void
{
// 999
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 11:51 AM (17 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828333
Default Alt Text
D1372.1775303477.diff (20 KB)
Attached To
Mode
D1372: Display a notice about the wallet state (Bifrost#T348752)
Attached
Detach File
Event Timeline