Page MenuHomePhorge

D1372.1775303477.diff
No OneTemporary

Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None

D1372.1775303477.diff

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

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)

Event Timeline