Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2513190
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
42 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/app/Http/Controllers/API/V4/Admin/StatsController.php b/src/app/Http/Controllers/API/V4/Admin/StatsController.php
index e00b6da8..ecc4a7c2 100644
--- a/src/app/Http/Controllers/API/V4/Admin/StatsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/StatsController.php
@@ -1,390 +1,428 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\Providers\PaymentProvider;
use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class StatsController extends \App\Http\Controllers\Controller
{
public const COLOR_GREEN = '#48d368'; // '#28a745'
public const COLOR_GREEN_DARK = '#19692c';
public const COLOR_RED = '#e77681'; // '#dc3545'
public const COLOR_RED_DARK = '#a71d2a';
public const COLOR_BLUE = '#4da3ff'; // '#007bff'
public const COLOR_BLUE_DARK = '#0056b3';
public const COLOR_ORANGE = '#f1a539';
/** @var array List of enabled charts */
protected $charts = [
'discounts',
'income',
'users',
'users-all',
+ 'vouchers',
];
/**
* Fetch chart data
*
* @param string $chart Name of the chart
*
* @return \Illuminate\Http\JsonResponse
*/
public function chart($chart)
{
if (!preg_match('/^[a-z-]+$/', $chart)) {
return $this->errorResponse(404);
}
$method = 'chart' . implode('', array_map('ucfirst', explode('-', $chart)));
if (!in_array($chart, $this->charts) || !method_exists($this, $method)) {
return $this->errorResponse(404);
}
$result = $this->{$method}();
return response()->json($result);
}
/**
* Get discounts chart
*/
protected function chartDiscounts(): array
{
$discounts = DB::table('wallets')
->selectRaw("discount, count(discount_id) as cnt")
->join('discounts', 'discounts.id', '=', 'wallets.discount_id')
->join('users', 'users.id', '=', 'wallets.user_id')
->where('discount', '>', 0)
->whereNull('users.deleted_at')
->groupBy('discounts.discount');
$addTenantScope = function ($builder, $tenantId) {
return $builder->where('users.tenant_id', $tenantId);
};
$discounts = $this->applyTenantScope($discounts, $addTenantScope)
->pluck('cnt', 'discount')->all();
$labels = array_keys($discounts);
$discounts = array_values($discounts);
// $labels = [10, 25, 30, 100];
// $discounts = [100, 120, 30, 50];
$labels = array_map(function ($item) {
return $item . '%';
}, $labels);
- // See https://frappe.io/charts/docs for format/options description
-
- return [
- 'title' => 'Discounts',
- 'type' => 'donut',
- 'colors' => [
- self::COLOR_BLUE,
- self::COLOR_BLUE_DARK,
- self::COLOR_GREEN,
- self::COLOR_GREEN_DARK,
- self::COLOR_ORANGE,
- self::COLOR_RED,
- self::COLOR_RED_DARK
- ],
- 'maxSlices' => 8,
- 'tooltipOptions' => [], // does not work without it (https://github.com/frappe/charts/issues/314)
- 'data' => [
- 'labels' => $labels,
- 'datasets' => [
- [
- 'values' => $discounts
- ]
- ]
- ]
- ];
+ return $this->donutChart(\trans('app.chart-discounts'), $labels, $discounts);
}
/**
* Get income chart
*/
protected function chartIncome(): array
{
$weeks = 8;
$start = Carbon::now();
$labels = [];
while ($weeks > 0) {
$labels[] = $start->format('Y-W');
$weeks--;
if ($weeks) {
$start->subWeeks(1);
}
}
$labels = array_reverse($labels);
$start->startOfWeek(Carbon::MONDAY);
// 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('period, wallets.currency');
$addTenantScope = function ($builder, $tenantId) {
$where = sprintf(
'`wallets`.`user_id` IN (select `id` from `users` where `tenant_id` = %d)',
$tenantId
);
return $builder->whereRaw($where);
};
$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));
// $payments = [1000, 1200.25, 3000, 1897.50, 2000, 1900, 2134, 3330];
$avg = collect($payments)->slice(0, count($labels) - 1)->avg();
// See https://frappe.io/charts/docs for format/options description
return [
- 'title' => "Income in {$currency} - last 8 weeks",
+ 'title' => \trans('app.chart-income', ['currency' => $currency]),
'type' => 'bar',
'colors' => [self::COLOR_BLUE],
'axisOptions' => [
'xIsSeries' => true,
],
'data' => [
'labels' => $labels,
'datasets' => [
[
// 'name' => 'Payments',
'values' => $payments
]
],
'yMarkers' => [
[
'label' => sprintf('average = %.2f', $avg),
'value' => $avg,
'options' => [ 'labelPos' => 'left' ] // default: 'right'
]
]
]
];
}
/**
* Get created/deleted users chart
*/
protected function chartUsers(): array
{
$weeks = 8;
$start = Carbon::now();
$labels = [];
while ($weeks > 0) {
$labels[] = $start->format('Y-W');
$weeks--;
if ($weeks) {
$start->subWeeks(1);
}
}
$labels = array_reverse($labels);
$start->startOfWeek(Carbon::MONDAY);
$created = DB::table('users')
->selectRaw("date_format(created_at, '%Y-%v') as period, count(*) as cnt")
->where('created_at', '>=', $start->toDateString())
->groupByRaw('1');
$deleted = DB::table('users')
->selectRaw("date_format(deleted_at, '%Y-%v') as period, count(*) as cnt")
->where('deleted_at', '>=', $start->toDateString())
->groupByRaw('1');
$created = $this->applyTenantScope($created)->get();
$deleted = $this->applyTenantScope($deleted)->get();
$empty = array_fill_keys($labels, 0);
$created = array_values(array_merge($empty, $created->pluck('cnt', 'period')->all()));
$deleted = array_values(array_merge($empty, $deleted->pluck('cnt', 'period')->all()));
// $created = [5, 2, 4, 2, 0, 5, 2, 4];
// $deleted = [1, 2, 3, 1, 2, 1, 2, 3];
$avg = collect($created)->slice(0, count($labels) - 1)->avg();
// See https://frappe.io/charts/docs for format/options description
return [
- 'title' => 'Users - last 8 weeks',
+ 'title' => \trans('app.chart-users'),
'type' => 'bar', // Required to fix https://github.com/frappe/charts/issues/294
'colors' => [self::COLOR_GREEN, self::COLOR_RED],
'axisOptions' => [
'xIsSeries' => true,
],
'data' => [
'labels' => $labels,
'datasets' => [
[
- 'name' => 'Created',
+ 'name' => \trans('app.chart-created'),
'chartType' => 'bar',
'values' => $created
],
[
- 'name' => 'Deleted',
+ 'name' => \trans('app.chart-deleted'),
'chartType' => 'line',
'values' => $deleted
]
],
'yMarkers' => [
[
- 'label' => sprintf('average = %.1f', $avg),
+ 'label' => sprintf('%s = %.1f', \trans('app.chart-average'), $avg),
'value' => collect($created)->avg(),
'options' => [ 'labelPos' => 'left' ] // default: 'right'
]
]
]
];
}
/**
* Get all users chart
*/
protected function chartUsersAll(): array
{
$weeks = 54;
$start = Carbon::now();
$labels = [];
while ($weeks > 0) {
$labels[] = $start->format('Y-W');
$weeks--;
if ($weeks) {
$start->subWeeks(1);
}
}
$labels = array_reverse($labels);
$start->startOfWeek(Carbon::MONDAY);
$created = DB::table('users')
->selectRaw("date_format(created_at, '%Y-%v') as period, count(*) as cnt")
->where('created_at', '>=', $start->toDateString())
->groupByRaw('1');
$deleted = DB::table('users')
->selectRaw("date_format(deleted_at, '%Y-%v') as period, count(*) as cnt")
->where('deleted_at', '>=', $start->toDateString())
->groupByRaw('1');
$created = $this->applyTenantScope($created)->get();
$deleted = $this->applyTenantScope($deleted)->get();
$count = $this->applyTenantScope(DB::table('users')->whereNull('deleted_at'))->count();
$empty = array_fill_keys($labels, 0);
$created = array_merge($empty, $created->pluck('cnt', 'period')->all());
$deleted = array_merge($empty, $deleted->pluck('cnt', 'period')->all());
$all = [];
foreach (array_reverse($labels) as $label) {
$all[] = $count;
$count -= $created[$label] - $deleted[$label];
}
$all = array_reverse($all);
// $start = 3000;
// for ($i = 0; $i < count($labels); $i++) {
// $all[$i] = $start + $i * 15;
// }
// See https://frappe.io/charts/docs for format/options description
return [
- 'title' => 'All Users - last year',
+ 'title' => \trans('app.chart-allusers'),
'type' => 'line',
'colors' => [self::COLOR_GREEN],
'axisOptions' => [
'xIsSeries' => true,
'xAxisMode' => 'tick',
],
'lineOptions' => [
'hideDots' => true,
'regionFill' => true,
],
'data' => [
'labels' => $labels,
'datasets' => [
[
// 'name' => 'Existing',
'values' => $all
]
]
]
];
}
+ /**
+ * Get vouchers chart
+ */
+ protected function chartVouchers(): array
+ {
+ $vouchers = DB::table('wallets')
+ ->selectRaw("count(discount_id) as cnt, code")
+ ->join('discounts', 'discounts.id', '=', 'wallets.discount_id')
+ ->join('users', 'users.id', '=', 'wallets.user_id')
+ ->where('discount', '>', 0)
+ ->whereNotNull('code')
+ ->whereNull('users.deleted_at')
+ ->groupBy('discounts.code')
+ ->havingRaw("count(discount_id) > 0")
+ ->orderByRaw('1');
+
+ $addTenantScope = function ($builder, $tenantId) {
+ return $builder->where('users.tenant_id', $tenantId);
+ };
+
+ $vouchers = $this->applyTenantScope($vouchers, $addTenantScope)
+ ->pluck('cnt', 'code')->all();
+
+ $labels = array_keys($vouchers);
+ $vouchers = array_values($vouchers);
+
+ // $labels = ["TEST", "NEW", "OTHER", "US"];
+ // $vouchers = [100, 120, 30, 50];
+
+ return $this->donutChart(\trans('app.chart-vouchers'), $labels, $vouchers);
+ }
+
+ protected static function donutChart($title, $labels, $data): array
+ {
+ // See https://frappe.io/charts/docs for format/options description
+
+ return [
+ 'title' => $title,
+ 'type' => 'donut',
+ 'colors' => [
+ self::COLOR_BLUE,
+ self::COLOR_BLUE_DARK,
+ self::COLOR_GREEN,
+ self::COLOR_GREEN_DARK,
+ self::COLOR_ORANGE,
+ self::COLOR_RED,
+ self::COLOR_RED_DARK
+ ],
+ 'maxSlices' => 8,
+ 'tooltipOptions' => [], // does not work without it (https://github.com/frappe/charts/issues/314)
+ 'data' => [
+ 'labels' => $labels,
+ 'datasets' => [
+ [
+ 'values' => $data
+ ]
+ ]
+ ]
+ ];
+ }
+
/**
* Add tenant scope to the queries when needed
*
* @param \Illuminate\Database\Query\Builder $query The query
* @param callable $addQuery Additional tenant-scope query-modifier
*
* @return \Illuminate\Database\Query\Builder
*/
protected function applyTenantScope($query, $addQuery = null)
{
// 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') {
$currency = $user->wallet()->currency;
}
// System currency for others
return \config('app.currency');
}
}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php
index 334f6500..9bff1132 100644
--- a/src/app/Http/Controllers/API/V4/Reseller/StatsController.php
+++ b/src/app/Http/Controllers/API/V4/Reseller/StatsController.php
@@ -1,36 +1,37 @@
<?php
namespace App\Http\Controllers\API\V4\Reseller;
use Illuminate\Support\Facades\Auth;
class StatsController extends \App\Http\Controllers\API\V4\Admin\StatsController
{
/** @var array List of enabled charts */
protected $charts = [
'discounts',
// 'income',
'users',
'users-all',
+ 'vouchers',
];
/**
* Add tenant scope to the queries when needed
*
* @param \Illuminate\Database\Query\Builder $query The query
* @param callable $addQuery Additional tenant-scope query-modifier
*
* @return \Illuminate\Database\Query\Builder
*/
protected function applyTenantScope($query, $addQuery = null)
{
if ($addQuery) {
$user = Auth::guard()->user();
$query = $addQuery($query, $user->tenant_id);
} else {
$query = $query->withSubjectTenantContext();
}
return $query;
}
}
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
index 664faa71..93ef6024 100644
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -1,84 +1,93 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used in the application.
*/
+ 'chart-created' => 'Created',
+ 'chart-deleted' => 'Deleted',
+ 'chart-average' => 'average',
+ 'chart-allusers' => 'All Users - last year',
+ 'chart-discounts' => 'Discounts',
+ 'chart-vouchers' => 'Vouchers',
+ 'chart-income' => 'Income in :currency - last 8 weeks',
+ 'chart-users' => 'Users - last 8 weeks',
+
'mandate-delete-success' => 'The auto-payment has been removed.',
'mandate-update-success' => 'The auto-payment has been updated.',
'planbutton' => 'Choose :plan',
'process-async' => 'Setup process has been pushed. Please wait.',
'process-user-new' => 'Registering a user...',
'process-user-ldap-ready' => 'Creating a user...',
'process-user-imap-ready' => 'Creating a mailbox...',
'process-distlist-new' => 'Registering a distribution list...',
'process-distlist-ldap-ready' => 'Creating a distribution list...',
'process-domain-new' => 'Registering a custom domain...',
'process-domain-ldap-ready' => 'Creating a custom domain...',
'process-domain-verified' => 'Verifying a custom domain...',
'process-domain-confirmed' => 'Verifying an ownership of a custom domain...',
'process-success' => 'Setup process finished successfully.',
'process-error-user-ldap-ready' => 'Failed to create a user.',
'process-error-user-imap-ready' => 'Failed to verify that a mailbox exists.',
'process-error-domain-ldap-ready' => 'Failed to create a domain.',
'process-error-domain-verified' => 'Failed to verify a domain.',
'process-error-domain-confirmed' => 'Failed to verify an ownership of a domain.',
'process-distlist-new' => 'Registering a distribution list...',
'process-distlist-ldap-ready' => 'Creating a distribution list...',
'process-error-distlist-ldap-ready' => 'Failed to create a distribution list.',
'distlist-update-success' => 'Distribution list updated successfully.',
'distlist-create-success' => 'Distribution list created successfully.',
'distlist-delete-success' => 'Distribution list deleted successfully.',
'distlist-suspend-success' => 'Distribution list suspended successfully.',
'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.',
'domain-verify-success' => 'Domain verified successfully.',
'domain-verify-error' => 'Domain ownership verification failed.',
'domain-suspend-success' => 'Domain suspended successfully.',
'domain-unsuspend-success' => 'Domain unsuspended successfully.',
'domain-setconfig-success' => 'Domain settings updated successfully.',
'user-update-success' => 'User data updated successfully.',
'user-create-success' => 'User created successfully.',
'user-delete-success' => 'User deleted successfully.',
'user-suspend-success' => 'User suspended successfully.',
'user-unsuspend-success' => 'User unsuspended successfully.',
'user-reset-2fa-success' => '2-Factor authentication reset successfully.',
'user-setconfig-success' => 'User settings updated successfully.',
'user-set-sku-success' => 'The subscription added successfully.',
'user-set-sku-already-exists' => 'The subscription already exists.',
'search-foundxdomains' => ':x domains have been found.',
'search-foundxgroups' => ':x distribution lists have been found.',
'search-foundxusers' => ':x user accounts have been found.',
'signup-invitations-created' => 'The invitation has been created.|:count invitations has been created.',
'signup-invitations-csv-empty' => 'Failed to find any valid email addresses in the uploaded file.',
'signup-invitations-csv-invalid-email' => 'Found an invalid email address (:email) on line :line.',
'signup-invitation-delete-success' => 'Invitation deleted successfully.',
'signup-invitation-resend-success' => 'Invitation added to the sending queue successfully.',
'support-request-success' => 'Support request submitted successfully.',
'support-request-error' => 'Failed to submit the support request.',
'siteuser' => ':site User',
'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/Admin/Stats.vue b/src/resources/vue/Admin/Stats.vue
index 420ea3b1..bffdc8cb 100644
--- a/src/resources/vue/Admin/Stats.vue
+++ b/src/resources/vue/Admin/Stats.vue
@@ -1,46 +1,46 @@
<template>
<div id="stats-container" class="container"></div>
</template>
<script>
import { Chart } from 'frappe-charts/dist/frappe-charts.esm.js'
export default {
data() {
return {
charts: {},
- chartTypes: ['users', 'users-all', 'income', 'discounts']
+ chartTypes: ['users', 'users-all', 'income', 'discounts', 'vouchers']
}
},
mounted() {
this.chartTypes.forEach(chart => this.loadChart(chart))
},
methods: {
drawChart(name, data) {
if (!data.title) {
return
}
const ch = new Chart('#chart-' + name, data)
this.charts[name] = ch
},
loadChart(name) {
const chart = $('<div>').attr({ id: 'chart-' + name }).appendTo(this.$el)
this.$root.addLoader(chart)
axios.get('/api/v4/stats/chart/' + name)
.then(response => {
this.$root.removeLoader(chart)
this.drawChart(name, response.data)
})
.catch(error => {
console.error(error)
this.$root.removeLoader(chart)
chart.append($('<span>').text(this.$t('msg.loading-failed')))
})
}
}
}
</script>
diff --git a/src/resources/vue/Reseller/Stats.vue b/src/resources/vue/Reseller/Stats.vue
index f7953506..88be2302 100644
--- a/src/resources/vue/Reseller/Stats.vue
+++ b/src/resources/vue/Reseller/Stats.vue
@@ -1,16 +1,16 @@
<template>
<div id="stats-container" class="container"></div>
</template>
<script>
import Stats from '../Admin/Stats'
export default {
mixins: [Stats],
data() {
return {
- chartTypes: ['users', 'users-all', 'discounts']
+ chartTypes: ['users', 'users-all', 'discounts', 'vouchers']
}
}
}
</script>
diff --git a/src/tests/Browser/Admin/StatsTest.php b/src/tests/Browser/Admin/StatsTest.php
index b877cc0f..0cb3aec4 100644
--- a/src/tests/Browser/Admin/StatsTest.php
+++ b/src/tests/Browser/Admin/StatsTest.php
@@ -1,41 +1,42 @@
<?php
namespace Tests\Browser\Admin;
use Tests\Browser;
use Tests\Browser\Pages\Admin\Stats;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\TestCaseDusk;
class StatsTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useAdminUrl();
}
/**
* Test Stats page
*/
public function testStats(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('jeroen@jeroen.jeroen', \App\Utils::generatePassphrase(), true)
->on(new Dashboard())
->assertSeeIn('@links .link-stats', 'Stats')
->click('@links .link-stats')
->on(new Stats())
- ->assertElementsCount('@container > div', 4)
+ ->assertElementsCount('@container > div', 5)
->waitForTextIn('@container #chart-users svg .title', 'Users - last 8 weeks')
->waitForTextIn('@container #chart-users-all svg .title', 'All Users - last year')
->waitForTextIn('@container #chart-income svg .title', 'Income in CHF - last 8 weeks')
- ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts');
+ ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts')
+ ->waitForTextIn('@container #chart-vouchers svg .title', 'Vouchers');
});
}
}
diff --git a/src/tests/Browser/Reseller/StatsTest.php b/src/tests/Browser/Reseller/StatsTest.php
index 27fe981d..29c3d961 100644
--- a/src/tests/Browser/Reseller/StatsTest.php
+++ b/src/tests/Browser/Reseller/StatsTest.php
@@ -1,51 +1,52 @@
<?php
namespace Tests\Browser\Reseller;
use Tests\Browser;
use Tests\Browser\Pages\Admin\Stats;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\TestCaseDusk;
class StatsTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useResellerUrl();
}
/**
* Test Stats page (unauthenticated)
*/
public function testStatsUnauth(): void
{
// Test that the page requires authentication
$this->browse(function (Browser $browser) {
$browser->visit('/stats')->on(new Home());
});
}
/**
* Test Stats page
*/
public function testStats(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->submitLogon('reseller@' . \config('app.domain'), \App\Utils::generatePassphrase(), true)
->on(new Dashboard())
->assertSeeIn('@links .link-stats', 'Stats')
->click('@links .link-stats')
->on(new Stats())
- ->assertElementsCount('@container > div', 3)
+ ->assertElementsCount('@container > div', 4)
->waitForTextIn('@container #chart-users svg .title', 'Users - last 8 weeks')
->waitForTextIn('@container #chart-users-all svg .title', 'All Users - last year')
- ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts');
+ ->waitForTextIn('@container #chart-discounts svg .title', 'Discounts')
+ ->waitForTextIn('@container #chart-vouchers svg .title', 'Vouchers');
});
}
}
diff --git a/src/tests/Feature/Controller/Admin/DiscountsTest.php b/src/tests/Feature/Controller/Admin/DiscountsTest.php
index dad9e3a6..434b2944 100644
--- a/src/tests/Feature/Controller/Admin/DiscountsTest.php
+++ b/src/tests/Feature/Controller/Admin/DiscountsTest.php
@@ -1,77 +1,77 @@
<?php
namespace Tests\Feature\Controller\Admin;
use App\Discount;
use Tests\TestCase;
class DiscountsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useAdminUrl();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
parent::tearDown();
}
/**
* Test listing discounts (GET /api/v4/users/{user}/discounts)
*/
public function testuserDiscounts(): void
{
$user = $this->getTestUser('john@kolab.org');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Non-admin user
$response = $this->actingAs($user)->get("api/v4/users/{$user->id}/discounts");
$response->assertStatus(403);
// Admin user
$response = $this->actingAs($admin)->get("api/v4/users/{$user->id}/discounts");
$response->assertStatus(200);
$json = $response->json();
- $discount_test = Discount::where('code', 'TEST')->first();
- $discount_free = Discount::where('discount', 100)->first();
+ $discount_test = Discount::withObjectTenantContext($user)->where('code', 'TEST')->first();
+ $discount_free = Discount::withObjectTenantContext($user)->where('discount', 100)->first();
$this->assertSame(3, $json['count']);
$this->assertSame($discount_test->id, $json['list'][0]['id']);
$this->assertSame($discount_test->discount, $json['list'][0]['discount']);
$this->assertSame($discount_test->code, $json['list'][0]['code']);
$this->assertSame($discount_test->description, $json['list'][0]['description']);
$this->assertSame('10% - Test voucher [TEST]', $json['list'][0]['label']);
$this->assertSame($discount_free->id, $json['list'][2]['id']);
$this->assertSame($discount_free->discount, $json['list'][2]['discount']);
$this->assertSame($discount_free->code, $json['list'][2]['code']);
$this->assertSame($discount_free->description, $json['list'][2]['description']);
$this->assertSame('100% - Free Account', $json['list'][2]['label']);
// A user in another tenant
$user = $this->getTestUser('user@sample-tenant.dev-local');
$response = $this->actingAs($admin)->get("api/v4/users/{$user->id}/discounts");
$response->assertStatus(200);
$json = $response->json();
$discount = Discount::withObjectTenantContext($user)->where('discount', 10)->first();
$this->assertSame(1, $json['count']);
$this->assertSame($discount->id, $json['list'][0]['id']);
$this->assertSame($discount->discount, $json['list'][0]['discount']);
$this->assertSame($discount->code, $json['list'][0]['code']);
$this->assertSame($discount->description, $json['list'][0]['description']);
$this->assertSame('10% - ' . $discount->description, $json['list'][0]['label']);
}
}
diff --git a/src/tests/Feature/Controller/Admin/StatsTest.php b/src/tests/Feature/Controller/Admin/StatsTest.php
index 14416b51..77d575c4 100644
--- a/src/tests/Feature/Controller/Admin/StatsTest.php
+++ b/src/tests/Feature/Controller/Admin/StatsTest.php
@@ -1,193 +1,211 @@
<?php
namespace Tests\Feature\Controller\Admin;
use App\Payment;
use App\Providers\PaymentProvider;
+use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class StatsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useAdminUrl();
Payment::truncate();
+ DB::table('wallets')->update(['discount_id' => null]);
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
Payment::truncate();
+ DB::table('wallets')->update(['discount_id' => null]);
parent::tearDown();
}
/**
* Test charts (GET /api/v4/stats/chart/<chart>)
*/
public function testChart(): void
{
$user = $this->getTestUser('john@kolab.org');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
// Non-admin user
$response = $this->actingAs($user)->get("api/v4/stats/chart/discounts");
$response->assertStatus(403);
// Unknown chart name
$response = $this->actingAs($admin)->get("api/v4/stats/chart/unknown");
$response->assertStatus(404);
// 'discounts' chart
$response = $this->actingAs($admin)->get("api/v4/stats/chart/discounts");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('Discounts', $json['title']);
$this->assertSame('donut', $json['type']);
$this->assertSame([], $json['data']['labels']);
$this->assertSame([['values' => []]], $json['data']['datasets']);
// '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]);
$this->assertSame([['values' => [0,0,0,0,0,0,0,0]]], $json['data']['datasets']);
// 'users' chart
$response = $this->actingAs($admin)->get("api/v4/stats/chart/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('Users - last 8 weeks', $json['title']);
$this->assertCount(8, $json['data']['labels']);
$this->assertSame(date('Y-W'), $json['data']['labels'][7]);
$this->assertCount(2, $json['data']['datasets']);
$this->assertSame('Created', $json['data']['datasets'][0]['name']);
$this->assertSame('Deleted', $json['data']['datasets'][1]['name']);
// 'users-all' chart
$response = $this->actingAs($admin)->get("api/v4/stats/chart/users-all");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('All Users - last year', $json['title']);
$this->assertCount(54, $json['data']['labels']);
$this->assertCount(1, $json['data']['datasets']);
+
+ // 'vouchers' chart
+ $discount = \App\Discount::withObjectTenantContext($user)->where('code', 'TEST')->first();
+ $wallet = $user->wallets->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+
+ $response = $this->actingAs($admin)->get("api/v4/stats/chart/vouchers");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('Vouchers', $json['title']);
+ $this->assertSame(['TEST'], $json['data']['labels']);
+ $this->assertSame([['values' => [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/Reseller/StatsTest.php b/src/tests/Feature/Controller/Reseller/StatsTest.php
index 0dbd9e4b..e4655faf 100644
--- a/src/tests/Feature/Controller/Reseller/StatsTest.php
+++ b/src/tests/Feature/Controller/Reseller/StatsTest.php
@@ -1,89 +1,109 @@
<?php
namespace Tests\Feature\Controller\Reseller;
+use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class StatsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
self::useResellerUrl();
+
+ DB::table('wallets')->update(['discount_id' => null]);
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
+ DB::table('wallets')->update(['discount_id' => null]);
+
parent::tearDown();
}
/**
* Test charts (GET /api/v4/stats/chart/<chart>)
*/
public function testChart(): void
{
$user = $this->getTestUser('john@kolab.org');
$admin = $this->getTestUser('jeroen@jeroen.jeroen');
$reseller = $this->getTestUser('reseller@' . \config('app.domain'));
// Unauth access
$response = $this->get("api/v4/stats/chart/discounts");
$response->assertStatus(401);
// Normal user
$response = $this->actingAs($user)->get("api/v4/stats/chart/discounts");
$response->assertStatus(403);
// Admin user
$response = $this->actingAs($admin)->get("api/v4/stats/chart/discounts");
$response->assertStatus(403);
// Unknown chart name
$response = $this->actingAs($reseller)->get("api/v4/stats/chart/unknown");
$response->assertStatus(404);
// 'income' chart
$response = $this->actingAs($reseller)->get("api/v4/stats/chart/income");
$response->assertStatus(404);
// 'discounts' chart
$response = $this->actingAs($reseller)->get("api/v4/stats/chart/discounts");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('Discounts', $json['title']);
$this->assertSame('donut', $json['type']);
$this->assertSame([], $json['data']['labels']);
$this->assertSame([['values' => []]], $json['data']['datasets']);
// 'users' chart
$response = $this->actingAs($reseller)->get("api/v4/stats/chart/users");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('Users - last 8 weeks', $json['title']);
$this->assertCount(8, $json['data']['labels']);
$this->assertSame(date('Y-W'), $json['data']['labels'][7]);
$this->assertCount(2, $json['data']['datasets']);
$this->assertSame('Created', $json['data']['datasets'][0]['name']);
$this->assertSame('Deleted', $json['data']['datasets'][1]['name']);
// 'users-all' chart
$response = $this->actingAs($reseller)->get("api/v4/stats/chart/users-all");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame('All Users - last year', $json['title']);
$this->assertCount(54, $json['data']['labels']);
$this->assertCount(1, $json['data']['datasets']);
+
+ // 'vouchers' chart
+ $discount = \App\Discount::withObjectTenantContext($user)->where('code', 'TEST')->first();
+ $wallet = $user->wallets->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+
+ $response = $this->actingAs($reseller)->get("api/v4/stats/chart/vouchers");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('Vouchers', $json['title']);
+ $this->assertSame(['TEST'], $json['data']['labels']);
+ $this->assertSame([['values' => [1]]], $json['data']['datasets']);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Apr 18, 10:10 AM (4 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
435616
Default Alt Text
(42 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment