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 @@
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 @@
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 @@
'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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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/)
*/
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 @@
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/)
*/
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']);
}
}